Repository navigation
Description
Found while implementing #9098 (the REST door-typing half, PR #9222). Filed unassigned; nobody is on it. Not fixed there — that card's scope was the door's typing, and widening this gate's scan is a separate, self-contained change with its own self-test obligations.
The gap
scripts/check-dispatcher-error-vocabulary.mjs publishes its recognised spellings in SHAPES, and its own header states the cost precisely:
the price of a source scan is that it sees only the spellings it knows, and an unrecognised one produces no finding, SILENTLY. Reaching for a spelling that is not here? Extend
SHAPESand add a--self-testcase in the same edit.
Of the four shapes, only one resolves a non-literal value — classconst, which handles readonly code = CONST on a class field. The objlit shape requires a quoted literal:
{ name: 'objlit', re: /\bcode:\s*'([A-Za-z][A-Za-z0-9_]*)'/g, literal: true },So both of these stamp a wire code and are matched by nothing:
throw Object.assign(new Error(msg), { code: SOME_MODULE_CONST }); // constant in an object literal
sendDeclaredFault(res, { code: `APPROVAL_${action}_FAILED`, ... }); // template-generatedNeither reaches resolveConstant, because that is only called for shapes declared literal: false, and objlit is literal: true. The value is not reported as unresolved either — the site is simply never seen. That is a different outcome from the header's stated bound ("A constant this gate cannot resolve is REPORTED as unresolved, never dropped"), which holds for classconst and not here.
Why it is worth a card rather than a note
This is not hypothetical, and it has already cost a round. #8885 could not lean on this gate for the template-generated APPROVAL_*_FAILED family and had to add a bespoke runtime pin instead — packages/rest/src/rest-approvals-wire-codes.test.ts — to mechanically redden a future approvals route whose generated code goes unregistered. One package now has a hand-written class pin doing the job the repo-wide gate advertises, and no equivalent exists for any other package that generates or indirects a code.
The blind spot is also the quiet kind: adding a producer in either spelling leaves the gate green and prints nothing in its declared bounds, so a partial gate keeps reading as a complete one.
Possible shapes, not a recommendation
- Extend
objlitinto aliteral: falsesibling (code: IDENTin an object literal) soresolveConstantruns on it, with--self-testcases in the same edit as the header demands. - Report a template-literal
code:asunresolvedrather than dropping it — the gate cannot evaluate it, and the header's own rule says an unresolvable value is a finding, never a silent pass. - Consider whether
rest-approvals-wire-codes.test.tsthen becomes redundant, or stays as the runtime half.
Related: #8087 (the gate), #8885 (the approvals workaround), #9098 / PR #9222 (where this was noticed; that PR added a door-typing half to the same script but did not touch SHAPES).
Generated by Claude Code
Activity
os-zhuang commentedon Aug 17, 2026
Triage: lands in
scripts/check-dispatcher-error-vocabulary.mjs— the gate orbits the ADR-0112 error-code contract, sodomain:spec(machine face; no*.zod.ts/ ledger edits expected). TypeBug: the gate's own declared bound ("a constant this gate cannot resolve is REPORTED as unresolved, never dropped") does not hold for theobjlitshape — acode: CONSTor template-literalcode:in an object literal is silently unseen, and #8885 already paid for the gap with a bespoke runtime pin.pm:queue.Size/model suggestion: M, opus — extend
SHAPES(aliteral: falseobjlit sibling + template-literal-as-unresolved) with--self-testcases in the same edit per the header's own contract; decide in-card whetherrest-approvals-wire-codes.test.tsstays as the runtime half. Serial note: PR #9222 touched the same script (door-typing half) — sync with its landed state before editing.Generated by Claude Code
os-steve commentedon Aug 17, 2026
Claim: PM loop round 1
Session:
session_01Fs18A2DdXLVN2h8PaaFBcPBranch:
claude/issue-9223-error-vocab-nonliteral-codeWorktree:
objectstack-issue-9223Domain:
domain:specFile surface:
scripts/check-dispatcher-error-vocabulary.mjs(SHAPES +--self-testin the same edit), plus a written disposition (not necessarily an edit) onpackages/rest/src/rest-approvals-wire-codes.test.ts(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus— judgment default; this round'snode scripts/pm/dispatch-gates.mjs --tieroutput shows no path-derived mandate and no clause-② suspect path for this surface (gate mechanics, not contract accept/reject behaviour); triage suggestion (M, opus) adoptedSerial constraints cleared: PR #9222 (same script, door-typing half) MERGED into main at
0668f02— same-day churn, dev must sync to currentorigin/mainbefore editing; no open PR touches this script (checked this round); #9228 / #9182 / PR #9191 in-flight surfaces all disjoint. No in-flight sibling's pins assert this gate's behaviour.Generated by Claude Code
fix(gate): see a non-literal `code:` in an object literal (#9223)
fix(gate): declare the sites the widened scan surfaces (#9223)
os-steve commentedon Aug 17, 2026
{ "issue": 9223, "status": "done", "branch": "claude/issue-9223-error-vocab-nonliteral-code", "pr": "https://github.com/objectstack-ai/objectstack/pull/9247", "premise_still_valid": true, "summary": "Closed the gate's own blind spot: `objlit` required a quoted literal, so `code: CONST` and a template-generated `code:` in an object literal matched NOTHING — not reported as unresolved, not reported at all. Added `objlitconst` (resolves the constant, like `classconst`) and `objlittemplate` (reported under a family identity, `${…}` → `*`), with `--self-test` cases in the same edit per the header's own rule; `literal: true|false` became an explicit `resolve: 'literal'|'constant'|'template'`. Two mechanisms the sketch did not name were required by measurement: `resolveConstant` now follows WORKSPACE-package imports (8 of the 15 first-pass findings were resolvable constants behind `@objectstack/*`, scoped per-package and unique-or-nothing so the two-drivers hazard still reports), and a new `runtime-pinned` verdict lets the declaration table classify an interpolated family with evidence — refused on any non-template shape and requiring a `pin:` file that EXISTS. The widening surfaced 6 real sites, all classified; one is a genuine wire hole (see below). Gate green: 13 sites, all classified.", "tests": "All at final HEAD 887fc557a. (1) `pnpm check:dispatcher-error-vocabulary` → `--self-test: 6 shapes + 62 assertions OK` (was 4 shapes + 37) and `OK — 13 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846)`. (2) `pnpm --filter @objectstack/runtime test` → `Test Files 165 passed (165) / Tests 2464 passed (2464)`. (3) `pnpm --filter @objectstack/runtime typecheck` → clean (tsc --noEmit, exit 0). (4) Re-derived gates for my ACTUAL diff via `node scripts/pm/dispatch-gates.mjs` — beyond the dispatch list it named check:cross-package-test-inputs plus the edits-a-test-file convention family; ran check:nul-bytes, check:cross-package-test-inputs, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:error-code-casing, scripts/docs-audit/check-affected-docs.mjs → all OK. `check:type-check-debt --re-measure` needs a full workspace build and is left to CI; no NEW test file was added so its structural half cannot move. (5) Control-char self-scan over all three changed files: no match. (6) REVERSE VERIFICATION, on the real tree, both restored and re-verified green afterwards — direction was red in both, as predicted: adding `{ code: ABLATION_PROBE_CODE }` to `endpoint-policy.ts` produced `[unclassified-site] … 'ABLATION_PROBE_ONE' (objlitconst)`, exit 1, which is precisely the class that produced ZERO output on main; renaming the #8885 pin file away produced `[missing-pin] … but that file does not exist`, exit 1. Baseline on main for contrast: green, 7 sites. No ablation ran against a build artifact, so no rebuild leg applies (this gate reads source, never dist).", "open_questions": [], "out_of_scope_findings": [ "filed as #9246: `UNIQUE_SCOPE_CONFIRMATION_REQUIRED` reaches a wire as `error.code` unregistered — the ADR-0120 D5e posture gate stopping a marketplace install, with `packages/cli/.../install.ts` reading that exact spelling off the wire. Every sibling code in the same seam IS in the ledger, so this member is a gap, not a design choice; it hid only because it is stamped through a constant in an object literal. Landed here as a `pending-registration` row at a new `plugin-route` door (this table explicitly never edits the ledger — that is the spec lane), so PENDING_LEDGER_REGISTRATION is non-empty again and the ratchet is armed: the registration landing reddens the now-stale row. Dedup-searched (only #8846, closed) and filed unassigned.", "noted inside #9246 rather than as a second card: nothing appears to parse the cloud-connection install seam's envelope against ApiErrorSchema at all — which is why nothing caught this. Left as a question for the spec lane on that card instead of a speculative gate." ] }Notes for the PM (outside the machine-read block)
Required in-card disposition — written in the PR body, stated here too:
packages/rest/src/rest-approvals-wire-codes.test.tsSTAYS as the runtime half and is NOT edited. Seeing an interpolation is not evaluating one: the gate cannot tell which codes`APPROVAL_${action.toUpperCase()}_FAILED`produces, and all nine ARE registered — a fact no source scan can establish. The pin enumerates the registered action routes and parses each generated code against the closed union, so a tenth route fails there mechanically. The repair to #8885's actual complaint is that the two halves are now explicitly complementary: theAPPROVAL_*_FAILEDrow names the pin inpin:, and the gate fails if that file disappears — which it would not have before this PR.Two contract-shaped extensions, flagged for review rather than buried:
CodeDoorgainedplugin-route(a plugin's own Hono routes pass through neither the dispatcher nor thepackages/restdoors — the install seam is one, and it does answer with an ADR-0112-shapederror.code), andCodeVerdictgainedruntime-pinned. The conformance suite'sdoor !== 'none' ⇒ pending-registrationinvariant had to admit the latter; I kept it narrow (template shapes only, pin must exist, excluded fromPENDING_LEDGER_REGISTRATIONso no undischargeable debt reaches #8846) and pinned each guard in both directions. If the maintainer would rather the three approvals templates be rewritten to stamp literal codes instead — which would need no new verdict, but is wire-behaviour work inrest-server.tsoutside this card — that is a clean alternative I deliberately did not take unasked.One standing-contract conflict, per instruction to report rather than pick a side silently: the dispatch said the PM would apply
skip-changeset; my standing rules make it the dev's step in this repo. I applied it and read it back (search_pull_requests—get_labelscannot resolve a PR number, so the read-then-write union was not possible in the usual way; the read-back showsskip-changesetas the only label, so nothing was clobbered).Worktree removed cleanly without
--force. Per the standing contract I am not waiting on CI.Generated by Claude Code
Generated by Claude Code
os-steve commentedon Aug 17, 2026
ACCEPT — PR #9247 (reviewer of record: spec seat, session
session_01Fs18A2DdXLVN2h8PaaFBcP, round 1).Verified on GitHub, not from the report's self-description:
main, first lineFixes #9223(correct — the card's whole scope is delivered). Changed files:scripts/check-dispatcher-error-vocabulary.mjs,packages/runtime/src/dispatcher-error-vocabulary.ts,packages/runtime/src/error-envelope.conformance.test.ts. The two runtime files exceed the claim's declared surface by one module; both are the gate's own declaration table + its conformance half, explained in the PR body — accepted as in-family, not a breach.objlitconstresolves constants (workspace-import following, unique-or-nothing),objlittemplatereports a family identity instead of dropping the site — the header's "reported, never dropped" bound now holds for object-literal sites. Ablation evidence is directional in both legs and was restored after.plugin-routedoor,runtime-pinnedverdict): judged here as gate strengthening, not weakening — sites of this class were previously invisible; the new verdict is machine-constrained on both sides (refused on non-template shapes;pin:file must exist; conformance test pins template-only + family identity + exclusion fromPENDING_LEDGER_REGISTRATION). Machine-face discretion, no human-floor trigger.rest-approvals-wire-codes.test.tsstays as the runtime half, now named inpin:so it can no longer vanish silently — this is the honest division of labour, accepted.@objectstack/runtimeis published, but the touched module is not on the package's export surface;skip-changesetlabel applied (this repo's real mechanism) and the Check Changeset job concluded success. Accepted per PR-body reasoning.packages/spec/src/**path — gate not triggered.UNIQUE_SCOPE_CONFIRMATION_REQUIREDunregistered on a live wire) — verified filed, unassigned, with the ratchet armed via thepending-registrationrow. Awaits triage; the ledger registration will be a spec-lane card.Landing: ESLint and TypeScript Type Check are still
in_progressat head887fc55— the PR flips ready and enters the queue only after both concludesuccess(gate-job conclusions, not the aggregate).Generated by Claude Code
feat(spec): register UNIQUE_SCOPE_CONFIRMATION_REQUIRED in ERROR_CODE…
fix(gate): see a non-literal `code:` in an object literal — a constan…
feat(spec): register UNIQUE_SCOPE_CONFIRMATION_REQUIRED in ERROR_CODE…
fix(tooling): dispatcher error-vocabulary scan reports lowercase code…
fix(tooling): dispatcher error-vocabulary scan reports lowercase code…
feat(devx): the dispatcher vocabulary gate learns the APIError argume…