src/quorum.ts has landed with three rules, but propose still writes the default majority for everyone and no door offers a choice — so the rule the group agreed to is invisible and unchangeable. This is the half of issue #5 that needs neither ballots nor a roster, and it can land now.
What done looks like
[ ] propose takes an optional rule string, validated by parseRule from src/quorum.ts — anything it refuses is invalid with a sentence naming the three forms (majority, all, named:alice,bob:2) — and stores the text it was given; no rule means majority, exactly as today.
[ ] getProposal and listProposals return the parsed rule alongside its stored text, so a door never parses it a second time.
[ ] The composer on / offers the rule as a <select> of the three kinds, with the names and count for named: as one small text field that only appears relevant for that kind (and works without JavaScript — a plain field with a label saying it is used only for "a named few").
[ ] /p/:id states the rule in words from RULES, above the options ("Decides by majority of the group", "Needs 2 of alice, bob, carol").
[ ] Every door carries it: rule in POST /api/proposals, in the MCP propose schema with the three forms described, and the terminal asking for the rule after the options (Enter for majority).
[ ] A proposal keeps the rule it was opened with; changing it later is not in this issue.
[ ] Tests: each of the three forms round-trips from POST /api/proposals to GET /api/proposals/:id; named: with a count above its name list is refused; named: naming nobody is refused; garbage ("unanimous", "majority:9", a 500-character string) is refused; no rule still yields majority; the proposal page prints the rule for each kind.
[ ] README.md shows the three forms in three lines. bun test is green.
Files I expect to change:src/actions.ts, src/actions.test.ts, src/routes.ts, src/pages/home.ts, src/pages/proposal.ts, src/mcp.ts, src/tty.ts, src/server.test.ts, README.md.
Risks: the rule is user text stored in a column and read back by parseRule — a row written before this change (or by hand) must still read, so treat an unparseable stored rule as a named failure at the read, never a throw that takes the page down. Do not teach a second module how to read the rule text: parseRule stays the only reader. Issue #5 (votes decide) assumes this is in place and no longer has to add it.
Fit: Ledger — a boundary that names what it refuses, and a value that means one thing everywhere. A person assigns; I do not.
src/quorum.tshas landed with three rules, butproposestill writes the defaultmajorityfor everyone and no door offers a choice — so the rule the group agreed to is invisible and unchangeable. This is the half of issue #5 that needs neither ballots nor a roster, and it can land now.What done looks like
proposetakes an optionalrulestring, validated byparseRulefromsrc/quorum.ts— anything it refuses isinvalidwith a sentence naming the three forms (majority,all,named:alice,bob:2) — and stores the text it was given; no rule meansmajority, exactly as today.getProposalandlistProposalsreturn the parsed rule alongside its stored text, so a door never parses it a second time./offers the rule as a<select>of the three kinds, with the names and count fornamed:as one small text field that only appears relevant for that kind (and works without JavaScript — a plain field with a label saying it is used only for "a named few")./p/:idstates the rule in words fromRULES, above the options ("Decides by majority of the group", "Needs 2 of alice, bob, carol").ruleinPOST /api/proposals, in the MCPproposeschema with the three forms described, and the terminal asking for the rule after the options (Enter for majority).POST /api/proposalstoGET /api/proposals/:id;named:with a count above its name list is refused;named:naming nobody is refused; garbage ("unanimous","majority:9", a 500-character string) is refused; no rule still yieldsmajority; the proposal page prints the rule for each kind.README.mdshows the three forms in three lines.bun testis green.Files I expect to change:
src/actions.ts,src/actions.test.ts,src/routes.ts,src/pages/home.ts,src/pages/proposal.ts,src/mcp.ts,src/tty.ts,src/server.test.ts,README.md.Risks: the rule is user text stored in a column and read back by
parseRule— a row written before this change (or by hand) must still read, so treat an unparseable stored rule as a named failure at the read, never a throw that takes the page down. Do not teach a second module how to read the rule text:parseRulestays the only reader. Issue #5 (votes decide) assumes this is in place and no longer has to add it.Fit: Ledger — a boundary that names what it refuses, and a value that means one thing everywhere. A person assigns; I do not.