Counting real ballots under twelve voting methods
Eighteen people each ranked eight classic novels. This tutorial imports their ballots as a dataset and shows how different ways of tallying them do (or don't) change the outcome.
01 The question and the data
Different voting methods can elect different winners from the same ballots — that is the entire reason this tool exists. Plurality rewards first-place strength; Borda rewards broad support; Condorcet methods reward head-to-head dominance; IRV rewards surviving eliminations. voting stores one set of inputs — options, voters, ballots — and lets any of 32 method names and aliases count them, so the comparison is always apples to apples.
To be clear about what this is: voting is a research instrument — for studying group preferences, comparing counting rules, teaching social choice, and settling low-stakes group decisions (which book, which logo, which restaurant). The vocabulary is the social-choice literature's — elections, voters, ballots — but nothing here administers civic elections, and nothing should be read as claiming to.
The worked example uses real data: eighteen people each ranked the eight novels, and their full rankings live in a production EDSL Results object that this page imports — actual human preferences, not synthetic data. (How you collect such a dataset — hosted surveys, email invitations, AI personas — is its own topic: voting docs show humanize and voting docs show recipes.)
voting ballot rank). Everything else — every command and the shape of every output — is what you will see. Outputs are JSON envelopes by default; add --human for a readable rendering.02 Installation and the output contract
uv tool install git+https://github.com/expectedparrot/voting.git
(uv tool gives voting its own isolated environment on your PATH; plain pip install works too.) Confirm the install:
voting version
Show command output
{
"schema_version": "1.0",
"command": "version",
"status": "ok",
"argv": [
"voting",
"version"
],
"data": {
"version": "0.1.0",
"package_path": "~/voting/voting",
"envelope_schema_version": "1.0"
},
"warnings": [],
"errors": [],
"next_steps": []
}That envelope is the whole interface. Every command prints exactly one JSON envelope to stdout — schema_version, command, status, argv, data, warnings, errors, next_steps — with failures as structured errors and nonzero exits. Add --human to any command for tables meant for people instead. voting capabilities states the contract machine-readably, including which commands touch the outside world:
voting capabilities
Show command output
{
"schema_version": "1.0",
"command": "capabilities",
"status": "ok",
"argv": [
"voting",
"capabilities"
],
"data": {
"envelope_schema_version": "1.0",
"output_contract": "Every command prints one JSON envelope {schema_version, command, status, argv, data, warnings, errors, next_steps} to stdout by default; --human is an opt-in presentation mode.",
"execution_boundary": "voting never executes model calls. `survey generate` builds a .jobs.ep package the user executes externally with `ep run`; counting and analysis are local.",
"external_service_actions": {
"survey publish": "Creates a hosted Humanize survey via the ep CLI (outward-facing).",
"survey email": "Sends invitation emails to configured voters via the ep CLI (outward-facing).",
"survey responses": "Downloads Humanize responses via the ep CLI (network read)."
},
"workflow_commands": [
"voting status",
"voting next",
"voting docs list"
]
},
"warnings": [],
"errors": [],
"next_steps": []
}build/tutorial/captures/ when you regenerate the page.03 Set up the project
Create a project and step into it. All state lives inside the project directory as plain JSON files, so everything the tool does is inspectable and versionable — the layout is covered at the end.
voting init classic_books
Show command output
{
"schema_version": "1.0",
"command": "init",
"status": "ok",
"argv": [
"voting",
"init",
"classic_books"
],
"data": {
"project": ".",
"data_dir": ".voting",
"meta": {
"id": "classic_books",
"title": "Classic Books",
"description": "",
"created_at": "2026-07-28T14:34:15",
"settings": {
"default_tie_policy": "lexicographic",
"allow_unregistered_voters": false
}
}
},
"warnings": [],
"errors": [],
"next_steps": [
"voting option add <id> <name>",
"voting voter add <id> <name>",
"voting election add <id> <name>"
]
}cd classic_books
voting next is the orientation command: it reports the current phase and the exact commands that make sense now. It exists mostly for AI agents driving the CLI — an agent that only knows how to run voting next can navigate the whole workflow — but it is just as useful when a person comes back to a project cold:
voting next
Show command output
{
"schema_version": "1.0",
"command": "next",
"status": "ok",
"argv": [
"voting",
"next"
],
"data": {
"phase": "setup",
"counts": {
"options": 0,
"voters": 0,
"elections": 0,
"ballots": 0,
"results": 0
},
"recommendation": "voting option add <id> <name>",
"reason": "Add first option",
"checklist": [
"Add options (candidates/proposals): `voting option add <id> <name>`",
"Add voters: `voting voter add <id> <name>`",
"Optionally set voter traits for survey generation: `voting voter set-trait <id> <key> <value>`"
]
},
"warnings": [],
"errors": [],
"next_steps": [
"voting option add <id> <name>",
"voting voter add <id> <name>"
]
}04 Define the election
An election is the central object here: it names the contest, fixes the ballot type (ranked, in this case — voters order all options; other types are single choice, approval, and score), and holds the list of eligible options. Deliberately absent: a counting method. The ballot type determines what voters are asked; how the ballots are counted is a lens you apply afterwards — as many lenses as you like, which is the whole point of chapter 6. An election starts as a draft and only accepts ballots once opened:
voting election add book_preference 'Classic book preference' --ballot-type ranked
Show command output
{
"schema_version": "1.0",
"command": "election add",
"status": "ok",
"argv": [
"voting",
"election",
"add",
"book_preference",
"Classic book preference",
"--ballot-type",
"ranked"
],
"data": {
"id": "book_preference",
"name": "Classic book preference",
"description": "",
"created_at": "2026-07-28T14:34:15",
"ballot_type": "ranked",
"seats": 1,
"status": "draft",
"options": [],
"settings": {
"tie_policy": "lexicographic",
"quota": "droop"
}
},
"warnings": [],
"errors": [],
"next_steps": [
"voting election add-option book_preference <option_id>",
"voting election open book_preference"
]
}The eight books load in one step from a JSON file — id plus display name each. The display names matter: when ballots arrive from a survey, answers reference options by these exact labels, and the importer maps labels back to ids. An unknown label skips that row and reports it; nothing is ever silently guessed. (voting option add exists for adding one at a time.)
Alongside options, a project also keeps a voter roster — who is in your panel, at what weight, with what traits (personas for AI studies, emails for invitations). Ours starts empty — the respondents in the imported dataset join it automatically in chapter 5.
cat books.json
Show books.json
[
{
"id": "great_gatsby",
"name": "The Great Gatsby \u2014 Fitzgerald"
},
{
"id": "mockingbird",
"name": "To Kill a Mockingbird \u2014 Harper Lee"
},
{
"id": "nineteen_eighty_four",
"name": "1984 \u2014 Orwell"
},
{
"id": "catcher_rye",
"name": "The Catcher in the Rye \u2014 Salinger"
},
{
"id": "lord_flies",
"name": "Lord of the Flies \u2014 Golding"
},
{
"id": "mice_men",
"name": "Of Mice and Men \u2014 Steinbeck"
},
{
"id": "animal_farm",
"name": "Animal Farm \u2014 Orwell"
},
{
"id": "scarlet_letter",
"name": "The Scarlet Letter \u2014 Hawthorne"
}
]voting option import --from books.json --election book_preference
Show command output
{
"schema_version": "1.0",
"command": "option import",
"status": "ok",
"argv": [
"voting",
"option",
"import",
"--from",
"books.json",
"--election",
"book_preference"
],
"data": {
"imported": 8,
"options": [
{
"id": "great_gatsby",
"name": "The Great Gatsby — Fitzgerald"
},
{
"id": "mockingbird",
"name": "To Kill a Mockingbird — Harper Lee"
},
{
"id": "nineteen_eighty_four",
"name": "1984 — Orwell"
},
"… 5 further entries elided …"
],
"election_id": "book_preference",
"attached": [
"great_gatsby",
"mockingbird",
"nineteen_eighty_four",
"… 5 further entries elided …"
]
},
"warnings": [],
"errors": [],
"next_steps": [
"voting election open book_preference",
"voting --human election show book_preference"
]
}Open the election and look at it the way a person would — --human renders tables instead of JSON:
voting election open book_preference
Show command output
{
"schema_version": "1.0",
"command": "election open",
"status": "ok",
"argv": [
"voting",
"election",
"open",
"book_preference"
],
"data": {
"ballot_type": "ranked",
"created_at": "2026-07-28T14:34:15",
"description": "",
"id": "book_preference",
"name": "Classic book preference",
"options": [
"great_gatsby",
"mockingbird",
"nineteen_eighty_four",
"catcher_rye",
"lord_flies",
"mice_men",
"animal_farm",
"scarlet_letter"
],
"seats": 1,
"settings": {
"quota": "droop",
"tie_policy": "lexicographic"
},
"status": "open",
"open_at": "2026-07-28T14:34:15"
},
"warnings": [],
"errors": [],
"next_steps": [
"voting ballot rank book_preference <voter_id> <opt1> <opt2> ...",
"voting survey generate book_preference"
]
}voting --human election show book_preference
| Name | Classic book preference |
| Ballot type | ranked |
| Status | open |
| Seats | 1 |
| Options (8) | The Great Gatsby — Fitzgerald, To Kill a Mockingbird — Harper Lee, 1984 — Orwell, The Catcher in the Rye — Salinger, Lord of the Flies — Golding, Of Mice and Men — Steinbeck, Animal Farm — Orwell, The Scarlet Letter — Hawthorne |
05 Import real ballots
The eighteen rankings are a dataset: an EDSL Results object on the Expected Parrot platform. ballot import reads one directly — from a local .ep package (--from-results) or by UUID (--from-coop, a network read using your EDSL credentials) — and converts each response into a ballot: display labels map back to option ids (an unknown label is itemized, never guessed), and --register-voters adds each respondent to the project's voter roster at weight 1.0, keeping the original respondent id as provenance:
voting ballot import --election book_preference \
--from-coop 917f9e19-d477-43cf-bdf0-664a1592400f --register-voters
Show command output
{
"schema_version": "1.0",
"command": "ballot import",
"status": "ok",
"argv": [
"voting",
"ballot",
"import",
"--election",
"book_preference",
"--from-coop",
"917f9e19-d477-43cf-bdf0-664a1592400f",
"--register-voters"
],
"data": {
"election_id": "book_preference",
"cast": 18,
"skipped": 0,
"skipped_detail": [],
"source_file": "coop:917f9e19-d477-43cf-bdf0-664a1592400f"
},
"warnings": [
{
"code": "voter_registered",
"voter_id": "r_95fa122e_8606_4b48_8903_a342103fedb3"
},
{
"code": "voter_registered",
"voter_id": "r_02a2ae52_9765_414d_9d2e_9cfc19942690"
},
"… 16 further warnings elided (18 total) …"
],
"errors": [],
"next_steps": [
"voting count run book_preference --method <method>"
]
}voting voter add v1 'Voter One'
voting ballot rank book_preference v1 nineteen_eighty_four mockingbird great_gatsby
Or run your own survey (voting survey publish) — or have AI personas vote: voting survey generate builds a job you execute with ep run and import with --from-results.One habit worth keeping: validate before counting. valid_ballots is the number of ballots that will actually enter a tally — anything else (a respondent not on the roster, an unknown option, a duplicate ranking) is itemized as a warning and left out rather than guessed at. (Without --register-voters, for instance, all eighteen ballots would have been recorded but none would count until you decided whose responses belong in the analysis.)
voting ballot validate book_preference
Show command output
{
"schema_version": "1.0",
"command": "ballot validate",
"status": "ok",
"argv": [
"voting",
"ballot",
"validate",
"book_preference"
],
"data": {
"valid_ballots": 18,
"warnings": []
},
"warnings": [],
"errors": [],
"next_steps": [
"voting count run book_preference --method <method>"
]
}Eighteen countable ballots. Here is the dataset — one row per respondent, top choices first:
voting --human ballot list --election book_preference
| voter | ballot (top choices) | recorded |
|---|---|---|
| r_95fa122e_86… | nineteen_eighty_four > lord_flies > scarlet_letter … (+5 more) | 2026-07-28 14:34:17 |
| r_02a2ae52_97… | catcher_rye > nineteen_eighty_four > great_gatsby … (+5 more) | 2026-07-28 14:34:17 |
| r_6785ca96_b4… | nineteen_eighty_four > animal_farm > great_gatsby … (+5 more) | 2026-07-28 14:34:17 |
| r_253c40f1_32… | nineteen_eighty_four > mice_men > mockingbird … (+5 more) | 2026-07-28 14:34:17 |
| r_2fbe4384_33… | mice_men > great_gatsby > lord_flies … (+5 more) | 2026-07-28 14:34:17 |
| r_0f85628b_50… | nineteen_eighty_four > scarlet_letter > catcher_rye … (+5 more) | 2026-07-28 14:34:17 |
| b29fe300_0612… | nineteen_eighty_four > animal_farm > mice_men … (+5 more) | 2026-07-28 14:34:17 |
| r_60abb9b9_1c… | nineteen_eighty_four > animal_farm > catcher_rye … (+5 more) | 2026-07-28 14:34:17 |
| r_4e8ddeca_81… | mockingbird > mice_men > nineteen_eighty_four … (+5 more) | 2026-07-28 14:34:17 |
| r_51e15daa_72… | mockingbird > nineteen_eighty_four > lord_flies … (+5 more) | 2026-07-28 14:34:17 |
| b2d6c619_8c5e… | nineteen_eighty_four > great_gatsby > mockingbird … (+5 more) | 2026-07-28 14:34:17 |
| r_2dcf9550_88… | catcher_rye > great_gatsby > mice_men … (+5 more) | 2026-07-28 14:34:17 |
| r_866b5ae1_22… | mockingbird > great_gatsby > scarlet_letter … (+5 more) | 2026-07-28 14:34:17 |
| abb414e9_cd1e… | nineteen_eighty_four > animal_farm > lord_flies … (+5 more) | 2026-07-28 14:34:17 |
| r_7cb11170_5a… | mockingbird > nineteen_eighty_four > mice_men … (+5 more) | 2026-07-28 14:34:17 |
| r_5b8d9c86_19… | great_gatsby > scarlet_letter > mockingbird … (+5 more) | 2026-07-28 14:34:17 |
| r_7df8a708_8e… | great_gatsby > nineteen_eighty_four > lord_flies … (+5 more) | 2026-07-28 14:34:17 |
| cd27fba8_b7ee… | animal_farm > catcher_rye > great_gatsby … (+5 more) | 2026-07-28 14:34:17 |
Eighteen full rankings are hard to eyeball. The built-in plots turn them into a picture — voting plot ranks shows, for each book, how many voters placed it first, second, and so on (hover any segment for the exact count):
voting plot ranks book_preference
Show command output
{
"schema_version": "1.0",
"command": "plot ranks",
"status": "ok",
"argv": [
"voting",
"plot",
"ranks",
"book_preference"
],
"data": {
"election_id": "book_preference",
"ballots": 18,
"path": ".voting/output/plots/ranks_book_preference.svg",
"format": "svg"
},
"warnings": [],
"errors": [],
"next_steps": [
"open .voting/output/plots/ranks_book_preference.svg"
]
}voting status is the project's dashboard — 8 options, 18 voters, 18 ballots, and a phase saying counting is what remains:
voting status
Show command output
{
"schema_version": "1.0",
"command": "status",
"status": "ok",
"argv": [
"voting",
"status"
],
"data": {
"phase": "counting",
"project_exists": true,
"counts": {
"options": 8,
"voters": 18,
"elections": 1,
"ballots": 18,
"results": 0
},
"checklist": [
"Run a counting method: `voting count run <election_id> --method <method>`",
"Run multiple methods for comparison: `voting count run <election_id> --method <method>`"
],
"recommended_next_steps": [
{
"label": "Count the ballots",
"command": "voting count run <election_id> --method <method>"
}
]
},
"warnings": [],
"errors": [],
"next_steps": [
"voting count run <election_id> --method <method>"
]
}06 Every method, one answer
Now the point of the exercise. Counts name their method explicitly (voting count run <id> --method borda) — there is no default, and that is a feature: a "winner" is always a method's winner. But nobody should have to type twelve commands to ask the obvious question. voting count compare counts the same ballots under every method that can read them — for ranked ballots, all 12 — and saves each result:
voting count compare book_preference
Show command output
{
"schema_version": "1.0",
"command": "count compare",
"status": "ok",
"argv": [
"voting",
"count",
"compare",
"book_preference"
],
"data": {
"election_id": "book_preference",
"ballot_type": "ranked",
"methods_run": [
"borda",
"irv",
"stv",
"… 9 further entries elided …"
],
"results": [
{
"method": "borda",
"result_id": "20260728T183418670409Z_f264c771_book_preference_borda",
"winners": [
"nineteen_eighty_four"
],
"runner_up": "mockingbird"
},
{
"method": "irv",
"result_id": "20260728T183418670959Z_9235f748_book_preference_irv",
"winners": [
"nineteen_eighty_four"
],
"runner_up": "mockingbird"
},
{
"method": "stv",
"result_id": "20260728T183418671304Z_3c6e721b_book_preference_stv",
"winners": [
"nineteen_eighty_four"
],
"runner_up": "mockingbird"
},
"… 9 further entries elided …"
],
"no_winner": [
"simple_majority"
],
"unanimous_winners": [
"nineteen_eighty_four"
],
"summary": {
"valid_ballots": 18,
"invalid_ballots": 0
}
},
"warnings": [],
"errors": [],
"next_steps": [
"voting plot methods --election book_preference",
"voting count show 20260728T183418670409Z_f264c771_book_preference_borda"
]
}voting --human count list --election book_preference
| method | winner | runner-up |
|---|---|---|
| borda | nineteen_eighty_four | mockingbird |
| irv | nineteen_eighty_four | mockingbird |
| stv | nineteen_eighty_four | mockingbird |
| schulze | nineteen_eighty_four | great_gatsby |
| copeland | nineteen_eighty_four | great_gatsby |
| minimax | nineteen_eighty_four | mockingbird |
| ranked_pairs | nineteen_eighty_four | great_gatsby |
| kemeny_young | nineteen_eighty_four | great_gatsby |
| bucklin | nineteen_eighty_four | great_gatsby |
| runoff | nineteen_eighty_four | mockingbird |
| fptp | nineteen_eighty_four | mockingbird |
| simple_majority | (no winner) | mockingbird |
11 of 12 methods elect 1984. The twelfth refuses to answer — and its refusal is worth reading. simple_majority requires an outright majority of first preferences; nineteen_eighty_four holds 8 of 18, a plurality but not a majority, so the method reports winners: [] with the threshold it applied. Like the valid_ballots check in chapter 5, an honest refusal beats a fabricated answer.
Every comparison run is a full saved record of how its method reasoned. Open a few with count show. Borda first — each ballot position contributes points, so it measures breadth of support:
voting count show 20260728T183418670409Z_f264c771_book_preference_borda
Show command output
{
"schema_version": "1.0",
"command": "count show",
"status": "ok",
"argv": [
"voting",
"count",
"show",
"20260728T183418670409Z_f264c771_book_preference_borda"
],
"data": {
"created_at": "2026-07-28T14:34:18",
"election_id": "book_preference",
"id": "20260728T183418670409Z_f264c771_book_preference_borda",
"method": "borda",
"ranking": [
{
"option_id": "nineteen_eighty_four",
"rank": 1,
"score": 93.0,
"status": "elected"
},
{
"option_id": "mockingbird",
"rank": 2,
"score": 79.0,
"status": "defeated"
},
{
"option_id": "great_gatsby",
"rank": 3,
"score": 73.0,
"status": "defeated"
},
"… 5 further entries elided …"
],
"rounds": [],
"scores": [
{
"option_id": "nineteen_eighty_four",
"total": 93.0
},
{
"option_id": "mockingbird",
"total": 79.0
},
{
"option_id": "great_gatsby",
"total": 73.0
},
"… 5 further entries elided …"
],
"settings": {
"quota": "droop",
"seats": 1,
"tie_policy": "lexicographic"
},
"summary": {
"exhausted_weight": 0.0,
"invalid_ballots": 0,
"total_valid_weight": 18.0,
"valid_ballots": 18
},
"warnings": [],
"winners": [
"nineteen_eighty_four"
]
},
"warnings": [],
"errors": [],
"next_steps": []
}voting --human count show 20260728T183418670409Z_f264c771_book_preference_borda
| rank | option | score | status |
|---|---|---|---|
| 1 | nineteen_eighty_four | 93 | elected |
| 2 | mockingbird | 79 | defeated |
| 3 | great_gatsby | 73 | defeated |
| 4 | mice_men | 65 | defeated |
| 5 | animal_farm | 62 | defeated |
| 6 | catcher_rye | 51 | defeated |
| 7 | lord_flies | 41 | defeated |
| 8 | scarlet_letter | 40 | defeated |
voting plot scores 20260728T183418670409Z_f264c771_book_preference_borda
Show command output
{
"schema_version": "1.0",
"command": "plot scores",
"status": "ok",
"argv": [
"voting",
"plot",
"scores",
"20260728T183418670409Z_f264c771_book_preference_borda"
],
"data": {
"result_id": "20260728T183418670409Z_f264c771_book_preference_borda",
"method": "borda",
"path": ".voting/output/plots/scores_20260728T183418670409Z_f264c771_book_preference_borda.svg",
"format": "svg"
},
"warnings": [],
"errors": [],
"next_steps": [
"open .voting/output/plots/scores_20260728T183418670409Z_f264c771_book_preference_borda.svg"
]
}Borda gives nineteen_eighty_four 93 points against mockingbird's 79. Instant-runoff reasons completely differently — eliminate the weakest option, retry until someone holds a majority — and its rounds array is that elimination story:
voting count show 20260728T183418670959Z_9235f748_book_preference_irv
Show command output
{
"schema_version": "1.0",
"command": "count show",
"status": "ok",
"argv": [
"voting",
"count",
"show",
"20260728T183418670959Z_9235f748_book_preference_irv"
],
"data": {
"created_at": "2026-07-28T14:34:18",
"election_id": "book_preference",
"exhausted_weight": 0.0,
"id": "20260728T183418670959Z_9235f748_book_preference_irv",
"method": "irv",
"ranking": [
{
"option_id": "nineteen_eighty_four",
"rank": 1,
"status": "elected"
},
{
"option_id": "mockingbird",
"rank": 2,
"status": "eliminated"
},
{
"option_id": "catcher_rye",
"rank": 3,
"status": "eliminated"
},
"… 5 further entries elided …"
],
"rounds": [
{
"eliminated": "lord_flies",
"exhausted_weight": 0.0,
"totals": [
{
"option_id": "nineteen_eighty_four",
"total": 8.0
},
{
"option_id": "mockingbird",
"total": 4.0
},
"… 6 further entries elided …"
]
},
{
"eliminated": "scarlet_letter",
"exhausted_weight": 0.0,
"totals": [
{
"option_id": "nineteen_eighty_four",
"total": 8.0
},
{
"option_id": "mockingbird",
"total": 4.0
},
"… 5 further entries elided …"
]
},
{
"eliminated": "animal_farm",
"exhausted_weight": 0.0,
"totals": [
{
"option_id": "nineteen_eighty_four",
"total": 8.0
},
{
"option_id": "mockingbird",
"total": 4.0
},
"… 4 further entries elided …"
]
},
"… 4 further entries elided …"
],
"scores": [
{
"option_id": "nineteen_eighty_four",
"total": 12.0
},
{
"option_id": "great_gatsby",
"total": 6.0
},
{
"option_id": "animal_farm",
"total": 0.0
},
"… 5 further entries elided …"
],
"settings": {
"quota": "droop",
"seats": 1,
"tie_policy": "lexicographic"
},
"summary": {
"exhausted_weight": 0.0,
"invalid_ballots": 0,
"total_valid_weight": 18.0,
"valid_ballots": 18
},
"warnings": [],
"winners": [
"nineteen_eighty_four"
]
},
"warnings": [],
"errors": [],
"next_steps": []
}Schulze (a Condorcet method) plays every option against every other; the pairwise matrix holds all 28 head-to-head margins, and voting plot pairwise makes it legible at a glance:
voting count show 20260728T183418671779Z_78e228ae_book_preference_schulze
Show command output
{
"schema_version": "1.0",
"command": "count show",
"status": "ok",
"argv": [
"voting",
"count",
"show",
"20260728T183418671779Z_78e228ae_book_preference_schulze"
],
"data": {
"created_at": "2026-07-28T14:34:18",
"election_id": "book_preference",
"id": "20260728T183418671779Z_78e228ae_book_preference_schulze",
"method": "schulze",
"pairwise": [
{
"a": "great_gatsby",
"a_over_b": 9.0,
"b": "mockingbird",
"b_over_a": 9.0,
"…": "3 further keys elided"
},
{
"a": "great_gatsby",
"a_over_b": 6.0,
"b": "nineteen_eighty_four",
"b_over_a": 12.0,
"…": "3 further keys elided"
},
{
"a": "great_gatsby",
"a_over_b": 12.0,
"b": "lord_flies",
"b_over_a": 6.0,
"…": "3 further keys elided"
},
"… 25 further entries elided …"
],
"path_strengths": {
"animal_farm": {
"catcher_rye": 10.0,
"great_gatsby": 0.0,
"lord_flies": 13.0,
"mice_men": 0.0,
"…": "3 further keys elided"
},
"catcher_rye": {
"animal_farm": 0.0,
"great_gatsby": 0.0,
"lord_flies": 0.0,
"mice_men": 0.0,
"…": "3 further keys elided"
},
"great_gatsby": {
"animal_farm": 10.0,
"catcher_rye": 11.0,
"lord_flies": 12.0,
"mice_men": 10.0,
"…": "3 further keys elided"
},
"lord_flies": {
"animal_farm": 0.0,
"catcher_rye": 0.0,
"great_gatsby": 0.0,
"mice_men": 0.0,
"…": "3 further keys elided"
},
"mice_men": {
"animal_farm": 0.0,
"catcher_rye": 11.0,
"great_gatsby": 0.0,
"lord_flies": 13.0,
"…": "3 further keys elided"
},
"mockingbird": {
"animal_farm": 12.0,
"catcher_rye": 13.0,
"great_gatsby": 0.0,
"lord_flies": 14.0,
"…": "3 further keys elided"
},
"nineteen_eighty_four": {
"animal_farm": 13.0,
"catcher_rye": 12.0,
"great_gatsby": 12.0,
"lord_flies": 16.0,
"…": "3 further keys elided"
},
"scarlet_letter": {
"animal_farm": 0.0,
"catcher_rye": 0.0,
"great_gatsby": 0.0,
"lord_flies": 0.0,
"…": "3 further keys elided"
}
},
"ranking": [
{
"option_id": "nineteen_eighty_four",
"rank": 1,
"status": "elected"
},
{
"option_id": "great_gatsby",
"rank": 2,
"status": "defeated"
},
{
"option_id": "mockingbird",
"rank": 3,
"status": "defeated"
},
"… 5 further entries elided …"
],
"rounds": [],
"settings": {
"quota": "droop",
"seats": 1,
"tie_policy": "lexicographic"
},
"summary": {
"exhausted_weight": 0.0,
"invalid_ballots": 0,
"total_valid_weight": 18.0,
"valid_ballots": 18
},
"warnings": [],
"winners": [
"nineteen_eighty_four"
]
},
"warnings": [],
"errors": [],
"next_steps": []
}voting plot pairwise 20260728T183418671779Z_78e228ae_book_preference_schulze
Show command output
{
"schema_version": "1.0",
"command": "plot pairwise",
"status": "ok",
"argv": [
"voting",
"plot",
"pairwise",
"20260728T183418671779Z_78e228ae_book_preference_schulze"
],
"data": {
"result_id": "20260728T183418671779Z_78e228ae_book_preference_schulze",
"method": "schulze",
"path": ".voting/output/plots/pairwise_20260728T183418671779Z_78e228ae_book_preference_schulze.svg",
"format": "svg"
},
"warnings": [],
"errors": [],
"next_steps": [
"open .voting/output/plots/pairwise_20260728T183418671779Z_78e228ae_book_preference_schulze.svg"
]
}The rest tell the same story in different dialects: copeland scores those pairings as wins minus losses, kemeny_young searches for the ordering that agrees with the most pairwise judgments, bucklin keeps adding voters' next choices until someone crosses half, runoff stages a two-option finale (nineteen_eighty_four vs mockingbird), and fptp throws away everything but the first line. Their envelopes are all in count list:
voting count list
Show command output
{
"schema_version": "1.0",
"command": "count list",
"status": "ok",
"argv": [
"voting",
"count",
"list"
],
"data": {
"results": [
{
"created_at": "2026-07-28T14:34:18",
"election_id": "book_preference",
"id": "20260728T183418670409Z_f264c771_book_preference_borda",
"method": "borda",
"…": "7 further keys elided"
},
{
"created_at": "2026-07-28T14:34:18",
"election_id": "book_preference",
"exhausted_weight": 0.0,
"id": "20260728T183418670959Z_9235f748_book_preference_irv",
"…": "8 further keys elided"
},
{
"created_at": "2026-07-28T14:34:18",
"election_id": "book_preference",
"exhausted_weight": 0.0,
"id": "20260728T183418671304Z_3c6e721b_book_preference_stv",
"…": "9 further keys elided"
},
"… 9 further entries elided …"
]
},
"warnings": [],
"errors": [],
"next_steps": []
}And the thesis of this whole page, as one picture — voting plot methods grids every option's finishing position under every saved count:
voting plot methods --election book_preference
Show command output
{
"schema_version": "1.0",
"command": "plot methods",
"status": "ok",
"argv": [
"voting",
"plot",
"methods",
"--election",
"book_preference"
],
"data": {
"election": "book_preference",
"results": 12,
"methods": [
"borda",
"bucklin",
"copeland",
"… 9 further entries elided …"
],
"path": ".voting/output/plots/methods_book_preference.svg",
"format": "svg"
},
"warnings": [],
"errors": [],
"next_steps": [
"open .voting/output/plots/methods_book_preference.svg"
]
}Every method that names a winner names 1984. That unanimity is itself the finding. Orwell's novel holds 8 of 18 first preferences, the top Borda score, and beats every rival head-to-head. When a candidate dominates like this, the choice of method cannot change the outcome; method choice matters exactly when support is fragmented, and these ballots are not fragmented at the top. (Beneath the winner the orderings do shuffle — compare the ranking arrays across the saved results.)
07 Where everything lives
Every entity the project holds is a small JSON file under the project's .voting/ directory: options, voters, ballots (append-only records; re-imports warn ballot_overwritten), and one saved result per count, so comparisons never overwrite each other — all inspectable with nothing but cat. next closes the loop:
voting next
Show command output
{
"schema_version": "1.0",
"command": "next",
"status": "ok",
"argv": [
"voting",
"next"
],
"data": {
"phase": "done",
"counts": {
"options": 8,
"voters": 18,
"elections": 1,
"ballots": 18,
"results": 12
},
"recommendation": "voting count list",
"reason": "View results",
"checklist": [
"Review results: `voting count list` and `voting count show <result_id>`",
"Run additional methods for comparison: `voting count run <election_id> --method <method>`"
]
},
"warnings": [],
"errors": [],
"next_steps": [
"voting count list",
"voting count run <election_id> --method <method>"
]
}| Need | Command family |
|---|---|
| Discover the workflow | version, capabilities, status, next, docs list |
| Define options and voters | option add|import, voter add, election add|add-option|open |
| Collect ballots | ballot rank|cast|approve|score, survey generate, survey humanize|publish|email|responses |
| Import and audit | ballot import --from|--from-results|--from-coop [--register-voters], ballot validate, ballot list |
| Count and compare | count run [--method], count list, count show |
Exact options and defaults belong to voting <command> --help; the README's generated command reference lists all 51 commands and is enforced against the CLI by tests/test_contract_sync.py.
That is the whole loop. The natural next step is to run it on a question you actually care about: define your options, publish the survey to the people whose answer matters (or let AI personas vote via survey generate and ep run), and — before reading any winner — check ballot validate, then count it more than one way. If all the methods agree, you have a robust answer. If they disagree, you have something more interesting.