Skip to content

fix(gate): see a non-literal code: in an object literal — a constant resolves, a template is reported (#9223) - #9247

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-9223-error-vocab-nonliteral-code
Aug 17, 2026
Merged

fix(gate): see a non-literal code: in an object literal — a constant resolves, a template is reported (#9223)#9247
os-steve merged 2 commits into
mainfrom
claude/issue-9223-error-vocab-nonliteral-code

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9223

check-dispatcher-error-vocabulary publishes a bound in its own header — "A constant this gate cannot resolve is REPORTED as unresolved, never dropped" — and that bound held for classconst and for nothing else. objlit demanded a quoted literal, so a code: SOME_CONST or a template-generated code: `A_${x}_B` in an object literal matched nothing: not a finding, not an unresolved note, not a line of output. A partial gate read as a complete one. This makes the header's bound true for object-literal code: sites.

Verified against current origin/main (2f65b1b42), which is past PR #9222's door-typing half (0668f02) — the card quotes pre-#9222 line content; the SHAPES table it describes is unchanged by that merge, so the premise held.

What changed

Two new shapes in SHAPES, with --self-test cases in the same edit as the header's edit rule demands (literal: true|false became an explicit resolve: 'literal' | 'constant' | 'template', since there are now three ways a match becomes a code):

  • objlitconstcode: CONST in an object literal. The same indirection classconst already followed, in the shape that stamps most of this repo's codes.
  • objlittemplate — an interpolated code:. No source scan can evaluate one, so it is reported under a family identity (${…}*, e.g. APPROVAL_*_FAILED) and must be classified like any other site. A template with no interpolation is a literal wearing backticks and is read as one.

The resolver now follows workspace-package imports.resolveConstant treated every bare specifier as "a dependency's constant — out of scan reach", but @objectstack/core is inside the scan by construction. Without this, the widening reported 8 resolvable constants as unresolvable (7 × ANONYMOUS_DENY_CODE, which resolves to the registered UNAUTHENTICATED) — noise that would have buried the real finding. Scoped to the named package's own sources and unique-or-nothing: two values for one name inside one package resolves to null and is reported, never guessed. That is the header's two-drivers-one-constant-name hazard, one package in. A genuine third-party dependency has no workspace directory and stays out of reach. Package directories are read from each package.json rather than derived from the name — @objectstack/plugin-auth lives at packages/plugins/plugin-auth, so name-to-path arithmetic answers wrongly.

runtime-pinned, a new verdict for the one thing a source scan cannot decide, and deliberately hard to misuse: refused on any shape other than objlittemplate (on a literal it would be an exemption from the registry check — the very hole this gate is), required to name a pin: file, and the gate fails when that file does not exist.

What the widening surfaced — measured before scope was settled

Running the widened gate against the whole tree turned a green gate (7 classified sites) into 15 findings; the workspace resolver removed 8 of those as resolvable, leaving 6 real sites now classified in dispatcher-error-vocabulary.ts. Final state: 13 sites, all classified, gate green.

One is a genuine, previously-invisible hole rather than a bookkeeping row:

UNIQUE_SCOPE_CONFIRMATION_REQUIRED reaches a wire unregistered — the ADR-0120 D5e posture gate stopping a marketplace install returns it as error.code, and packages/cli/.../install.tsreads that exact spelling off the wire to print the per-index decision list. That the seam speaks registered vocabulary is measurable, not assumed: every sibling code in the same file (PLUGIN_MANIFEST_INVALID, MARKETPLACE_UNAVAILABLE, INVALID_REQUEST, RESOURCE_NOT_FOUND, …) is already in the ledger, which is why the scan never reported them. It hid for one reason only: it is stamped through a constant in an object literal. Classified pending-registration at a new plugin-route door (a plugin mounting its own Hono routes passes through neither the dispatcher nor the packages/rest doors); the ledger edit itself belongs to the spec lane, which this table explicitly does not touch — filed as #9246. PENDING_LEDGER_REGISTRATION is non-empty again and the ratchet is armed: when that registration lands, the stale row reddens CI and must come out.

The other four are non-wire and say which vocabulary they belong to: better-auth's own YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER (domains/auth.ts catches everything the auth service throws and answers INTERNAL_ERROR_MESSAGE, 500 — re-verified, not inherited from the older row), the two ADR-0087 conversion diagnostics passed to onNotice/onConflict, and ERR_BULK_PER_ROW_HOOK_LIMIT, whose declaring source already rules on itself ("Deliberately … NOT an ADR-0112 wire code").

Disposition: rest-approvals-wire-codes.test.ts STAYS as the runtime half

Required by the card, so stated plainly: the #8885 bespoke pin is not redundant under the widened gate, and it is not edited here.

The measurement is what decides it. The widened scan can now see the three approvals templates — that is the fix — but seeing an interpolation is not evaluating one. The gate cannot say which codes `APPROVAL_${action.toUpperCase()}_FAILED` produces, so it cannot say whether they are registered; all nine ARE, and nothing in a source scan can know that. The pin does exactly the job the scan structurally cannot: it enumerates the registered POST /approvals/requests/:id/<action> routes and asserts the code each catch arm would generate parses against ApiErrorSchema's closed union, mirroring the production template exactly. A tenth action route whose generated code nobody registers fails there, mechanically.

So the two halves are now explicitly complementary instead of accidentally overlapping, which is the real repair to #8885's complaint that "one package has a hand-written class pin doing the job the repo-wide gate advertises". The gate no longer advertises that job for templates: the APPROVAL_*_FAILED row names the pin in pin:, and the gate fails if that file disappears — so the runtime half can no longer be deleted silently, which was true before this PR. What remains genuinely unsolved for any other package is unchanged and now visible: a new template family anywhere is an unclassified-site finding demanding either literal codes or its own pin.

Verification

Run at final HEAD 887fc557a:

  • pnpm check:dispatcher-error-vocabulary — green. --self-test: 6 shapes + 62 assertions OK (was 4 shapes + 37); main run OK — 13 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry.
  • pnpm --filter @objectstack/runtime test165 files / 2464 tests passed. Includes the declaration half's conformance suite, whose door !== 'none' ⇒ pending-registration invariant had to admit runtime-pinned (a source-scan limit, not a reachability claim), plus a new case asserting every runtime-pinned row names its runtime half and only a template may carry it. A stale comment claiming the pending list is empty was corrected — it is not, and the reason is now recorded there.
  • pnpm --filter @objectstack/runtime typecheck — clean.
  • Re-derived gates for the actual diff (node scripts/pm/dispatch-gates.mjs, beyond the dispatch list): check:cross-package-test-inputs, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:error-code-casing, check:nul-bytes, scripts/docs-audit/check-affected-docs.mjs — all green. 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.
  • Ablation, on the real tree, both restored afterwards. A new code: ABLATION_PROBE_CODE producer in endpoint-policy.ts[unclassified-site] … 'ABLATION_PROBE_ONE' (objlitconst), exit 1 — the class of site that produced no output at all before this PR. Renaming the pin file away ⇒ [missing-pin] … but that file does not exist, exit 1.

No changeset: scripts and an internal declaration table publish nothing user-visible.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 8 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx(via approvals.getRequest (sdk), getRequest (sdk))
  • content/docs/api/plugin-endpoints.mdx(via /approvals/requests/:id (route))
  • content/docs/automation/approvals.mdx(via getRequest (sdk), /approvals/requests/:id (route))
  • content/docs/automation/flows.mdx(via /approvals/requests/:id (route))
  • content/docs/kernel/services-checklist.mdx(via /approvals/requests/:id (route))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via getRequest (sdk))
  • content/docs/releases/v17.mdx(via getRequest (sdk))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json origin/mainpackageMentionDocs.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-steveos-steve added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l tests labels Aug 17, 2026 — with Claude
@os-steve
os-steve marked this pull request as ready for review August 17, 2026 06:49
@os-steve
os-steve added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit afba4ecAug 17, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-9223-error-vocab-nonliteral-code branch August 17, 2026 07:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-dispatcher-error-vocabulary is blind to a non-literal code: in an object literal — a constant or template produces no finding, silently

2 participants

@os-steve@claude