Skip to content

fix(tooling): check-error-code-casing — reach our default through an ||/?? fallback chain, and stop printing an unqualified clean sweep - #10760

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10658-error-code-casing-fallback
Aug 21, 2026
Merged

fix(tooling): check-error-code-casing — reach our default through an ||/?? fallback chain, and stop printing an unqualified clean sweep#10760
os-zhuang merged 1 commit into
mainfrom
claude/issue-10658-error-code-casing-fallback

Conversation

@claude

@claude claude Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Closes #10658. Verified at 613fc4363b; base origin/main = 8163a1c8e2.

One file: scripts/check-error-code-casing.mjs. No changeset — gate tooling, publishes nothing (skip-changeset).

The defect, reproduced before anything changed

All four CODE_POSITION_PATTERNS anchor the string literal immediately after the position token, so an intervening expression makes the literal invisible to the whole set — silently, and then the run prints a total that reads as complete.

$ node scripts/check-error-code-casing.mjs      # on the base
EXIT=0
no lowercase error codes in 4356 scanned file(s) (ADR-0112).

That tree has two, both inside the scanned set. Positive control, so the zero is read as a broken probe rather than a clean tree — the gate's own emission regex on both spellings:

code: 'oops_bad'                -> ["oops_bad"]
code: p?.code || 'oops_bad'     -> []
code: p?.code ?? 'oops_bad'     -> []

Re-derived extent: 2, and it is still 2

