Skip to content

feat(devx): discover check-route-envelope's express population instead of enumerating it - #10351

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9937-route-envelope-express-population
Aug 20, 2026
Merged

feat(devx): discover check-route-envelope's express population instead of enumerating it#10351
os-zhuang merged 1 commit into
mainfrom
claude/issue-9937-route-envelope-express-population

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #9937

Surface 4 of scripts/check-route-envelope.mjs shipped enumerated in #9813 — one file named by hand — while surfaces 1 and 3 refuse any undeclared response-writing module. This builds the walk.

Why it needed a discriminator, and what the discriminator cost

res.json is one spelling for two opposite acts, byte-identical in receiver name:

res.json({ success: true, data })     // express WRITE, one argument
const body = await res.json();        // fetch READ, zero arguments

Triage's sizing note said the false-positive surface was unmeasured and had to be measured before sweeping. It was, on origin/main (736cfb1), and the result is what made a full sweep affordable rather than a narrower shape:

discriminator files swept false positives
receiver res only 20 13 pure fetch readers (65%)
argument count only 18 11 DSL files — Field.json({…}), t.json(c.name), table.json(name)
both (shipped) 7 0

301 res .json calls under packages/: 218 writes in 7 files, 83 reads in 13 others, and no file mixes the two. The 20 in row 1 reproduces the card's own "~20 res.json( files measured on main" exactly.

Both express spellings are writes, because the second is where the refusals live: res.json(body) (81 calls) and the chained res.status(400).json(body) (137). A bare-form-only walk would have counted the successes and missed the errors — this gate's recurring failure, one dialect at a time (#7295, #8884, #9267).

What landed

  • discoverExpressRoutes() — parses every non-test .ts under packages/ and refuses any undeclared express writer, with failure text mirroring surface 3's. Surfaces 3 and 4 now share one discoverResponseWriters(), so the two populations cannot drift apart in whichever copy nobody edits.
  • The discriminator, self-tested in both directions. The reject side is asserted positively as reads: 1, not as bodies: 0 — a scanner that had simply stopped matching the receiver produces bodies: 0 too. The audit prints the reject side as well (13 file(s) skipped as fetch readers, 83 zero-argument calls), because a walk can otherwise only ever show what it swept in.
  • Excluded, not unaudited: surface 1's MODULES and SHARED_BUILDERrest-server.ts (137 of the write calls), error-response.ts and response-envelope.ts are held there by write-site counts that see these same calls.
  • IHTTP_ROUTE_MODULESEXPRESS_RESPONSE_MODULES. The card's third open question — one population or two — is answered one, and not as a preference: the walk found a middleware and two refusal helpers, none of them route modules, and the repo types its handlers any at exactly the seam a mounting-mechanism split would need (query-allowlist.ts says so in its own signature). No references to the old name exist outside this file.

First-audit verdict for every file swept in

file bodies verdict
packages/runtime/src/dispatcher-plugin.ts 9 {} — already declared; unchanged
packages/runtime/src/security/inbound-rate-limit.ts 1 conformant {} — the 429 is { success: false, error: buildApiError({…}) }, the same door dispatcher-plugin.ts's conformant exits use
packages/rest/src/query-allowlist.ts 1 ratchet unenveloped: 1 (L194)
packages/rest/src/query-multiplicity.ts 1 ratchet unenveloped: 1 (L139)

The two ratchets are the finding this walk was worth building for: the shared 400 refusals rest-server.ts delegates to are ADR-0112-nested already (error: { code, message }, the reference shape security-suggested-bindings-envelope.test.ts pins the family onto) but carry no success above them, so unwrapResponse hands them to callers raw. Neither file is a route module, neither follows the *-routes.ts convention, and both were invisible to all four surfaces. They ratchet against #9559, the standing owner of packages/rest's convergence onto the shared sendOk/sendError; converting the wire answers is that line's work, not this card's.

The premise, re-derived on current main

Half of it held, half did not, and the report says which:

Verification

Every gate quoted by its own verdict line, exit codes captured before any pipe, all on the final commit 4594bd26.

$ pnpm check:route-envelope                → EXIT=0
✓ Express-style response modules — 4 module(s) discovered and audited (walked, not
  enumerated — #9937), 12 hand-built body/bodies (count reported, NOT pinned):
  2 conformant, 2 ratcheted, 0 exempt
  read/write discriminator: 13 file(s) skipped as fetch readers (83 zero-argument
  `res.json()` call(s), none swept in)
$ pnpm check:cross-package-test-inputs      → EXIT=0   All 60 self-test cases passed.
$ pnpm check:nul-bytes                      → EXIT=0   scanned 6090 text file(s); no raw ASCII control bytes

Surfaces 1, 2 and 3 are unmoved by the scanner change — 10 route modules, 16 dispatcher domains, 11 Hono modules / 165 bodies, identical to the baseline run on 736cfb14. That is the no-collateral evidence for touching the shared scanner.

A green self-test proves nothing until it can go red

Six mutations, each confirmed on disk before running (injected marker present, anchor text gone) and each restored byte-identically afterwards:

mutation audit --self-test
M1 drop the argument-count half red — 13 fetch readers arrive as NOT DECLARED red — a zero-argument res.json() must read as a READ
M2 drop the receiver-name half red — the DSL files arrive as NOT DECLARED red — a res.json write must not enter surface 3
M3 drop chained-receiver support red — no longer writes an express-style response red — res.status(n).json(body) must read as a WRITE
M4 break the walk (discover nothing) red red — the walk must re-find every declared express module
M5 un-declare inbound-rate-limit.ts red — NOT DECLARED … EXPRESS_RESPONSE_MODULES green (a table fact, not a scanner fact)
M6 mis-declare a count (1 → 0) red — unenveloped: found 1, declared 0 green (same reason)

M4 is the reproduce-before-believing pin, kept permanently in the self-test: the walk must re-find every file the enumerated list named on the real tree, before it is trusted on files nobody enumerated.

Scope

skip-changeset: root scripts/ is not a publish surface and no packages/* file is touched. Clause ② does not attach — its path limb is packages/spec/src/** and nothing here is under it, and on the content limb this changes a lint gate's accept/reject behaviour, not any wire contract, Zod schema or published API. No route behaviour changed; not one byte of product code is in this diff.


Generated by Claude Code

…d of enumerating it (#9937)

Surface 4 shipped enumerated in #9813 — one file named by hand — because a
discovery walk for the express dialect needs something the other three surfaces
never did: a read/write discriminator. `res.json` is one spelling for two
opposite acts, and the reading half is the majority of it in this repo:

    res.json({ success: true, data })     // express WRITE, one argument
    const body = await res.json();        // fetch READ, zero arguments

Measured on origin/main (736cfb1) BEFORE widening the population, because the
cost of being wrong is a gate that reddens on innocent files — 301 `res` `.json`
calls under packages/: 218 writes in 7 files, 83 reads in 13 others, and not one
file mixes them. Argument count alone would sweep in 11 DSL files
(`Field.json({…})`, `t.json(c.name)`); the receiver name alone would sweep in all
13 fetch readers, 65% of the 20 files touching the spelling. Together: zero false
positives.

- `discoverExpressRoutes()` walks packages/ and refuses any undeclared express
  writer, mirroring surface 3. Both walks now share `discoverResponseWriters()`,
  so the two populations cannot drift apart in whichever copy nobody edits.
- Both express spellings count: `res.json(body)` (81 calls) and the chained
  `res.status(400).json(body)` (137) — the second is where the refusals live, so
  a bare-form-only walk would count the successes and miss the errors.
- The discriminator is self-tested in BOTH directions, the reject side asserted
  positively as `reads: 1` rather than as `bodies: 0` — a count a scanner that had
  simply stopped matching the receiver would also produce. The audit prints the
  reject side too (13 files skipped as fetch readers), since a walk can otherwise
  only ever show what it swept in.
- First-audit verdict for all 3 newly swept-in files: `inbound-rate-limit.ts`
  conformant (its 429 goes through `buildApiError`); `query-allowlist.ts` and
  `query-multiplicity.ts` ratcheted at `unenveloped: 1` each — the shared 400
  refusals are ADR-0112-nested already but carry no `success` above it.
- `IHTTP_ROUTE_MODULES` becomes `EXPRESS_RESPONSE_MODULES`: the walk found a
  middleware and two refusal helpers, none of them route modules, and the repo
  types its handlers `any` at exactly the seam a mounting-mechanism split would
  need. The dialect is the population — the third open question in #9937.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuang os-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 20, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 20, 2026 16:15
@os-zhuang
os-zhuang enabled auto-merge August 20, 2026 16:15
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 20, 2026
Merged via the queue into main with commit b1893a8 Aug 20, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9937-route-envelope-express-population branch August 20, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants