VxAdmin: Refactor adjudication data models#8211
Conversation
13fb2cc to
cd8b8c3
Compare
cd8b8c3 to
20a370e
Compare
…ted_votes; make adjudicateWriteIn private, use adjudicateCvrContest instead
… of cvr contest tags; make tag required on ballotAdjudicationData now that isResolved lives on every cvr
… ballot adjudication data
aa63d4e to
d671f6f
Compare
d671f6f to
ea56e67
Compare
There was a problem hiding this comment.
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_adjudicationswithcvrs.adjudicated_votes,cvrs.has_marginal_mark, andcvrs.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. |
…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.
7bf0bd3 to
73779d3
Compare
…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
73779d3 to
eb744c4
Compare
eb744c4 to
85ce4a4
Compare
jonahkagan
left a comment
There was a problem hiding this comment.
Nice work, seems like things got a good bit simpler!
| const existingContestTag = this.byContestId.get(contestId); | ||
| if (existingContestTag) { | ||
| return existingContestTag; | ||
| export function deriveCvrContestTag({ |
There was a problem hiding this comment.
This function seems to duplicate a lot of the logic of getCastVoteRecordAdjudicationFlags. Can they be unified?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
…ion flag functions; use deepEqual
Overview
Closes #8188
Follow up to #8080
This PR refactors our data models related to adjudication. In the previous PR, I added in
CvrTagsto mirrorCvrContestTags, to indicate if a blank ballot has been adjudicated or not through a resolved flag. This PR cleans this up by droppingCvrTagsand adding theisResolvedflag to the CVR, letting thecvr.adjudication_flagsplay the role that theCvrTagwas playing, as they already were being set on every CVR at import time.Additionally, this PR folds in the
VoteAdjudicationstable into theCVRtable underadjudicatedVotes, where the presence of a contest key to votes value indicates that a contest has been adjudicated. This removes the need for theisResolvedonCvrContestTags, 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