Skip to content

VxAdmin: Refactor adjudication data models#8211

Merged
nikhilb4a merged 11 commits intomainfrom
nikhil/admin-adjudication-refactor
Apr 2, 2026
Merged

VxAdmin: Refactor adjudication data models#8211
nikhilb4a merged 11 commits intomainfrom
nikhil/admin-adjudication-refactor

Conversation

@nikhilb4a
Copy link
Copy Markdown
Contributor

@nikhilb4a nikhilb4a commented Mar 30, 2026

Overview

Closes #8188

Follow up to #8080

This PR refactors our data models related to adjudication. In the previous PR, I added in CvrTags to mirror CvrContestTags, to indicate if a blank ballot has been adjudicated or not through a resolved flag. This PR cleans this up by dropping CvrTags and adding the isResolved flag to the CVR, letting the cvr.adjudication_flags play the role that the CvrTag was playing, as they already were being set on every CVR at import time.

Additionally, this PR folds in the VoteAdjudications table into the CVR table under adjudicatedVotes, where the presence of a contest key to votes value indicates that a contest has been adjudicated. This removes the need for the isResolved on CvrContestTags, making that table unnecessary as well, as the remaining fields from the tag can be derived whenever needed.

Probably easiest to review commit by commit. Last two commits do a bit of code cleanup, but only minor changes.

Demo Video or Screenshot

Backend only, except for now a CVR must be "Accepted" to have it count as resolved. Previously only blank ballots had that property, and other CVRs were considered resolved once all contests were resolved.

Testing Plan

Automated tests added/extended. Manual testing.

Checklist

  • I have prefixed my PR title with "VxDesign: ", "VxPollBook: ", or "HWTA: " if my change is specific to one of those products.
  • I have added logging where appropriate for any new user actions.
  • I have added the "user-facing-change" label to this PR, if relevant, to automate an announcement in #machine-product-updates.

@nikhilb4a nikhilb4a force-pushed the nikhil/admin-adjudication-refactor branch 3 times, most recently from 13fb2cc to cd8b8c3 Compare March 30, 2026 23:43
@nikhilb4a nikhilb4a changed the title admin/backend: Consolidate vote_adjudications table into cvr.adjudica… VxAdmin: Refactor adjudication data models Mar 30, 2026
@nikhilb4a nikhilb4a force-pushed the nikhil/admin-adjudication-refactor branch from cd8b8c3 to 20a370e Compare March 30, 2026 23:49
…ted_votes; make adjudicateWriteIn private, use adjudicateCvrContest instead
… of cvr contest tags; make tag required on ballotAdjudicationData now that isResolved lives on every cvr
@nikhilb4a nikhilb4a force-pushed the nikhil/admin-adjudication-refactor branch from aa63d4e to d671f6f Compare March 31, 2026 20:11
@nikhilb4a nikhilb4a requested a review from Copilot March 31, 2026 20:13
@nikhilb4a nikhilb4a force-pushed the nikhil/admin-adjudication-refactor branch from d671f6f to ea56e67 Compare March 31, 2026 20:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors VxAdmin’s adjudication data model by moving adjudication state into the cvrs table (flags + adjudicated_votes + is_resolved) and removing the separate tag/adjudication tables, while also extending reporting support for marginal marks.

Changes:

  • Add a new adjudication flag (hasMarginalMark) and propagate it through reporting UI + backend report titles.
  • Replace cvr_tags/cvr_contest_tags/vote_adjudications with cvrs.adjudicated_votes, cvrs.has_marginal_mark, and cvrs.is_resolved, updating store logic and adjudication workflow accordingly.
  • Update/extend backend and frontend tests to match the new models and adjudication flow.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
libs/types/src/admin/reporting.ts Adds hasMarginalMark to adjudication flag enums/labels used across reporting.
apps/admin/frontend/src/utils/reporting.ts Adds filename prefix handling for hasMarginalMark reporting filters.
apps/admin/frontend/src/screens/contest_adjudication_screen.test.tsx Updates contest tag test fixtures to match updated tag shape.
apps/admin/frontend/src/screens/ballot_adjudication_screen.test.tsx Updates ballot adjudication fixtures (e.g., BallotAdjudicationData.tag now required).
apps/admin/frontend/src/hooks/use_contest_adjudication_state.test.ts Updates tag fixtures to align with removed source field.
apps/admin/backend/vitest.config.ts Adjusts coverage thresholds.
apps/admin/backend/test/mock_cvr_file.ts Updates mock CVR import helper to compute/store new adjudication flags and stop writing removed tag tables.
apps/admin/backend/src/util/cast_vote_records.ts Adds marginal mark flag computation + introduces doesCvrNeedAdjudication and contest-tag derivation helper.
apps/admin/backend/src/util/cast_vote_records.test.ts Adds/updates tests for marginal mark flagging and new helper functions.
apps/admin/backend/src/types.ts Updates adjudication-related types (e.g. remove contest tag source, make ballot tag required, remove reset action).
apps/admin/backend/src/tabulation/full_results.test.ts Updates write-in adjudication test helper to work via adjudicateCvrContest + adjudicated_votes.
apps/admin/backend/src/tabulation/card_counts.test.ts Extends card count filtering tests to cover hasMarginalMark.
apps/admin/backend/src/store.ts Moves queue/tag/adjudication logic onto cvrs columns and adjudicated_votes.
apps/admin/backend/src/reports/titles.ts Adds report title generation for hasMarginalMark filter.
apps/admin/backend/src/reports/titles.test.ts Adds test coverage for marginal mark report titles.
apps/admin/backend/src/cast_vote_records.ts Stops generating/storing removed tag records; uses doesCvrNeedAdjudication to decide image persistence.
apps/admin/backend/src/app.adjudication.test.ts Updates adjudication tests to resolve ballots via new resolveBallotTags behavior.
apps/admin/backend/src/adjudication.ts Refactors adjudication to write adjudicated_votes instead of vote_adjudications/tags; simplifies resolve path.
apps/admin/backend/src/adjudication.test.ts Updates adjudication tests for new persistence model and queue behavior.
apps/admin/backend/schema.sql Adds new CVR columns and removes old tag/adjudication tables + updates triggers.

Comment thread apps/admin/backend/src/util/cast_vote_records.ts Outdated
Comment thread apps/admin/backend/src/store.ts
Comment thread apps/admin/backend/src/store.ts
Comment thread apps/admin/backend/src/store.ts
Comment thread apps/admin/backend/src/adjudication.ts Outdated
Comment thread apps/admin/backend/src/tabulation/card_counts.test.ts
Comment thread apps/admin/frontend/src/utils/reporting.ts Outdated
…ive contest tags from adjudicated state

Move hasMarginalMark computation into getCastVoteRecordAdjudicationFlags
alongside other flags. Add hasMarginalMark to the shared
CastVoteRecordAdjudicationFlag type, which also enables it as a reporting
filter. In deriveCvrContestTag, detect when adjudicated votes differ
from scanned votes to set hasMarginalMark and recalculate over/undervote
status, covering cases where a user corrects issues the scanner missed.
Remove unused source field from CvrContestTag.
@nikhilb4a nikhilb4a force-pushed the nikhil/admin-adjudication-refactor branch 3 times, most recently from 7bf0bd3 to 73779d3 Compare March 31, 2026 21:39
…tion action type since it is no longer possible; only save marginally marked ballot images if system setting is set, add regression test
…ved, shorten test helper, split up adjudicateCvrContest into vote handling then write-in handling
@nikhilb4a nikhilb4a force-pushed the nikhil/admin-adjudication-refactor branch from 73779d3 to eb744c4 Compare March 31, 2026 21:55
@nikhilb4a nikhilb4a force-pushed the nikhil/admin-adjudication-refactor branch from eb744c4 to 85ce4a4 Compare March 31, 2026 22:19
@nikhilb4a nikhilb4a marked this pull request as ready for review March 31, 2026 22:20
@nikhilb4a nikhilb4a requested a review from jonahkagan March 31, 2026 22:20
Copy link
Copy Markdown
Contributor

@jonahkagan jonahkagan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, seems like things got a good bit simpler!

Comment thread apps/admin/backend/schema.sql Outdated
const existingContestTag = this.byContestId.get(contestId);
if (existingContestTag) {
return existingContestTag;
export function deriveCvrContestTag({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function seems to duplicate a lot of the logic of getCastVoteRecordAdjudicationFlags. Can they be unified?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, there is a good amount of overlap. The high level difference is that getCastVoteRecordAdjudicationFlags operates on the CVR level, while deriveCvrContestTag is on the CVR-contest level.

I could, for example, keep the core logic in deriveCvrContestTag and then aggregate across contests in getCastVoteRecordAdjudicationFlags. That would be more work at import time though. deriveCvrContestTag also takes into account adjudicated status in its current implementation.

Did a way to unify them stand out to you?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also noting one other difference is that deriveCvrContestTag takes adminAdjudicationReasons into account, while getCastVoteRecordAdjudicationFlags flags CVRs regardless of the adjudication reasons. That's how it was set up for reporting, and Drew confirmed that's how it should be.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a sketch: 72ae082

Comment thread apps/admin/backend/src/util/cast_vote_records.ts Outdated
@nikhilb4a nikhilb4a merged commit 641f5ce into main Apr 2, 2026
59 checks passed
@nikhilb4a nikhilb4a deleted the nikhil/admin-adjudication-refactor branch April 2, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VxAdmin: consolidate cvr.adjudication_flags and cvr_tags. Settle on plan for schema updates to vote_adjudications, cvr_contest_tags

3 participants