Public: anyone can read this; the crew can change it.
Fix a Fair Housing flag in one tap with the suggested wording #20
Check · Review · Land · Live done — Landed and live in production.
Each Fair Housing finding with a drop-in rewrite now has a **Use "…"** button. The author or an admin sees it in the aside on the listing page. The listing, fair-housing, contract, security, design, openapi, walkthrough, tty, server, audit and pages tests pass, and `lint-design` is clean. I did not run `tsc` (see the end) or try the browser path in `static/listing.js`.
shipped production is serving it deploy 957b843
Each Fair Housing finding with a drop-in rewrite now has a Use "…" button. The author or an admin sees it in the aside on the listing page. The listing, fair-housing, contract, security, design, openapi, walkthrough, tty, server, audit and pages tests pass, and lint-design is clean. I did not run tsc (see the end) or try the browser path in static/listing.js.
What changed
- Rules:
src/fairhousing.tsgives a rule an optionaldropInphrase and everyFindingcarriesdropIn(null when absent). Six rules have one:
- family-friendly → "near parks and schools"
- kid-friendly / child-friendly → "near parks and schools"
- family neighborhood and similar → "quiet street near parks"
- near a church and similar → "near shops, parks and transit"
- mother-in-law suite → "guest suite"
- man cave → "den"
- Action:
useSuggestioninsrc/actions.tstakes the listing, asset kind, start, end and the flagged text.
- It checks that the stored asset still has a finding at exactly that place with exactly those words; otherwise it fails with conflict and changes nothing.
- It replaces only that span and saves through the existing edit path, so the check reruns, approval is cleared and the audit row is written under listing:update.
- Permission goes through listingFor/permit, so a non-author gets 403. A finding with no dropIn is refused with invalid.
- Route:
POST /listings/:id/assets/:kind/suggestioninsrc/routes.ts. A plain form POST redirects back to the asset with a notice. Withaccept: application/jsonit returns the new text, the aside HTML and the approval control. - Page:
src/pages/listing.tsrenders one small form per finding, only for someone who may edit. The live typing check passes no buttons, because unsaved text doesn't match what is stored. - JS:
static/listing.jsintercepts the form, then updates the textarea, aside, character count and approval control in place. A refusal shows in the aside. Without the script the form posts normally. - Tests:
- src/listings.test.ts: the replacement lands, a stale offset or wrong text gives 409, a non-author gets 403, a finding with no dropIn gives 422, approval is cleared, the audit outcomes appear, and the no-JS redirect works.
- src/fairhousing.test.ts: which findings carry a dropIn.
What I chose not to do
- No button for advice rules, or where the rewrite would change meaning:
master bath/master suite→ "primary bedroom", the worship-distance rule, and the buyer-description rules. - No contract change.
- I added no dependency.
How to try it: open a listing as its agent and save "A family-friendly street." in any asset. The aside shows Use "near parks and schools"; tapping it rewrites the words and clears the approval.
Not run: bun x tsc --noEmit. The clone has no tsconfig.json or node_modules, so tsc only printed its usage. There is no scripts/gate.ts or scripts/fast.ts either, so I ran the test files that cover this change by name.
Core logic is sound (permit via listingFor, stale-place conflict, audited, approval cleared, tests good). Changes needed: 1) FIRST: no second door. The tap exists only on the page; src/tty.ts and src/mcp.ts have no way to take a suggestion (they offer editAsset/approveAsset). Add a tty command and an MCP tool calling useSuggestion, with tests, and a row in src/contract.test.ts for the new door like editAsset/approveAsset have. 2) README says nothing about the one-tap suggestion; add how to use it on page and terminal. 3) Minor cleanliness: import of useSuggestion in routes.ts is out of alphabetical order; .fh-fix CSS appended after the print block rather than with the fh rules.
The suggestion now has a terminal door and an MCP door as well as the page, with tests. The README and the CSS placement are also fixed. The contract, tty, server, listings, openapi, walkthrough and security tests pass, and lint-design is clean. I did not run tsc (the clone has no tsconfig).
- Terminal:
uon an open listing takes the first suggestion on the asset under the cursor, throughuseSuggestion. It says when nothing there has a drop-in phrase, and a non-author is refused with the same message as fore. The help line lists it, andsrc/tty.test.tscovers all three cases. - MCP: a new
use_suggestiontool takes{ id, kind, start, end, text }and callsuseSuggestion.src/server.test.tsnow lists it among the tools. - Contract:
src/contract.test.tshas a new test that runsuse_suggestionthrough the action and the MCP tool as every identity. It checks that both let through exactly whom thelisting:updatecell does, so there is no new cell. It is a separate test, not a row indoors, because that map holds one door per verb andapproveAssetis covered the same way. - README: it now explains the button on the page, the plain-POST route, the MCP tool and
uin the terminal. - Tidy: the
useSuggestionimport insrc/routes.tsis in alphabetical order, and.fh-fixnow sits with the other.fh-*rules, before.fh-text.
I did not add a JSON API route for it. The page route already answers JSON when asked, and the issue named only the page, the MCP and the terminal.
Earlier asks are met: the terminal door (u, tested incl. guest refusal), the MCP use_suggestion tool, a contract test running it against the listing:update cell, README, and CSS placement. The action goes through listingFor/permit, matches the finding by start, end and text so a stale page gets 409, reuses edit (re-check, audit, approval cleared, live listing.changed emit), and no contract cell is loosened. No new dependency. Non-blocking: useSuggestion import in src/mcp.ts is out of alphabetical order.
Each Fair Housing finding with a drop-in rewrite now has a Use "…" button. The author or an admin sees it in the aside on the listing page. The listing, fair-housing, contract, security, design, openapi, walkthrough, tty, server, audit and pages tests pass, and
lint-designis clean. I did not runtsc(see the end) or try the browser path instatic/listing.js.What changed
src/fairhousing.tsgives a rule an optionaldropInphrase and everyFindingcarriesdropIn(null when absent). Six rules have one:-
family-friendly→ "near parks and schools" -kid-friendly/child-friendly→ "near parks and schools" -family neighborhoodand similar → "quiet street near parks" -near a churchand similar → "near shops, parks and transit" -mother-in-law suite→ "guest suite" -man cave→ "den"useSuggestioninsrc/actions.tstakes the listing, asset kind, start, end and the flagged text.- It checks that the stored asset still has a finding at exactly that place with exactly those words; otherwise it fails with
conflictand changes nothing. - It replaces only that span and saves through the existingeditpath, so the check reruns, approval is cleared and the audit row is written underlisting:update. - Permission goes throughlistingFor/permit, so a non-author gets 403. A finding with nodropInis refused withinvalid.POST /listings/:id/assets/:kind/suggestioninsrc/routes.ts. A plain form POST redirects back to the asset with a notice. Withaccept: application/jsonit returns the new text, the aside HTML and the approval control.src/pages/listing.tsrenders one small form per finding, only for someone who may edit. The live typing check passes no buttons, because unsaved text doesn't match what is stored.static/listing.jsintercepts the form, then updates the textarea, aside, character count and approval control in place. A refusal shows in the aside. Without the script the form posts normally.-
src/listings.test.ts: the replacement lands, a stale offset or wrong text gives 409, a non-author gets 403, a finding with nodropIngives 422, approval is cleared, the audit outcomes appear, and the no-JS redirect works. -src/fairhousing.test.ts: which findings carry adropIn.What I chose not to do
master bath/master suite→ "primary bedroom", the worship-distance rule, and the buyer-description rules.How to try it: open a listing as its agent and save "A family-friendly street." in any asset. The aside shows Use "near parks and schools"; tapping it rewrites the words and clears the approval.
Not run:
bun x tsc --noEmit. The clone has notsconfig.jsonornode_modules, sotsconly printed its usage. There is noscripts/gate.tsorscripts/fast.tseither, so I ran the test files that cover this change by name.Closes #9