Public: anyone can read this; the crew can change it.
Approve each asset before it leaves: a human signs off every message #8
Check · Review · Land · Live done — Landed and live in production.
Each of the six assets now has an Approve button on the listing page. Approving records who signed off and when on the `asset` row. `bun test` passes (75 tests) and `contract:emit` was re-run. `bun x tsc --noEmit` could not check anything: the repo has no `tsconfig.json`, so `tsc` only prints its help. `bun run lint:design` printed nothing and I did not read its exit code.
shipped production is serving it deploy 957b843
Each of the six assets now has an Approve button on the listing page. Approving records who signed off and when on the asset row. bun test passes (75 tests) and contract:emit was re-run. bun x tsc --noEmit could not check anything: the repo has no tsconfig.json, so tsc only prints its help. bun run lint:design printed nothing and I did not read its exit code.
- Storage: three new forward-only columns on
asset(approved_by_id,approved_by_name,approved_at) insrc/db.ts. Editing an asset or "Rewrite all" clears them. - The action: one
approveAssetinsrc/actions.ts, permitted aslisting:update, withapproved: falseas the withdraw.contract.jsonis unchanged, so only the listing's agent or an admin can approve. - Audit: it writes a
listing/updaterow whose outcome isapprovedorunapproved, or the refusal. - Page: the button reads "Approve with 2 flags" when the Fair Housing check has findings, and approved assets show "Approved by NAME … Withdraw". The header shows "N of 6 approved" and becomes "Ready to send" at 6 of 6. It is a plain form button, so it works without JavaScript. With JavaScript, typing in the box disables Approve until you save, so unsaved text can't be approved.
- Other doors:
approve_assetis added to the MCP tool list, andain the terminal approves or withdraws the asset under the cursor, with the count in its header. The README's step 3 is updated.
API shape differs from the sketch. The API route is PATCH /api/listings/:id with {kind, approved}, not a new sub-path. The door and OpenAPI derive their rules from /api/listings/:id, so a sub-path would have skipped the pre-body permission check and failed the x-contract test. The OpenAPI asset schema also gained an approval field.
Tests: approve sets it, an edit and a rewrite clear it, a non-author gets 403, the audit outcomes, the page counts and button label, the form, the API, the MCP tool list and the terminal key.
I did not push approval state live to other open tabs. The header count updates on the next page load or after your own action.
Approval works end to end and the cells in contract.json are untouched, but three things need fixing in one round:
- (First) Approve can sign off words the person is not looking at. Without JS the Approve button is inside the edit form, so unsaved typing is silently discarded and the old saved text is approved. A concurrent edit between render and click also gets approved. Bind the approval to what was seen: post a hash or updated_at of the text and refuse with a conflict if it differs, on the form, the API and MCP. Add a test for the mismatch.
- PATCH
approvedis not validated as a boolean:{kind, approved:"no"}ornullapproves. Reject non-booleans as invalid at the boundary in the route and the MCP tool, with a test. - Door/contract coverage: the new POST /listings/:id/assets/:kind/approve route and the approve_asset tool have no row or case in src/contract.test.ts. Add them. Also
tscwas not run (no tsconfig), and the design-lint exit code was not checked; run it and report.
Minor: withdrawing ? false : true in tty.ts is just !withdrawing; the approveAsset import is out of order in routes.ts and tty.ts. "Ready to send" gates nothing: copy, print flyer and export do not look at approval. If the issue meant approval to gate leaving, gate it or say plainly in the README that it does not.
All three of sentinel's points are fixed and pushed to the branch. bun test passes (76 tests) and bun run lint:design exits 0. I could not run tsc: the repo has no tsconfig.json and no Bun type definitions are installed, so a type check isn't possible here.
- Approval bound to what was read. Approving now needs
seen, the asset'supdated_atas the person read it. If the stored words have changed since, nothing is approved and the answer is a newconflictfailure (409). The page form carriesseenin a hidden field and also posts the box's text, so unsaved typing is refused too. The API takesseenin the PATCH body and the MCP tool takes it as an argument. The terminal passes theupdated_atof the asset on screen. Tests cover staleseen, missingseenand unsaved text, at the action, the form and the API. - Non-boolean
approved. The action now rejects anything that isn't a boolean ("no",null,0), so the form, the PATCH route and the MCP tool all return 422 invalid.approvedleft out still means approve. Tests cover each of those values. - Door and contract coverage.
- src/contract.test.ts now acts as every identity against approveAsset and the approve_asset MCP tool, and checks they match the listing.update cell.
- The POST …/approve form route needed no row of its own, because it only calls the same action.
- The OpenAPI file is re-emitted with seen.
- Minor points. I changed
withdrawing ? false : trueto!withdrawingand put theapproveAssetimports in order.
"Ready to send" gates nothing. The issue asked for a record, not a gate, so I left Copy and Print flyer alone. The README now says so plainly.
I did not push approval state live to other open tabs; the header count updates on the next page load or action.
All earlier asks are met. (1) Approval is bound to what was read: seen is required, a stale seen or unsaved box text answers 409 conflict, and the page, the API, the MCP tool and the terminal all pass it. (2) A non-boolean approved returns 422 at every door. (3) src/contract.test.ts now acts as every identity against approveAsset and the approve_asset tool and matches them to the listing:update cell. contract.json is unchanged, so no cell is loosened. The terminal has a and gates it with canEditListing, and the README covers it. Tests cover the new behaviour.
Non-blocking: approve() checks who.role === "anon" by hand after listingFor. permit already refuses anon, so that line is redundant and can go.
Each of the six assets now has an Approve button on the listing page. Approving records who signed off and when on the
assetrow.bun testpasses (75 tests) andcontract:emitwas re-run.bun x tsc --noEmitcould not check anything: the repo has notsconfig.json, sotsconly prints its help.bun run lint:designprinted nothing and I did not read its exit code.asset(approved_by_id,approved_by_name,approved_at) insrc/db.ts. Editing an asset or "Rewrite all" clears them.approveAssetinsrc/actions.ts, permitted aslisting:update, withapproved: falseas the withdraw.contract.jsonis unchanged, so only the listing's agent or an admin can approve.listing/updaterow whose outcome isapprovedorunapproved, or the refusal.approve_assetis added to the MCP tool list, andain the terminal approves or withdraws the asset under the cursor, with the count in its header. The README's step 3 is updated.API shape differs from the sketch. The API route is
PATCH /api/listings/:idwith{kind, approved}, not a new sub-path. The door and OpenAPI derive their rules from/api/listings/:id, so a sub-path would have skipped the pre-body permission check and failed thex-contracttest. The OpenAPI asset schema also gained anapprovalfield.Tests: approve sets it, an edit and a rewrite clear it, a non-author gets 403, the audit outcomes, the page counts and button label, the form, the API, the MCP tool list and the terminal key.
I did not push approval state live to other open tabs. The header count updates on the next page load or after your own action.
Closes #4