Skip to content

fix(tooling): dispatcher error-vocabulary scan reports lowercase codes and the three stamp positions it could not see (#9460) - #9573

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-9460-widen-dispatcher-error-vocabulary-scan
Aug 18, 2026
Merged

fix(tooling): dispatcher error-vocabulary scan reports lowercase codes and the three stamp positions it could not see (#9460)#9573
os-zhuang merged 2 commits into
mainfrom
claude/issue-9460-widen-dispatcher-error-vocabulary-scan

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9460

Widens check:dispatcher-error-vocabulary so it measures "is this code outside the vocabulary" instead of "is this code SCREAMING_SNAKE" — scope half (1) only, per triage. ⛔ No ledger edit, no allowlist entry, no narrowed pattern, no raised ceiling.

The card's premise was wrong, and the real shape is bigger

The issue diagnosed a case-sensitive pattern. It is notassign, classfield and objlit already matched [A-Za-z]. Two explicit post-match filters dropped the value with a named delegate:

if (!/^[A-Z][A-Z0-9_]*$/.test(code)) continue; // lowercase -> check:error-code-casing

And the card's own motivating producer was invisible for a completely different reason, so the prescribed one-line widening would not have found it. Measured, not assumed:

scansitesnew findings
today12
naive case-widening (the card's literal prescription)9482, and owd_widening_forbidden is not among them
this PR186, including owd_widening_forbidden

All 82 of the naive widening's new findings were D6/D6b/D6c neighbours or Zod's own issue codes — it reports ctx.addIssue({ code: 'custom' }) as an unregistered ObjectStack error code, which is simply false.

What was actually broken: a hole between two gates

check:error-code-casing needs a quoted lowercase literal beside the token code (code: 'x', .code = 'x', code === 'x', code?: 'x' | 'y'). In those positions the delegation is genuine — that gate reads the identical characters and carries the D6/D6b/D6c discrimination this one does not have, so objlit and assign keep delegating.

Everywhere else it was a hole, not a hand-off. A code arriving through a constant, a template, or a helper parameter has no quoted literal at the stamp site, so that gate is structurally blind — and this gate dropping it for its casing meant nobody reported it. Both gates read object-posture-gate.ts and both reported nothing, each leaving it to the other. Those shapes now carry lowercase: 'here'.

Three stamp positions the scan could not see

  • codehelper — a file declares one factory and throws through it everywhere. The stamp (err as any).code = code knows the token code but not the value; the call site knows the value and never writes the token. The join is the parameter, so its index names the argument to read — derived, never assumed zero, because makeError(status, code, message) and exposureError(message, code, status) both put it second and a first-argument rule reads a number and an English sentence as error codes. Constructors count, and a call-site argument that is itself a constant goes through the existing resolveConstant.
  • assignconsterr.code = DENY_CODE. check-dispatcher-error-vocabulary is blind to a non-literal code: in an object literal — a constant or template produces no finding, silently #9223 closed exactly this gap for object literals; the assign position kept it.
  • assign with a cast on the left — the old anchor demanded a bare identifier where (err as any).code = 'X' puts a ).

Predicted, then observed

Predicted: the pre-change scan cannot see the six sites this one reports, so run against the same tree with the same declaration table it must call every one of them stale.

Observedorigin/main's script, this tree:

check-dispatcher-error-vocabulary: 6 finding(s)
[stale-row] ... 'FLOW_CONVERSION_CONFLICT' at packages/metadata-protocol/src/protocol.ts (assign) but the scan no longer finds it.
[stale-row] ... 'owd_widening_forbidden' at packages/plugins/plugin-security/src/object-posture-gate.ts (codehelper) but the scan no longer finds it.
[stale-row] ... 'NO_SUCH_RUN' / 'NOT_COMPENSABLE' / 'PLAN_CHANGED' / 'PREFLIGHT_FAILED' at packages/core/src/utils/migration-journal.ts (codehelper)
scope: ... 12 unregistered code-stamping site(s) found; 18 classified.

Six producers sitting in source that the old scan reports as "moved or went away". A zero-hit control is not a finding, so the widened pattern was also fed the known lowercase stamp directly and reported it by name.

Every new find: fixed or filed, none silently classified

Six new sites, all classified with evidence in dispatcher-error-vocabulary.ts, and the two substantive decisions filed rather than taken here:

PENDING_LEDGER_REGISTRATION goes 0 → 2 and classified sites 12 → 18 — the ratchet moving in the direction it exists to move.

Residual gap found while measuring and filed, not fixed: #9568 — a code held in a local ternary of literals (sys-metadata-repository.ts's live 403 NOT_CREATABLE / NOT_OVERRIDABLE) is still unresolved. Named in the published bounds too.

⚠️Premise note, no action taken: the issue states #9232 already demotes this code at the flat door. On origin/main at 65d4fff it does not — packages/rest/src/meta-object-owd-gate.test.ts asserts the raw lowercase code on the wire, and no commit carrying that change is in the history. Recorded in #9567; out of scope here.

Verification — all at 7c1840e46, the final commit

check-dispatcher-error-vocabulary: OK — 18 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry (#8846).
scope: 1839 non-test source files under packages/; 292 registered codes (239 ledger + 53 standard); 18 sites found; 18 classified.
--self-test: 8 shapes + 81 assertions OK (was 6 shapes + 57)
check-error-code-casing: no lowercase error codes in 4212 scanned file(s)
check-cross-package-test-inputs / affected-docs / nul-bytes / changeset gates: OK
pnpm --filter @objectstack/runtime typecheck: clean
pnpm --filter @objectstack/runtime test: 169 files, 2515 tests passed

The self-test is pinned capable of failing, in both directions:

  • revert the lowercase-ownership rule → 4 red, incl. "the code-carrying-helper producer this widening exists for was not reported"
  • read argument zero instead of the derived parameter index → 2 red

Restored byte-identical after each (empty git diff), and --self-test green again.

⚠️Lint & Repo Gates is the CI job that carries this gate family — the local runs above are not that job green.

File-surface note for the PM: the claim named scripts/check-dispatcher-error-vocabulary.mjs only. packages/runtime/src/dispatcher-error-vocabulary.ts is also edited — it is the gate's declaration half, and a widened scan that reports 6 more sites leaves the gate red until they are classified there. Same defect class, same gate family, no other claim on the file, and the row shape is pinned by the 12 rows already present. Flagging it rather than assuming it was covered.

Generated by Claude Code


Generated by Claude Code

…s and the stamp positions it could not see (#9460)
The gate's published bound said "only SCREAMING_SNAKE literals" and handed
lowercase to check:error-code-casing. Half that delegation was real and half
was a hole — and the hole is where plugin-security's live 403
`owd_widening_forbidden` sat through two ADR-0112 sweeps, read by both gates
and reported by neither.
Lowercase is now reported except in the two positions where
check:error-code-casing reads the identical characters (`code: 'x'`,
`.code = 'x'`), where it carries the D6/D6b/D6c discrimination this gate does
not have. Measured first: reporting every lowercase stamp took the scan from
12 sites to 94, and all 82 new findings were D6/D6b/D6c or Zod's own issue
codes.
Three unseen stamp positions, all widenings: `codehelper` (a factory whose
parameter carries the code, so the literal lives at the call site with no
`code` token beside it — the parameter INDEX is derived, not assumed zero),
`assignconst` (the assign position's constant sibling, the gap #9223 closed
for object literals), and `assign` with a cast on the left.
12 -> 18 classified sites; 0 -> 2 awaiting a ledger entry. No allowlist, no
narrowed pattern, no raised ceiling.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WKSnqAaoqtW3QX7SSf1Vk
#9460)
The two new stamp positions the widened scan reports need members in the
declaration table's own union, or every row carrying them is a tsc error.
Caught by `pnpm --filter @objectstack/runtime typecheck`, not by the gate —
the gate reads the table textually and never typechecks it.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WKSnqAaoqtW3QX7SSf1Vk
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/wire-format.mdx(via /api/v1/meta/object/:name (route))
  • content/docs/permissions/authorization.mdx(via owd_widening_forbidden (literal))
  • content/docs/protocol/kernel/error-handling.mdx(via /api/v1/meta/object/:name (route))
  • content/docs/protocol/kernel/http-protocol.mdx(via /api/v1/meta/object/:name (route))
  • content/docs/ui/forms.mdx(via /api/v1/meta/object/:name (route))
What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 23 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 e6ee690999b5e47e88eece047a6b9edc311d0729packageMentionDocs.

Which tree this was computed on

This run read content/docs from 40e00b64bbc424e8b7ae33ebd79b65b26be1c0fd — the merge of head 7c1840e46599a7ab74b561d61807cc575aad558b into base e6ee690999b5e47e88eece047a6b9edc311d0729, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 40e00b64bbc424e8b7ae33ebd79b65b26be1c0fd && git checkout 40e00b64bbc424e8b7ae33ebd79b65b26be1c0fd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e6ee690999b5e47e88eece047a6b9edc311d0729 7c1840e46599a7ab74b561d61807cc575aad558b && git checkout -B drift-repro e6ee690999b5e47e88eece047a6b9edc311d0729 && git merge --no-ff 7c1840e46599a7ab74b561d61807cc575aad558b
node scripts/docs-audit/affected-docs.mjs --json e6ee690999b5e47e88eece047a6b9edc311d0729

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

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 e6ee690999b5e47e88eece047a6b9edc311d0729 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Aug 18, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 18, 2026 11:35
@os-zhuang
os-zhuang enabled auto-merge August 18, 2026 11:35
@os-zhuang
os-zhuang added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit c8e85fcAug 18, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9460-widen-dispatcher-error-vocabulary-scan branch August 18, 2026 12:07
os-steve pushed a commit that referenced this pull request Aug 18, 2026
…on row (#9567)
#9460 landed on main (PR #9573) while this branch's registration PR was in
the merge queue, adding a pending-registration row for
FLOW_CONVERSION_CONFLICT to dispatcher-error-vocabulary.ts's
UNREGISTERED_CODE_SITES. That collided with this branch's ledger admission
of the same code -- a registered code with a pending-registration row still
on file fails the #8087 conformance gate in the direction the row's own
docs describe ("a pending-registration row whose code is registered fails
the gate in the other direction").
Ratchets the row out following the #8846/#9246 precedent exactly: delete
the discharged row, fold a one-line note into the running log at the top of
UNREGISTERED_CODE_SITES. owd_widening_forbidden -- the #9460 batch's other
new row -- stays pending; its lowercase spelling is a naming decision for
the maintainer (#9567 half 2), not a plain admission, and is untouched.
node scripts/check-dispatcher-error-vocabulary.mjs: PENDING_LEDGER_REGISTRATION
2 -> 1 (only owd_widening_forbidden remains). error-envelope.conformance.test.ts
(the #8087 pin): 51/51 passing. error-code-ledger.test.ts: 17/17 passing.
check:generated: all 13 artifacts still up to date after the merge.
Part of #9567
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/ltooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] check:dispatcher-error-vocabulary cannot see a lowercase thrown code — plugin-security's live owd_widening_forbidden was never swept

2 participants

@os-zhuang@claude