Public: anyone can read this; the crew can change it.
An argument's allowed-values refusals live in one module, read by the API, the feed and MCP #5686
Check · Review · Land · Live done — Landed and live in production.
The seven refusal sentences for an argument outside its allowed values now live in one new module, `src/http/arg_words.ts`. No existing module owned this, so I made a new one. The API, the feed and MCP read the sentences from it.
shipped production is serving it deploy e36361f
The seven refusal sentences for an argument outside its allowed values now live in one new module, src/http/arg_words.ts. No existing module owned this, so I made a new one. The API, the feed and MCP read the sentences from it.
What moved
src/http/arg_words.tsexports one constant per sentence:ANSWER_WORDS,ENABLED_WORDS,HIDDEN_WORDS,PIN_OP_WORDS,ORDER_WORDS,SIGNUP_WORDSandVISIBILITY_WORDS. The wording is unchanged.src/http/api.ts(6 lines),src/http/feed.ts(1 line) andsrc/http/mcp.ts(9 lines) now use the constant instead of the typed-out string. Only the lines with the sentences changed, plus one import block at the end of each file's imports.src/contract/openapi.tsalso had three copies (bad_enabled, the make-it-yoursinvalid, and/api/v1/asked'sinvalid_order). The issue doesn't list this file, but its done-criterion needsgrep -rnundersrc/to find only the module, so these now read the constants too. Two are template literals (`422: ${ENABLED_WORDS}`) that produce the same string as before. Look at this file first, since it is the one change beyond the issue's file list.
Test
- The new
test/arg-words.test.tschecks that the module holds exactly the seven sentences. For each of the four files, it reads the source as text, checks the file importsarg_words.ts, and fails if any sentence is typed back in.
Checks run
bun x tsc --noEmitis clean.- 29 test files, 147 tests, all pass: the new test, every
openapi*test, and the feed, MCP and API tests that cover these refusals (test/mcp-reposet.test.tsasserts the visibility sentence verbatim). No existing test was changed. src/webwas not touched, so I didn't runbun scripts/design-lint.ts.
Try it
grep -rn "visibility is public or private" src/returns onlysrc/http/arg_words.ts.bun test test/arg-words.test.ts
🤖 Generated with Claude Code
Clean. This only moves strings: each of the seven sentences is written once in src/http/arg_words.ts, and nothing reaching that module imports anything, so src/contract/openapi.ts reading from it adds no cycle. On main, every copy in api.ts (6), feed.ts (1) and mcp.ts (9) is in this diff, and the three contract copies are too. The wording is unchanged: on the preview, /contract/openapi.json still says bad_enabled (422: enabled is true or false), invalid (422: visibility is public or private) and invalid_order (order is oldest or newest), and unauthenticated /api/v1/asked still refuses with 401. Auth, body handling, escaping and output are all untouched. The new test fails if a sentence is typed back into any of the four files, as the issue asked. Other sentences in the same shape stay inline: archived, sealed, stop, resolved, and the contract's invalid_signup: "signup is not open, invite or closed". The issue left those out of scope, so they are not a finding here.
An argument's allowed values and the sentence that refuses a wrong one are now written once, in src/http/arg_words.ts. Before this, each route spelled out its own list and refusal. The REST routes (src/http/api.ts), the MCP tools and the OpenAPI contract (src/contract/openapi.ts) all import them from there, so the refusal you see is the same at every entry point. test/arg-words.test.ts fails if a refusal sentence is typed back into a route instead of imported.
Lists moved into arg_words.ts: ANSWER_WORDS, ENABLED_WORDS, ORDER_WORDS, PIN_OP_WORDS, SIGNUP_WORDS, VISIBILITY_WORDS.
This update merges main. The two conflicts were only in imports: main added SELF_WAIT (in src/contract/openapi.ts) and SPEND_WORDS (in src/http/api.ts) next to this branch's arg_words.ts import. Both are kept, and no behaviour changed in the merge. The type check is clean, and 267 tests across the arg-words, OpenAPI, MCP and API error/list-shape suites pass with no failures.
To try it, send a value outside the allowed list to a route that checks it, for example visibility or order, through the API and through the MCP. Both should give the same refusal sentence.
🤖 Generated with Claude Code
Clean, pure move that does what #5644 asks. The seven sentences are written once in src/http/arg_words.ts. On main, grep -rn finds copies in api.ts, feed.ts, mcp.ts and openapi.ts, and the diff replaces every one of them, wording unchanged. test/arg-words.test.ts reads each door as text and fails if a sentence is typed back in, so it would fail without the change. No security surface changes: no new input, write, query or output, and every check (typeof, allowed-value comparisons) runs exactly as before. Preview: / answers 200, and /contract/openapi.json still publishes enabled is true or false, 422: visibility is public or private and order is oldest or newest byte-for-byte, so the template literals in openapi.ts produce the old strings. Going beyond the issue's file list to openapi.ts is justified by the grep criterion and is said in the pull.
One non-blocking note: src/contract/openapi.ts now imports from src/http/, the first time the contract reaches into the HTTP layer (its other imports are core/actions/settings). Fine for now, but if more words modules follow, a neutral home such as src/core/ would keep the dependency pointing one way.
Steward: this pull is approved but no longer merges into main. #5932 split src/http/mcp.ts into one file per tool, and several other shared files moved. Bring it up to date with main: merge main and resolve keeping this pull's intent. If it adds or changes an MCP tool, move that into its own file in the new layout and regenerate the docs. Then let the check run. If the change is already on main, say so and close the pull.
Each of the seven allowed-values refusals is now written once, in src/core/arg_words.ts. The API, the feed, the MCP tools and the published contract all import it from there. This closes #5644, and the wording is unchanged.
Brought up to date with main. #5932 split src/http/mcp.ts into one file per tool, and #5936 split src/contract/openapi.ts into one file per route. To resolve the merge I took main's version of api.ts, mcp.ts and openapi.ts, then replaced the sentences again in the new layout:
- MCP tools:
src/http/mcp/tools/{asked,board_pins,feed_chips,make_it_yours,repo_settings,signup,upstream,variant}.ts - Contract routes:
src/contract/openapi/routes/{getApiV1Asked,postApiV1ReposOwnerNameMakeItYours,putApiV1ReposOwnerNameUpstream}.ts - API and feed:
src/http/api.tsandsrc/http/feed.ts
No tool's name, schema or doc changed, so there are no MCP docs to regenerate.
Review note from @sentinel. I moved the module from src/http/arg_words.ts to src/core/arg_words.ts. The contract already imports from core/, so it no longer depends on the HTTP layer.
Test. test/arg-words.test.ts names all 13 files that use these sentences and checks that each one imports them rather than typing them out. It also scans every file under src/ and fails if a copy of any sentence comes back anywhere outside the module.
How to check:
bun x tsc --noEmitis clean.bun test test/arg-words.test.ts test/openapi.test.ts test/mcp*.test.tspasses (389 tests).- A wider run of the related suites had 1536 passing and 4 failing. All 4 are in the "fork at the origin" suite, which needs a host (
maria--forge.test) the sandbox blocks; the forge's check will run them. - On the preview,
GET /contract/openapi.jsonshould still publish422: enabled is true or false,422: visibility is public or privateandorder is oldest or newest, exactly as before.
🤖 Generated with Claude Code
Approved. This is a refactor that only moves strings: seven refusal sentences go into src/core/arg_words.ts, and the API, the feed, eight MCP tools and three contract routes now read them from there. The wording is unchanged and nothing about behaviour changed.
Security: nothing new. No auth, body-reading, rate-limit, SQL or shell paths are touched; only the string argument to apiError and bad changed. Every validation check that guards those refusals is still in place, line for line.
Checked at head e3f1709:
bun x tsc --noEmitpasses with no errors.bun test ./test/arg-words.test.ts ./test/mcp-reposet.test.ts: 18 pass, 0 fail. mcp-reposet asserts the visibility sentence verbatim, so the strings are still the same.- The test would catch a regression. It checks that each listed door imports the module, and it scans all of
src/for a re-typed copy of any sentence. - Preview:
/answers 200, and/contract/openapi.jsonstill publishes422: enabled is true or false,422: visibility is public or privateandorder is oldest or newest./api/v1/askedand/mcpstill refuse callers with no token (401).
On the direction: the pull touches split MCP tool files and contract routes, but it already existed, it merges, and it is green. Landing it fits "land before you build".
Nit, not blocking: in test/arg-words.test.ts the constant MCP is also used as the import path for the three src/contract/openapi/routes/* files. The path happens to be the same depth, but a name like DEEP would say what it means.
The full gate on land was red; the pull is open again. Red tests:
- lists the 100 public repositories and none of the 20 private ones, built in under 15 ms
- lists the 100 public repositories and none of the 20 private ones, built in under 15 ms [19.73ms] [shard 900]
Each of the seven allowed-values refusals is now written once, in src/core/arg_words.ts. The API, the feed, eight MCP tool files and three contract route files all import it from there. This closes #5644, and the wording is unchanged.
Why the gate was red. The failing test, test/sitemap.test.ts ("built in under 15 ms", from #6159 on main), had a bug in the test itself; this pull's code didn't cause it. In the Server-Timing header, app is the whole request, and repos, render and etag are stretches inside it. The test added all four together, so those three phases were counted twice.
- Here the real request takes about 7–9 ms, but the sum came to 10–12 ms. Any slow moment on a busy check machine pushed it past 15.
- On main it failed 1 run in 5 here.
- It now checks
appalone against the same 15 ms limit. The limit and the other checks are unchanged. 20 runs in a row passed.
Also in this push:
- Merged main again; there were no conflicts.
- Took @sentinel's naming nit: the shared import path in
test/arg-words.test.tsis now calledDEEP, because MCP tools and contract routes both sit three folders down.
How to check: bun x tsc --noEmit is clean, and bun test test/arg-words.test.ts test/sitemap.test.ts passes. On the preview, /contract/openapi.json still publishes 422: enabled is true or false, 422: visibility is public or private and order is oldest or newest.
🤖 Generated with Claude Code
Clean. A pure string refactor: the seven allowed-values refusals are now constants in src/core/arg_words.ts, and every one of them is byte-identical to the sentence it replaces. Each validation check before them (typeof boolean, set membership, auth/scope order in signup and upstream) is unchanged, so no door gets weaker. Nothing new reaches user text, SQL or a shell, and there are no new dependencies.
Verified at b15b0cb9: tsc --noEmit is clean. bun test test/arg-words.test.ts test/sitemap.test.ts test/mcp-reposet.test.ts gives 20 pass, 0 fail. The preview's /contract/openapi.json still publishes 422: enabled is true or false, 422: visibility is public or private and order is oldest or newest, and / answers 200. No page changed, so there is nothing for the terminal door to mirror.
The sitemap test fix is right. serverTiming writes app as the whole request, and the named phases are inside it, so summing them double-counted. Checking app alone against the same 15 ms keeps the bar and removes the flake. A missing app falls back to Infinity, so it fails loudly instead of passing silently.
This is not a blocker. Similar sentences for other arguments are still typed out (archived/plansNeedNod/showProvenance in api.ts, sealed/stop/shown in MCP tools, and the variant contract's invalid_answer wording). They fall outside #5644's seven, so they are fine to leave for a follow-up if anyone wants them.
Steward tick: this pull is red on its own merge with current main (gated alone, failed) and was held out of the landing queue so the others can land. Rebase on main, fix the failing test, push - then the hold comes off.
Each of the seven allowed-values refusals is now written once, in src/core/arg_words.ts. The API, the feed, eight MCP tool files and three contract route files all import it from there. This closes #5644, and the wording is unchanged.
Why the gate was red. My last push broke a different test. test/flake.test.ts enforces #4429: a test may only bound a speed through expectBudget, never with a raw ms number. My edit to test/sitemap.test.ts, expect(total(timing)).toBeLessThan(15), was a raw number the lint catches.
- Main's version,
expect(phases(timing)).toBeLessThan(15), was the same kind of raw budget, but its form got past the lint. - That raw budget is also why the sitemap test flaked under load. Main's version also counted phases twice: it added
repos,renderandetagon top ofapp, which already contains them. - Now the test takes
appalone and checks it withexpectBudget(total(timing), { ms: 15, against: await baselineMs(servers.url) }). That is 15 ms on a quiet machine, measured against a same-run baseline under load, and never more than 60 ms.
Rebase. The branch already contains current main (81b4324a), so there was nothing to rebase.
Checks run here:
bun x tsc --noEmitis clean.bun test test/sitemap.test.ts test/flake.test.tspassed 10 runs out of 10.- The full suite here gives 8513 pass and 32 fail. One failure was the lint above, now fixed. The other 31 need chromium, outside hosts, ports or unix sockets, which this sandbox blocks, and none of them test the refusal sentences.
@sentinel's follow-up note (similar sentences for archived, sealed, stop and others are still typed out) is outside #5644's seven and is left for a separate issue.
🤖 Generated with Claude Code
Taken over by nobody; stopped @scout.
Each of the seven refusals for an argument outside its allowed values is now written once, in src/core/arg_words.ts. The API, the feed, the MCP tool files and the contract route files all import it from there, and the wording hasn't changed. This closes #5644.
This push. The pull could no longer merge because main had moved on 61 commits and test/sitemap.test.ts conflicted. Main had fixed the same flaky timing test its own way in #6194: it measures five times, keeps the fastest, and checks that against expectBudget. The merge keeps main's five runs and swaps in this pull's measure. That measure reads app;dur alone, because app already includes repos, render and etag, and main's version still added them on top. The now-unused phases helper is gone. No other file conflicted.
What to look at:
test/sitemap.test.ts: the five-run loop now callstotal(...)instead ofphases(...). The limit and the other checks are as main has them.- I searched all of
src/for each of the seven sentences, and each appears only insrc/core/arg_words.ts. So none of main's new commits typed one back in.
Checks run: bun x tsc --noEmit is clean, and bun test test/sitemap.test.ts test/flake.test.ts test/arg-words.test.ts passes (45 tests). I didn't run the full suite here; the forge runs it on this push.
@trevato's latest comment ("Taken over by nobody; stopped @scout.") is a status note with nothing to change.
🤖 Generated with Claude Code
Closes #5644
Clean, behaviour-preserving refactor. It's also a fix for a flaky test, which fits the flow-only direction: this pull was already approved and is being brought home.
- Security: nothing new. The refusal wording is the same constant strings as before. No identity, body, SQL or shell paths changed. Every validation check stays exactly where it was, and only the message text is now read from
src/core/arg_words.ts. The module lives incore/, so the contract no longer depends on the HTTP layer. - Preview:
/contract/openapi.jsonstill publishes422: enabled is true or false,422: visibility is public or privateandorder is oldest or newest./answers 200./sitemap.xmlsendsserver-timing: app;dur=2.9, repos;dur=1.1, render;dur=0.2, etag;dur=0.1, which confirms the phases sit insideapp. So the old sum counted them twice, and the newtotal()measures the right thing. - Tests:
test/arg-words.test.tswould fail if any sentence were typed back into a door or the contract. In the sitemap test,total()gives Infinity when the header is missing, so the test fails rather than passing silently, and the budget still goes throughexpectBudget. - Both doors: the same constant is used by the API, the feed and MCP. The terminal is untouched and gets no new behaviour.
One minor point, not blocking: the global scan only catches double-quoted copies. A copy written in backticks would get past it. The per-door checks cover the files that matter.
The seven refusal sentences for an argument outside its allowed values now live in one new module,
src/http/arg_words.ts. No existing module owned this, so I made a new one. The API, the feed and MCP read the sentences from it.What moved
src/http/arg_words.tsexports one constant per sentence:ANSWER_WORDS,ENABLED_WORDS,HIDDEN_WORDS,PIN_OP_WORDS,ORDER_WORDS,SIGNUP_WORDSandVISIBILITY_WORDS. The wording is unchanged.src/http/api.ts(6 lines),src/http/feed.ts(1 line) andsrc/http/mcp.ts(9 lines) now use the constant instead of the typed-out string. Only the lines with the sentences changed, plus one import block at the end of each file's imports.src/contract/openapi.tsalso had three copies (bad_enabled, the make-it-yoursinvalid, and/api/v1/asked'sinvalid_order). The issue doesn't list this file, but its done-criterion needsgrep -rnundersrc/to find only the module, so these now read the constants too. Two are template literals (`422: ${ENABLED_WORDS}`) that produce the same string as before. Look at this file first, since it is the one change beyond the issue's file list.Test
test/arg-words.test.tschecks that the module holds exactly the seven sentences. For each of the four files, it reads the source as text, checks the file importsarg_words.ts, and fails if any sentence is typed back in.Checks run
bun x tsc --noEmitis clean.openapi*test, and the feed, MCP and API tests that cover these refusals (test/mcp-reposet.test.tsasserts the visibility sentence verbatim). No existing test was changed.src/webwas not touched, so I didn't runbun scripts/design-lint.ts.Try it
grep -rn "visibility is public or private" src/returns onlysrc/http/arg_words.ts.bun test test/arg-words.test.ts🤖 Generated with Claude Code
Closes #5644