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

The rule closes the proposal: the decision lands once, in the transaction that cast the ballot #57

open not picked up yet opened this
opened this

The second half of #5, which is cut down to recording a ballot. With ballots in the table and tally reading them, this is the moment the app is named for: the rule is met and the proposal is decided, exactly once, by nobody pressing anything.

What done looks like

  • [ ] A migration adds decided_at to proposal (if #5 did not already need it) and nothing else.
  • [ ] After a ballot is written, in the same transaction, the proposal is re-tallied and — when tally says decided — its state becomes decided, decided_option_id is the winner and decided_at is stamped.
  • [ ] The write is guarded: UPDATE proposal SET … WHERE id = ?1 AND state = 'open', and the decision is announced only if that statement changed a row. Two ballots arriving at once decide the proposal once and emit one decision.made; a test drives two votes through the action against one database and asserts a single event.
  • [ ] decision.made carries the proposal, the winning option and the tally that decided it — enough for #7 to sign and send without reading anything back.
  • [ ] A ballot that decides nothing emits vote.cast. Both events cross processes through the event table, as everything else does.
  • [ ] A decided proposal takes no further ballots: vote answers invalid with a sentence naming the state, and the existing ballots stay exactly as they are — the record of how it was decided.
  • [ ] A decision is never undone: there is no action, route, tool or key in this pull that moves a proposal out of decided, and README.md says that in one sentence, with the reason.
  • [ ] Tests in src/actions.test.ts: the majority threshold landing; all landing only when every member has voted and agreed; a named: rule landing on its own names while other ballots pile up unused; a tie deciding nothing however many ballots arrive; the single-decision.made race; a vote after the decision refused; the winner and decided_at unchanged by anything that happens afterwards.
  • [ ] bun test is green.

Files I expect to change: src/db.ts, src/actions.ts, src/actions.test.ts, README.md.

Risks: this is the one irreversible write in the app, so the guard and the transaction are the entire issue — a re-tally outside the transaction that wrote the ballot is a decision that can fire twice, and the only symptom will be a webhook delivered twice weeks later. Do not add a page, a route, a tool or a terminal key here: #44 and #45 are the doors, and keeping them out is what makes this fit a run. Do not let a roster change reach into this path; that is #51's problem and it must reuse this code rather than copy it.

Fit: Ledger — the one transaction the whole app exists to get right. A person assigns; I do not.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs