The Worker calculates results from D1 ballots unless an administrator has stored manual results for the election.
Computed result envelope
{
"election_id": "elec_123",
"ballot_type": "SIMPLE_TRIPLE",
"results": {},
"turnout": {
"total_tokens": 40,
"used_tokens": 28,
"unused_tokens": 12,
"turnout_percent": 70
}
}Turnout uses token rows, not parsed ballot rows:
turnout_percent = used_tokens / total_tokens × 100It is 0 when no tokens exist. Because token use is recorded before ballot insertion, used_tokens can exceed the number of stored ballots after a failed insert.
Yes, no, abstain
SIMPLE_TRIPLE counts the three literal payload choices:
{
"yes": 18,
"no": 7,
"abstain": 3,
"total": 28,
"yesPercent": 64.28571428571429,
"noPercent": 25,
"abstainPercent": 10.714285714285714
}Each percentage uses category / total × 100. The calculation returns raw JavaScript numbers; presentation code decides how to round them.
Malformed JSON is skipped. A parseable SIMPLE_TRIPLE payload with an unrecognized choice contributes to none of the counts.
Polls
Poll options come from the election’s candidates rows in display_order.
{
"options": [
{
"option_id": "option-a",
"option_name": "Courtyard",
"count": 14,
"percent": 70
}
],
"total_votes": 20,
"total_selections": 31,
"response_type": "multiple"
}| Field | Calculation |
|---|---|
total_votes | Parsed POLL ballot payloads |
total_selections | Distinct selections counted across those ballots |
Option count | Ballots selecting that option |
Option percent | count / total_votes × 100 |
For multiple-choice polls, option percentages can total more than 100 percent. Duplicate option ids within one stored ballot are counted once during result calculation, even though the submission validator should reject them.
A parseable poll payload with neither selection field still increments total_votes but adds no selection. Malformed JSON is skipped.
Ranked Condorcet
The implementation first builds a pairwise matrix. For every ranked ballot, a candidate receives one preference over each candidate placed below it.
flowchart TD BALLOTS["Full candidate rankings"] --> MATRIX["Pairwise preference counts"] MATRIX --> UNBEATEN{"Candidate never loses a pair?"} UNBEATEN -->|yes| DIRECT["method: condorcet"] UNBEATEN -->|no| PAIRS["Sort victories by margin, then winning votes"] PAIRS --> LOCK["Lock edges that do not create a cycle"] LOCK --> TOPO["Topological order"] TOPO --> RANKED["method: ranked_pairs"]
The direct-winner check accepts a candidate whose pairwise count is greater than or equal to every opponent’s count. A tie therefore counts as “not losing.” If more than one candidate meets that condition, the first candidate in database display order wins.
When that branch succeeds:
methodiscondorcet;winneris the first unbeaten candidate; andrankingcontains that winner followed by all remaining candidates in their original display order.
When no candidate is unbeaten, the Ranked Pairs branch:
- creates a directed edge for each non-tied pair;
- sorts edges by victory margin descending, then winning votes descending;
- skips an edge when it would create a cycle; and
- topologically sorts the locked graph, using candidate-id lexical order when multiple zero-indegree candidates are available.
The first ranked candidate is the winner. pairwiseMatrix omits tied pairs in both branches.
Implementation terminology
The direct branch is labeled
condorcetin the response, although its “never loses” test permits pairwise ties rather than requiring a strict win over every opponent.
Manual results
PUT /admin/elections/:id/manual-results stores:
{
"results": {
"contests": [
{
"label": "Council vote",
"items": [
{ "label": "YES", "votes": 18 },
{ "label": "NO", "votes": 7 }
]
}
]
},
"certified_at": "2026-07-26T14:00:00Z",
"certified_by": "Election committee",
"notes": "Imported certified count"
}Every contest and item needs a non-empty label. Votes must be non-negative integers.
For a simple election, labels YES, NO, and ABSTAIN are recognized case-insensitively after trimming; ABSTAINED and ABSTENTION also map to abstain. For a poll, the first non-empty contest becomes the option list.
Manual turnout is synthetic: total tokens and used tokens both equal the summed manual vote count, unused tokens are zero, and turnout is 100 percent when that count is nonzero. Ranked manual results do not produce a computed result envelope.
The legacy JSON route returns the stored contest payload, certification, and computed_results derived from it. Deleting manual results returns the election to D1 ballot calculation.
Cache and visibility
Computed results are cached in an in-memory Worker map for 60 seconds. A successful ballot submission clears that election’s key in the current isolate. There is no public force query parameter; route handlers request the normal cached result.
GET /e/:id/results:
- redirects HTML clients to
/e/:id; - returns JSON when
Acceptincludesapplication/json; and - returns
403before close whenhide_results_until_closeis true.
The combined election page calculates results after close, or while open when results are not hidden.
Sources
Verified against ihnyc-rc-vote commit 2451aa1.
src/utils/results.tssrc/utils/condorcet.tssrc/models/manual-results.tssrc/routes/results.tssrc/routes/vote.tssrc/templates/election-page.tsx