Swept packages/** .ts/.tsx (comments masked, the gate's own walk) with a deliberately broad probe — code: then any non-quote gap, then ||/??, then a lower_snake literal:

file line code
packages/plugins/plugin-auth/src/register-sso-provider.ts 411 request_domain_verification_failed
packages/plugins/plugin-auth/src/register-sso-provider.ts 465 verify_domain_failed

2 hits in 4356 files — matches the card, so the known-list below is exactly two rows. The sibling shape .code = <expr> || '<lower>' measures 0 occurrences; it is covered anyway (below), so the class is closed rather than the instance. origin/main advanced to 78ac958552 while this was in flight and touched 0 paths under packages/, so the scanned population — and the extent — are unchanged.

The landing hazard, and how it is resolved

Triage split the two wire-visible renames to #10716 (services lane), so this PR must not rename them — but a widened recognizer finds them on main and exits 1. Option 2 was checked, not assumed: #10716 is open and both codes are still live at 411/465 on the base, so it has not landed.

Resolved by option 1, the KNOWN_IMPORT_UNSAFE shape from #10665: a ⛔ SHRINK-ONLY KNOWN_LOWERCASE_CODES carrying exactly those two rows, each naming the owning card. Keyed <file>::<literal> rather than by line, so a line shift cannot quietly invalidate an entry. Nothing new can join: a fresh finding is never absorbed by the list, and the author-facing remedy says so and refuses that route rather than offering it. When #10716 lands, each entry goes stale and the gate fails until the line is deleted — the list only ever shrinks. Both directions are pinned in --self-test.

Where the line is drawn on vendor codes

The widened pattern still only ever captures a string literal, and a literal in our source is by construction ours. A vendor code passing through is a runtime value (parsed?.code, err.code, a variable) — it has no literal for any pattern here to capture, before or after this change. So widening cannot start flagging a pass-through; it reaches only the default we author, which is exactly the operand ADR-0112 D1 governs (the code our failing request answers with). A vendor's spelling hard-coded as our default is a real violation, not a false positive.

The second half of the line is the gap class, and it is what keeps a match inside one property's value. It admits an operand chain (identifiers, member/optional access, calls, indexes, further ||/??) and refuses , ; : { } = and every quote. The real false positive here is a neighbour's fallback —

{ code: a.code, message: m || 'lower_thing' }

— and the comma is what stops it. Pinned as a reject case. The length bound is a runaway guard, also pinned.

Reject side, at tree scale

Differential sweep of the four base patterns vs. the five, per pattern, over the same 4356 files. The new pattern is additive — the other four are byte-identical to the base and unchanged in what they match:

pattern base this branch added removed
emission 508 508 0 0
assignment 0 0 0 0
comparison 176 176 0 0
union-type 2 2 0 0
fallback (new) 0 2 the two rows above

Zero false positives repo-wide: the new pattern's only two raw matches are the two known rows. check:dispatcher-error-vocabulary reports neither code on the base (a chain with one runtime limb reduces to nothing under #9568's all-or-nothing rule), so this shape was owned by no gate at all — which is the card.

Both spellings pinned, as a pair

--self-test grows 12 recognizer cases and 4 registry cases (17 -> 29, plus 4). The pair matters: the direct spelling of the same code is pinned beside the fallback spelling, because a recognizer that reached the new shape by breaking the old one would pass a self-test that only pinned the new one.

Ablation

Prediction stated before running: reverting the widening reddens exactly 4 recognizer cases — the four expecting a hit through the fallback pattern — while the reject-side cases (expecting 0) and the registry cases stay green.

Mutation confirmed on disk before measuring: the two anchors name: 'fallback', and its re: line both went 1 -> 0, 151 bytes removed, git diff --stat showing 4 deletions. Observed, on the mutated tree:

  self-test "our default behind an || fallback": expected 1 hit(s), got 0
  self-test "our default behind a ?? fallback": expected 1 hit(s), got 0
  self-test "fallback in the assignment position": expected 1 hit(s), got 0
  self-test "fallback at the end of a chain": expected 1 hit(s), got 0
check-error-code-casing self-test failed (4 case(s)).     EXIT=1

4 predicted, 4 observed, and the direct-spelling half stayed green — the pair did its job. The full scan independently flipped 0 -> 1 with 2 stale KNOWN_LOWERCASE_CODES entries, which exercises the shrink-only limb from the other side. Restore leg proven too: anchors back to 1, tree clean against HEAD, both runs green again. No build/dist is involved — package.json runs this script from source — so there is no stale-artifact risk in either leg.

The verdict shape: yes, it now says what it could not read

The card asks this to be weighed either way. Decided yes, on the #10501 precedent, because the reporting defect is the point of the card: an unqualified total renders a bounded read exactly like a complete one, and that is what shipped over a tree carrying two.

no unlisted lowercase error codes in 4356 scanned file(s) (ADR-0112).
  what this run did NOT read - the line above is a bounded claim, not a clean bill:
    - 23 file(s) skipped whole (EXEMPT_FILES: D6/D6b/D6c and foreign vocabularies)
    - 12 literal(s) suppressed by an adr0112-ok: reason
    - 2 known lowercase code(s) deferred to their owning card (KNOWN_LOWERCASE_CODES)
    - a code value with NO literal at the position - a constant, a template, a ternary,
      a helper parameter - is out of reach for every pattern here by construction; that
      half belongs to check:dispatcher-error-vocabulary, which reports its own scope.

Two deliberate choices about what it counts:

The qualifier prints on the failure path too: a partial finding list reads as complete for the same reason a partial total does.

Gates

node scripts/pm/dispatch-gates.mjs with no paths, at 613fc4363b, clean tree — it derived the change set from the merge base as 1 path (a two-dot origin/main diff shows 5 files here, four of them sibling PRs that landed after this branch was cut). It named 3 families; the other three below were run because they were reasoned, not named:

gate verdict line exit
check:error-code-casing no unlisted lowercase error codes in 4356 scanned file(s) 0
check:cross-package-test-inputs OK: 13 package(s) read outside themselves, all declared 0
check:ratchet-remedy-authority 111 scripts swept; 7 mark ... 3 turn it down outright, 101 hand out no ratchet-expanding remedy 0
check:entry-guard 125 scripts/ file(s) ... 83 export bindings, 73 of them inert on import (10 known-unsafe, SHRINK-ONLY) 0
check:parse-guard self-test + sweep clean 0
check:nul-bytes OK (scanned 6234 text file(s) ... no raw ASCII control bytes) 0

Two of those were run because a new shrink-only registry is exactly the shape they police, and both were verified directly rather than inferred from a green exit:

  • check:ratchet-remedy-authority hand-classifies this very file as excluded. A shrink-only registry whose remedy offers expansion would flip it to unmarked — a violation. Its own classify() run against this branch's source returns excluded, 0 live offers, 0 refused, 0 anchors, so the control entry stays correct and untouched.
  • check:entry-guard — this file gains a second export (partitionKnown), and fix(scripts): burn 25 of the 35 KNOWN_IMPORT_UNSAFE entry-guard debts down #10704's isEntrypoint guard on it is untouched. It stays off the KNOWN_IMPORT_UNSAFE SHRINK-ONLY ledger (0 occurrences), and importing it for its exports alone runs nothing, which is how the recognizer was proven against the real file.

Out of scope, filed separately

The two renames are not addressed here#10716 remains open and owns them. One follow-up filed for a documentation drift this change creates in the sibling gate's delegation map; see the report comment on #10658.

Generated by Claude Code


Generated by Claude Code

…, and qualify its verdict

The four CODE_POSITION_PATTERNS all anchor the string literal immediately after
the position token, so an intervening expression makes the literal invisible to
the whole set. Two live wire-visible codes shipped through that gap while the
gate printed an unqualified 'no lowercase error codes'.

- fifth pattern reaching our default through an ||/?? chain, bounded so a match
  cannot leap into a neighbouring property's fallback;
- KNOWN_LOWERCASE_CODES, shrink-only, carrying the two codes whose rename is
  owned by #10716 (services lane) — a stale entry fails;
- the verdict now states what the run could not read (#10501 precedent).

Part of #10658
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 10:57
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit f06d8ed Aug 21, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10658-error-code-casing-fallback branch August 21, 2026 11:11
os-zhuang pushed a commit that referenced this pull request Aug 21, 2026
… runtime-limbed chain

`scripts/check-dispatcher-error-vocabulary.mjs` documents its delegation
contract with `check:error-code-casing` by enumerating that gate's
recognizers. PR #10760 added a fifth — the OUR-DEFAULT slot of a `||`/`??`
fallback chain — and the enumerations were left short by one.

Comment-only. Both enumerations now carry the fifth spelling as the owning
gate spells it, and the #9568 ALL-OR-NOTHING bound now records who owns a
runtime-limbed chain, split by position because the two answers differ:
delegated to `check:error-code-casing` at the stamp site, owned by nobody in
a local's initializer (filed as #10897).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_c970724d-303c-5614-9d20-a3f92205cfad
os-zhuang pushed a commit that referenced this pull request Aug 21, 2026
`check-error-code-casing`'s fifth recognizer (#10760) anchors on the POSITION
token — `code:` / `code?:` / `.code =` — so it reaches an `||`/`??` fallback
chain only where the chain sits AT the stamp site. The identical chain one
indirection earlier, in a local's initializer, matched nothing:

    const code = parsed?.code || 'lower_thing';   // our authored default
    err.code = code;

`const code =` is neither spelling, and a type annotation does not rescue it:
`const code: string = …` does match `code:`, but then the gap has to cross an
`=`, which that character class refuses on purpose.

Nothing else saw it either. `check:dispatcher-error-vocabulary` reaches the
local (`err.code = code` is its `codehelper`/`assignconst` shape) but its
`resolveConstant` reduction is ALL-OR-NOTHING by design (#9568): one runtime
limb reduces the whole chain to nothing, because half an expression's values
is a finding wrong in both directions at once. That bound is deliberate and is
untouched here — its verdict line is byte-identical before and after.

So the literal half is this gate's, on exactly the reasoning #10760 published
for the stamp site: the capture is still only ever a STRING LITERAL, and a
literal in our source is by construction the default WE author. Where we write
the chain does not change whose default it is; the asymmetry was an artifact of
where the recognizer anchored, not a decision anyone took.

Adds a sixth recognizer, `local-fallback`, with the annotation gap spelled
`[^=;\n]` (the same spelling `classfield` uses in the sibling gate) and the
fifth pattern's own gap class and tail verbatim. The delegation runs the OTHER
way for an ALL-literal initializer — a bare literal, a ternary, a chain — which
IS reducible and stays the dispatcher gate's site under `assignconst`,
lowercase included; the quote refusal keeps this pattern off those, so one
literal never gets two reporters.

Also updates the three delegation enumerations in
`check-dispatcher-error-vocabulary.mjs` that #10762 had just corrected: they
recorded this position as owned by NOBODY and named #10897 as the open half.
Comment-only there; 8 shapes + 102 assertions and the full-run verdict all
unchanged.

Fixes #10897

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_c970724d-303c-5614-9d20-a3f92205cfad
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 23, 2026
… runtime-limbed chain (objectstack-ai#10898)

`scripts/check-dispatcher-error-vocabulary.mjs` documents its delegation
contract with `check:error-code-casing` by enumerating that gate's
recognizers. PR objectstack-ai#10760 added a fifth — the OUR-DEFAULT slot of a `||`/`??`
fallback chain — and the enumerations were left short by one.

Comment-only. Both enumerations now carry the fifth spelling as the owning
gate spells it, and the objectstack-ai#9568 ALL-OR-NOTHING bound now records who owns a
runtime-limbed chain, split by position because the two answers differ:
delegated to `check:error-code-casing` at the stamp site, owned by nobody in
a local's initializer (filed as objectstack-ai#10897).


Claude-Session: https://claude.ai/code/session_c970724d-303c-5614-9d20-a3f92205cfad

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 23, 2026
…too (objectstack-ai#10914)

`check-error-code-casing`'s fifth recognizer (objectstack-ai#10760) anchors on the POSITION
token — `code:` / `code?:` / `.code =` — so it reaches an `||`/`??` fallback
chain only where the chain sits AT the stamp site. The identical chain one
indirection earlier, in a local's initializer, matched nothing:

    const code = parsed?.code || 'lower_thing';   // our authored default
    err.code = code;

`const code =` is neither spelling, and a type annotation does not rescue it:
`const code: string = …` does match `code:`, but then the gap has to cross an
`=`, which that character class refuses on purpose.

Nothing else saw it either. `check:dispatcher-error-vocabulary` reaches the
local (`err.code = code` is its `codehelper`/`assignconst` shape) but its
`resolveConstant` reduction is ALL-OR-NOTHING by design (objectstack-ai#9568): one runtime
limb reduces the whole chain to nothing, because half an expression's values
is a finding wrong in both directions at once. That bound is deliberate and is
untouched here — its verdict line is byte-identical before and after.

So the literal half is this gate's, on exactly the reasoning objectstack-ai#10760 published
for the stamp site: the capture is still only ever a STRING LITERAL, and a
literal in our source is by construction the default WE author. Where we write
the chain does not change whose default it is; the asymmetry was an artifact of
where the recognizer anchored, not a decision anyone took.

Adds a sixth recognizer, `local-fallback`, with the annotation gap spelled
`[^=;\n]` (the same spelling `classfield` uses in the sibling gate) and the
fifth pattern's own gap class and tail verbatim. The delegation runs the OTHER
way for an ALL-literal initializer — a bare literal, a ternary, a chain — which
IS reducible and stays the dispatcher gate's site under `assignconst`,
lowercase included; the quote refusal keeps this pattern off those, so one
literal never gets two reporters.

Also updates the three delegation enumerations in
`check-dispatcher-error-vocabulary.mjs` that objectstack-ai#10762 had just corrected: they
recorded this position as owned by NOBODY and named objectstack-ai#10897 as the open half.
Comment-only there; 8 shapes + 102 assertions and the full-run verdict all
unchanged.

Fixes objectstack-ai#10897


Claude-Session: https://claude.ai/code/session_c970724d-303c-5614-9d20-a3f92205cfad

Co-authored-by: Claude <noreply@anthropic.com>
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

1 participant