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

Propose, read and withdraw: a proposal with options replaces the note #2

shipped production is serving it opened this has it
opened this

A proposal — a title, a paragraph and two to eight options — becomes the thing this app is about, at every door, and the template's note goes.

What done looks like

  • [ ] A migration appended to migrations in src/db.ts creates proposal (id, title 1–120, body 1–2000, rule TEXT NOT NULL DEFAULT 'majority', state in open/decided/withdrawn default open, decided_option_id nullable, author_id, author_name, created_at, updated_at) and option (id, proposal_id REFERENCES proposal(id) ON DELETE CASCADE, label 1–80, position), with an index on option(proposal_id). A second appended migration drops the template's note table. No migration that has run is edited.
  • [ ] src/actions.ts loses the note functions and gains listProposals(db, who, {state?, limit?}), getProposal(db, who, {id}) (the proposal with its options), propose(db, who, {title, body, options, rule}) and withdrawProposal(db, who, {id}).
  • [ ] propose refuses anon with signin_required; refuses fewer than two or more than eight options, blank or duplicate labels, a title over 120 or a paragraph over 2000 characters, each with invalid and a sentence a person can act on; it trims every field and writes the proposal and its options in one transaction.
  • [ ] withdrawProposal is the author or an admin only — the rule an exported canWithdraw(who, proposal), the way canDelete was — and only while the state is open; a second withdrawal is invalid.
  • [ ] propose emits proposal.opened, withdrawProposal emits proposal.withdrawn, and the list region carries data-live for both.
  • [ ] Two pages, so src/page.ts becomes src/pages/layout.ts, src/pages/home.ts and src/pages/proposal.ts with one layout: / lists open proposals newest first (title, author, when, how many options, state) and carries the composer for a signed-in person; /p/:id shows the title, the paragraph, the options in order, the author, the state and a Withdraw form for whoever may. A guest sees both pages and no forms. Every value rendered goes through escape.
  • [ ] Routes in src/routes.ts: POST /proposals and POST /proposals/:id/withdraw (forms, back to the page with the message on failure), GET /api/proposals, GET /api/proposals/:id, POST /api/proposals (201) and POST /api/proposals/:id/withdraw — statuses follow Failure through answer. GET /p/:id for something that is not a proposal answers 404 in the layout.
  • [ ] MCP tools in src/mcp.ts are list_proposals, get_proposal, propose and withdraw_proposal, each with a JSON schema that states its limits; the note tools go.
  • [ ] src/tty.ts lists proposals, j/k moves, Enter opens the one under the cursor (paragraph and options), n proposes by asking for a title, then the paragraph, then options one per line until a blank line, Esc cancels, w withdraws when allowed, q quits; a guest at the terminal is told to sign in rather than shown a composer.
  • [ ] src/actions.test.ts covers: anon cannot propose; one option refused; nine refused; duplicate labels refused; a 2001-character paragraph refused; another user cannot withdraw; an admin can; withdrawing twice refused. src/server.test.ts covers /, /p/:id, the JSON routes, a 404 for a missing proposal, and a cross-origin POST still refused.
  • [ ] README.md describes proposals rather than notes in its table and its "Any interface" section, and bun test is green.

Files I expect to change: src/db.ts, src/actions.ts, src/actions.test.ts, src/routes.ts, src/pages/ (new, from src/page.ts), src/mcp.ts, src/tty.ts, src/server.test.ts, static/app.css, README.md.

Risks: this is the widest change on the map — settle the schema and the actions first and let the four doors follow, or they drift apart. Dropping note is a new migration, never an edit to migration 1. Option labels are user text at four doors: escape at the page, bind every parameter, build no SQL by concatenation. Keep src/server.ts's same-origin refusal and rate limit exactly as they are.

Fit: Ledger — a state machine whose rows carry a reason, validated at the boundary in the first commit. A person assigns; I do not.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs