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

Approve each asset before it leaves: a human signs off every message #8

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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) in src/db.ts. Editing an asset or "Rewrite all" clears them.
  • The action: one approveAsset in src/actions.ts, permitted as listing:update, with approved: false as the withdraw. contract.json is unchanged, so only the listing's agent or an admin can approve.
  • Audit: it writes a listing/update row whose outcome is approved or unapproved, 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_asset is added to the MCP tool list, and a in 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.

Closes #4

commented

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) in src/db.ts. Editing an asset or "Rewrite all" clears them.
  • The action: one approveAsset in src/actions.ts, permitted as listing:update, with approved: false as the withdraw. contract.json is unchanged, so only the listing's agent or an admin can approve.
  • Audit: it writes a listing/update row whose outcome is approved or unapproved, 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_asset is added to the MCP tool list, and a in 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.

Changes requested — before the latest push

Approval works end to end and the cells in contract.json are untouched, but three things need fixing in one round:

  1. (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.
  2. PATCH approved is not validated as a boolean: {kind, approved:"no"} or null approves. Reject non-booleans as invalid at the boundary in the route and the MCP tool, with a test.
  3. 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 tsc was 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.

commented

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.

  1. Approval bound to what was read. Approving now needs seen, the asset's updated_at as the person read it. If the stored words have changed since, nothing is approved and the answer is a new conflict failure (409). The page form carries seen in a hidden field and also posts the box's text, so unsaved typing is refused too. The API takes seen in the PATCH body and the MCP tool takes it as an argument. The terminal passes the updated_at of the asset on screen. Tests cover stale seen, missing seen and unsaved text, at the action, the form and the API.
  2. 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. approved left out still means approve. Tests cover each of those values.
  3. 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.

  1. Minor points. I changed withdrawing ? false : true to !withdrawing and put the approveAsset imports 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.

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs