src/quorum.ts answers, from a roster and a set of ballots, whether a proposal is decided and by what — with no database, no HTTP and no Who anywhere in it.
What done looks like
[ ] src/quorum.ts exports RULES (the three names, each with one line of human description), parseRule(text) and tally(input), all pure functions over plain data.
[ ] A rule is {kind:"majority"} | {kind:"all"} | {kind:"named", who: string[], needed: number}, stored as text: majority, all, named:alice,bob,carol:2. parseRule is the only thing that reads that text, and it returns undefined for anything else — an unknown kind, an empty name list, needed below 1 or above the list's length, a name over 64 characters, more than 32 names, a name outside [A-Za-z0-9_.-]. It never throws.
[ ] tally({rule, roster, ballots, optionIds}) returns {decided, winner?, counts, needed, of, reason}, where counts covers every option id (zero included) and reason is one sentence a page can print verbatim: "4 of 7 — a majority needs 4", "2 of 3 named — this needs 2", "6 of 7 voted — all must agree".
[ ] majority: more than half the roster on one option decides it. all: every member of the roster has voted and all on the same option. named: needed of the named people on one option, and only their ballots count toward deciding, though every ballot shows in counts.
[ ] A ballot from someone outside the roster (or outside the names, for named) never moves decided. A tie decides nothing. An empty roster decides nothing. A roster of one under majority decides on one ballot.
[ ] Integer arithmetic only: a majority needs Math.floor(of / 2) + 1, never a float comparison.
[ ] src/quorum.test.ts is a table of cases: each rule just below, exactly at and above its threshold; a tie; an outsider's ballot; an empty roster; and every refusal parseRule makes.
[ ] Nothing else in the repository changes, and bun test is green.
Files I expect to change:src/quorum.ts (new), src/quorum.test.ts (new).
Risks: if this module needs a Database or a Who, the shape is wrong — the caller does the reading and hands it plain arrays. The stored rule text is user input arriving from another issue's door, so parseRule refusing cleanly is what keeps that safe. Keep the returned reason free of anything a person typed, so a page can print it without a surprise.
Fit: Ledger — totals recomputed from lines, thresholds exact, refusals named. A person assigns; I do not.
src/quorum.tsanswers, from a roster and a set of ballots, whether a proposal is decided and by what — with no database, no HTTP and noWhoanywhere in it.What done looks like
src/quorum.tsexportsRULES(the three names, each with one line of human description),parseRule(text)andtally(input), all pure functions over plain data.{kind:"majority"}|{kind:"all"}|{kind:"named", who: string[], needed: number}, stored as text:majority,all,named:alice,bob,carol:2.parseRuleis the only thing that reads that text, and it returnsundefinedfor anything else — an unknown kind, an empty name list,neededbelow 1 or above the list's length, a name over 64 characters, more than 32 names, a name outside[A-Za-z0-9_.-]. It never throws.tally({rule, roster, ballots, optionIds})returns{decided, winner?, counts, needed, of, reason}, wherecountscovers every option id (zero included) andreasonis one sentence a page can print verbatim:"4 of 7 — a majority needs 4","2 of 3 named — this needs 2","6 of 7 voted — all must agree".majority: more than half the roster on one option decides it.all: every member of the roster has voted and all on the same option.named:neededof the named people on one option, and only their ballots count toward deciding, though every ballot shows incounts.named) never movesdecided. A tie decides nothing. An empty roster decides nothing. A roster of one undermajoritydecides on one ballot.Math.floor(of / 2) + 1, never a float comparison.src/quorum.test.tsis a table of cases: each rule just below, exactly at and above its threshold; a tie; an outsider's ballot; an empty roster; and every refusalparseRulemakes.bun testis green.Files I expect to change:
src/quorum.ts(new),src/quorum.test.ts(new).Risks: if this module needs a
Databaseor aWho, the shape is wrong — the caller does the reading and hands it plain arrays. The stored rule text is user input arriving from another issue's door, soparseRulerefusing cleanly is what keeps that safe. Keep the returnedreasonfree of anything a person typed, so a page can print it without a surprise.Fit: Ledger — totals recomputed from lines, thresholds exact, refusals named. A person assigns; I do not.