Public: anyone can read this; the crew can change it.

Three quorum rules as one pure module: majority, all, a named few #11

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`src/quorum.ts` is the whole change, plus its test: two new files, nothing else in the repository touched, `bun test` green (32 tests, 14 of them new) and no new dependency.

shipped production is serving it deploy 2adbe7d

opened this

src/quorum.ts is the whole change, plus its test: two new files, nothing else in the repository touched, bun test green (32 tests, 14 of them new) and no new dependency.

The module is pure data in, pure data out — a roster of person ids, a list of option ids and a list of ballots go in; {decided, winner?, counts, needed, of, reason} comes out. parseRule is the only reader of the stored rule text (majority, all, named:alice,bob,carol:2) and returns undefined rather than throwing for an unknown kind, an empty or repeated name, a name over 64 characters or outside [A-Za-z0-9_.-], more than 32 names, a needed outside 1..names.length, text longer than any rule this file can write, and anything that is not a string. A majority is Math.floor(of / 2) + 1 — integers throughout, no float comparison anywhere. reason is built only from integers and sentences the module owns, so a page can print it without escaping surprises.

Worth looking at: counts is an array in the order the options were given, one row per option id with votes (every ballot, whoever cast it) and counting (only ballots that move the decision) — that split is how an outsider's vote stays visible without touching decided. A named rule is measured against its names, not the roster, per the issue's wording; keeping those names inside the roster is the caller's job and I said so in the module's doc comment, since enforcing it here would need a roster the rule text does not have. I added two small things beyond the checklist and would rather flag them than hide them: formatRule, so the caller has one canonical spelling to store and the tests can round-trip, and a last-ballot-wins fold so a person appearing twice counts once (the ballot table's UNIQUE (proposal_id, voter_id) in issue #5 makes that unreachable in practice, but the function is total either way). I chose not to touch actions.ts, db.ts or any page: wiring this into a vote is issue #5, which waits on this one.

Nothing to click in the preview — this lands no route. bun test src/quorum.test.ts is the thing to read: the table names each refusal, and every rule is exercised just below, exactly at and above its threshold, along with a tie, an outsider's ballot, an empty roster and a roster of one.

🤖 Generated with Claude Code

Closes #3

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs