Skip to content

Commit 23e86a2

Browse files
committed
fix(devx): read the our-default slot in a code local's initializer too
`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
1 parent 58563be commit 23e86a2

2 files changed

Lines changed: 112 additions & 15 deletions

File tree

scripts/check-dispatcher-error-vocabulary.mjs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,17 @@
8989
* is WHICH gate reports it. `check:error-code-casing` owns the lowercase
9090
* sweep, and every pattern it has needs a QUOTED lowercase literal beside
9191
* the token `code` — `code: 'x'`, `.code = 'x'`, `code === 'x'`,
92-
* `code?: 'x' | 'y'`, and [#10760] `code: parsed?.code || 'x'` /
92+
* `code?: 'x' | 'y'`, [#10760] `code: parsed?.code || 'x'` /
9393
* `.code = e?.code ?? 'x'` — the OUR-DEFAULT slot of a `||`/`??`
94-
* fallback chain, that gate's `fallback` pattern. It still captures only
95-
* a string literal, and a literal in our source is by construction the
96-
* default WE author. That fifth one is a position this gate has NO shape
97-
* for rather than one it hands over: an intervening expression leaves
98-
* `objlit` and `assign` with no quote to match.
94+
* fallback chain, that gate's `fallback` pattern — and [#10897] the same
95+
* our-default slot one indirection earlier, in a `code`-named local's
96+
* initializer (`const code = parsed?.code || 'x'`, typed or not), that
97+
* gate's `local-fallback` pattern. Both still capture only a string
98+
* literal, and a literal in our source is by construction the default WE
99+
* author. Those last two are positions this gate has NO shape for rather
100+
* than ones it hands over: an intervening expression leaves `objlit` and
101+
* `assign` with no quote to match, and the local's chain is declined here
102+
* by the ALL-OR-NOTHING rule below.
99103
* In those positions (`objlit`, `assign`) the
100104
* delegation is real: that gate reads the same characters and carries the
101105
* D6/D6b/D6c discrimination — field-addressed catalogs, persisted audit
@@ -143,12 +147,20 @@
143147
* the shape is DELEGATED, not dropped. That is the position the two live
144148
* SSO codes in `register-sso-provider.ts` shipped through. In a LOCAL'S
145149
* INITIALIZER (`const code = parsed?.code || 'lit'; err.code = code`) it
146-
* is neither: the ALL-OR-NOTHING rule above declines it here, and that
147-
* gate's `fallback` pattern anchors on `code:`/`code?:`/`.code =`, so a
148-
* `const code =` is out of ITS reach too. That one is still owned by
149-
* NOBODY — deliberately on this side, filed as #10897 for the other.
150-
* Stated so the next reader does not re-derive the hole and close it in
151-
* the wrong gate.
150+
* was owned by NOBODY: the ALL-OR-NOTHING rule above declines it here, and
151+
* that gate's `fallback` pattern anchored on `code:`/`code?:`/`.code =`,
152+
* so a `const code =` was out of ITS reach too. [#10897] closed the second
153+
* half — that gate now carries a `local-fallback` pattern for exactly this
154+
* position — so the local initializer is DELEGATED as well, on the same
155+
* reasoning: the capture is a literal, and where we WRITE the chain does
156+
* not change whose default it is. This side is unchanged and stays so; the
157+
* ALL-OR-NOTHING bound is the deliberate half and closing the hole there
158+
* would have moved the defect rather than fixed it.
159+
* Not symmetric, and the asymmetry is the point: a local whose initializer
160+
* is ALL literals (a bare literal, a ternary, a chain) IS reducible, so it
161+
* stays THIS gate's site under `assignconst`, lowercase included, and that
162+
* gate's pattern deliberately does not reach it. Measured on both sides,
163+
* not inferred — one literal, one reporter.
152164
* - A constant this gate cannot resolve is REPORTED as unresolved, never
153165
* dropped: a deriver that goes quietly blind is the same failure one layer
154166
* down. [#9223] A constant imported from a WORKSPACE package is resolved
@@ -824,11 +836,13 @@ export function deriveSites({ registered, files, readFile, packageDirs = new Map
824836
*
825837
* `check:error-code-casing` owns the lowercase sweep, and every pattern it
826838
* has requires a QUOTED lowercase literal sitting next to the token `code`:
827-
* `code: 'x'`, `.code = 'x'`, `code === 'x'`, `code?: 'x' | 'y'`, and
839+
* `code: 'x'`, `.code = 'x'`, `code === 'x'`, `code?: 'x' | 'y'`,
828840
* [#10760] `code: parsed?.code || 'x'` / `.code = e?.code ?? 'x'`, the
829841
* OUR-DEFAULT slot of a `||`/`??` fallback chain (that gate's `fallback`
830-
* pattern; it still captures only a literal, which in our source is the
831-
* default we author). Where this
842+
* pattern), and [#10897] the same slot in a `code`-named local's
843+
* initializer, `const code = parsed?.code || 'x'` (its `local-fallback`
844+
* pattern). Each still captures only a literal, which in our source is the
845+
* default we author. Where this
832846
* gate finds a lowercase code in one of those same positions — `objlit`,
833847
* `assign` — the delegation is real: that gate sees the identical text, and
834848
* it carries the D6/D6b/D6c discrimination (field-addressed catalogs,

scripts/check-error-code-casing.mjs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,58 @@ const CODE_POSITION_PATTERNS = [
142142
name: 'fallback',
143143
re: /(?:\bcode\s*\??\s*:|\.code\s*=(?!=))\s*(?![`'"])[\w$.?!()[\]|&\s]{0,80}?(?:\|\||\?\?)\s*'([a-z][a-z0-9_]*)'/g,
144144
},
145+
// [#10897] The SAME our-default slot, one indirection EARLIER — in the
146+
// initializer of a `code`-named local, rather than at the stamp site:
147+
//
148+
// const code = parsed?.code || 'lower_thing'; err.code = code;
149+
// const code: string = e?.code ?? 'lower_thing';
150+
//
151+
// The pattern above anchors on the POSITION token (`code:` / `code?:` /
152+
// `.code =`), so it reaches a fallback chain only where the chain sits AT
153+
// the stamp site. `const code =` is neither spelling, and a TYPE ANNOTATION
154+
// does not rescue it: `const code: string = …` does match `code:`, but then
155+
// the gap has to cross an `=`, which that character class refuses on purpose
156+
// (refusing `=` is part of what stops a match leaping out of one property's
157+
// value into a neighbour's). So both spellings matched nothing.
158+
//
159+
// And nothing else saw them either — this was a hole between two gates, not
160+
// a hand-off. `check:dispatcher-error-vocabulary` does reach the local:
161+
// `err.code = code` is its `codehelper`/`assignconst` shape, and its
162+
// `resolveConstant` reduces a local whose initializer is a ternary or a
163+
// chain OF LITERALS (#9568). But that reduction is ALL-OR-NOTHING by design:
164+
// one runtime limb (`parsed?.code`) reduces the whole chain to nothing,
165+
// because half an expression's values is a finding wrong in both directions
166+
// at once. That bound is deliberate, correct, and unchanged by this pattern.
167+
//
168+
// Which leaves the literal half to this gate, on exactly the reasoning
169+
// #10760 published for the stamp site: the capture is still only ever a
170+
// STRING LITERAL, and a literal in our source is by construction ours — the
171+
// default WE author, which is the operand ADR-0112 D1 governs. A vendor code
172+
// passing through is a RUNTIME value with no literal to capture. WHERE we
173+
// write the chain does not change whose default it is; the asymmetry between
174+
// the two positions was an artifact of where the recognizer anchored, not a
175+
// decision anyone took.
176+
//
177+
// The delegation runs the OTHER way for a local this gate must NOT touch: an
178+
// all-literal initializer (`const code = 'lower_thing'`, a ternary of
179+
// literals, a chain of literals) IS reducible, so the dispatcher gate emits
180+
// a site for it under `assignconst` — measured, all three cases, lowercase
181+
// included. The lookahead `(?!['"`])` and the gap class (which admits no
182+
// quote at all) together keep this pattern off the head of such a chain, so
183+
// the two gates never both report one literal.
184+
//
185+
// The annotation gap is `[^=;\n]`, the spelling `check-dispatcher-error-
186+
// vocabulary`'s own `classfield` uses for this same job, so it cannot
187+
// swallow the `=` it is meant to stop before. Everything after the `=` is
188+
// the pattern above's gap class and tail verbatim: same operand alphabet,
189+
// same 80-char runaway bound, same lowercase value space — an uppercase
190+
// default stays out of it, and every filter in `findViolations` (D6
191+
// field-addressed, NOT_CODES, `adr0112-ok:`) still applies. All pinned in
192+
// --self-test, in both directions.
193+
{
194+
name: 'local-fallback',
195+
re: /\b(?:const|let|var)\s+code\s*(?::[^=;\n]+)?=\s*(?![`'"])[\w$.?!()[\]|&\s]{0,80}?(?:\|\||\?\?)\s*'([a-z][a-z0-9_]*)'/g,
196+
},
145197
];
146198

147199
/**
@@ -290,6 +342,37 @@ function selfTest() {
290342
0,
291343
'the gap is bounded: a runaway expression is a declared miss, not a leap',
292344
],
345+
346+
// [#10897] The same our-default slot in a LOCAL'S INITIALIZER. Pinned as a
347+
// pair with the stamp-site spellings for the same reason those were pinned
348+
// as a pair with the direct one: a recognizer that reached the new position
349+
// by breaking an older one would pass a self-test that only pinned the new
350+
// position. Every case below carries the error-shaped neighbour the filters
351+
// require, positive AND negative — a zero that comes from a MISSING
352+
// neighbour would be a broken probe testing nothing about the recognizer.
353+
[`const code = parsed?.code || 'local_lower_failed'; const err = new Error(msg); err.code = code;`, 1, 'our default in an untyped local initializer'],
354+
[`const code: string = parsed?.code ?? 'typed_lower_failed'; const err = new Error(msg); err.code = code;`, 1, 'our default in a TYPED local initializer (the annotation is what puts an = in the gap)'],
355+
[`let code = e?.code || 'let_lower_failed'; const err = new Error(msg); err.code = code;`, 1, 'let, not only const'],
356+
[`const code = a?.code || b?.code || 'local_chained_failed'; const err = new Error(msg); err.code = code;`, 1, 'local initializer, fallback at the end of a chain'],
357+
[`error: { code: parsed?.code || 'stamp_still_seen_failed', message }`, 1, 'stamp-site objlit still matches (the pair half that must not regress)'],
358+
[`const err = new Error(msg); (err as any).code = e?.code || 'assign_still_seen_failed';`, 1, 'stamp-site assignment still matches (the pair half that must not regress)'],
359+
360+
// Reject side for the local position.
361+
[`const code = parsed?.code || 'LOCAL_UPPER_FAILED'; const err = new Error(msg); err.code = code;`, 0, 'a SCREAMING default in a local is compliant'],
362+
[`const code = parsed?.code; const err = new Error(msg); err.code = code;`, 0, 'a vendor code through a local has no literal to capture, before or after this widening'],
363+
[`const codeName = parsed?.code || 'not_the_code_local'; throw new Error(codeName);`, 0, 'a local whose name merely STARTS with code is not the code position'],
364+
[`const message = parsed?.message || 'lower_thing'; throw new Error(message);`, 0, "a NEIGHBOUR's local fallback is not the code's value"],
365+
[`const code = 'local_direct_failed'; const err = new Error(msg); err.code = code;`, 0, 'an all-literal local REDUCES (#9568), so its site is the dispatcher gate\'s, not ours'],
366+
[`const code = 'chain_lower_a' || 'chain_lower_b'; const err = new Error(msg); err.code = code;`, 0, 'an all-literal chain reduces too, and stays the dispatcher gate\'s'],
367+
[`const code: Foo = fallbackFor(e); const other = x || 'leapt_failed'; throw new Error(msg);`, 0, 'the annotation gap refuses ; and =, so a match cannot leap into the NEXT statement'],
368+
[`const code = row.code || 'ok'; const err = new Error(msg); err.code = code;`, 0, 'NOT_CODES still applies through the local shape'],
369+
[`issues.push({ field: 'email' }); const code = e?.code || 'invalid_email'; throw new Error(msg);`, 0, 'D6 still wins through the local shape'],
370+
[`const code = e?.code || 'local_optout_failed'; throw new Error(msg); // adr0112-ok: D6b persisted audit column`, 0, 'opt-out still applies through the local shape'],
371+
[
372+
`const code = a.b.c.${'d'.repeat(90)} || 'local_far_away_failed'; const err = new Error(msg); err.code = code;`,
373+
0,
374+
'the local gap is bounded too: a runaway expression is a declared miss, not a leap',
375+
],
293376
];
294377
let failed = 0;
295378
for (const [src, want, label] of cases) {

0 commit comments

Comments
 (0)