Skip to content

[finding] check-error-code-casing's ||/?? fallback recognizer anchors on the STAMP SITE only, so our authored default in a const code = … initializer is still seen by no gate #10897

Closed

Description

@os-zhuang

Found while executing #10762 (documenting the delegation map after the #10658 / PR #10760 widening). Filed rather than absorbed: it is a different gate, a behaviour question rather than a comment one, and #10762 is comment-only by construction.

The shape

PR #10760 added a fifth recognizer to scripts/check-error-code-casing.mjs for the OUR-DEFAULT slot of a ||/?? chain. Its anchor is the position token:

re: /(?:\bcode\s*\??\s*:|\.code\s*=(?!=))\s*(?['"`])[\w$.?!()[\]|&\s]{0,80}?(?:\|\||\?\?)\s*'([a-z][a-z0-9_]*)'/g

So it reaches the chain when the chain sits at the stamp site — { code: parsed?.code || 'lit' }, err.code = e?.code ?? 'lit'. It does not reach the identical chain one indirection earlier, in a local's initializer:

const code = parsed?.code || 'lower_thing';   // ← our authored default, same as above
const err = new Error(msg);
err.code = code;
throw err;

const code = is neither code:/code?: nor .code =, so no pattern matches. A typed local does not help either: const code: string = … puts an = in the gap class, which the class refuses.

Measured, with a live positive control on both sides

Driving both gates' own exported entry points (findViolations, deriveSites) on origin/main at 9faa9bc51d:

shape check:dispatcher-error-vocabulary check:error-code-casing
stamp-site { code: p?.code || 'stamp_lower_failed' } no site 1 (fallback)
stamp-site err.code = p?.code || 'stamp_lower_failed' no site 1 (fallback)
local-init const code = p?.code || 'local_lower_failed' no site, no unresolved 0
local-init const code = p?.code || 'LOCAL_UPPER_FAILED' no site, no unresolved 0 (out of its value space)
control: local-init const code = flag ? 'TERN_A' : 'TERN_B' 2 sites (assignconst) n/a

The last row is the control that makes the zeros readable: the dispatcher gate does reduce and report a local initializer when every limb is a literal (#9568), so its silence on row 3 is the deliberate ALL-OR-NOTHING bound, not a broken probe. The casing-gate rows 1–2 are the matching control on the other side.

Row 3 is therefore reported by neither gate — the same "two gates, each assuming the other" configuration that check-dispatcher-error-vocabulary's own header records for owd_widening_forbidden.

Why it is arguably the casing gate's, not the dispatcher gate's

The dispatcher gate's silence is correct and should not change: under #9568 a chain with one runtime limb reduces to nothing, because harvesting the literal half would report a code the program may never stamp while staying silent about the limb it cannot see.

But that is exactly the epistemic position PR #10760 already ruled on for the stamp site, and ruled the other way, in its own words: the pattern "still only ever captures a STRING LITERAL, and a literal in our source is by construction ours … it reaches only the default WE author, which is exactly the operand ADR-0112 D1 governs." const code = parsed?.code || 'lower_thing' is our authored default by that same reasoning. The asymmetry between the two positions is an artifact of where the recognizer anchors, not a decision anyone took.

Extent today: 0

Swept 1885 non-test .ts/.tsx files under packages/ (comments masked with the gates' own maskComments) for a code-named local whose initializer is a runtime-limbed ||/?? chain with a lowercase literal default: 0 hits. Like #10762, the drift understates coverage and has no live victim today — recorded so the next reader does not have to re-derive it.

Refs

#10762 (the delegation map, where this boundary is now written down) · #10658 / PR #10760 (the widening) · #9568 (the ALL-OR-NOTHING reduction) · ADR-0112 D1

Generated by Claude Code

Activity

  1. claude commented on Aug 21, 2026

    @claude
    Contributor

    PM claim — dispatching now.

    • session: c970724d-303c-5614-9d20-a3f92205cfad
    • branch: claude/issue-10897-casing-local-initializer
    • seat: domain:devx

    Dispatched as the natural successor to #10762, which merged at 16:47Z (PR #10898). That card documented the delegation and recorded that a runtime-limbed chain is "owned by check:error-code-casing rather than by nobody" — and its own agent measured, while writing that sentence, that the statement is true at the stamp site and false in a local's initializer, where the chain is owned by nobody. It refused to write the card's unqualified version for exactly that reason and recorded both branches instead. This card is the hole that survived.

    So the two land in the right order: the map now says what is true, and this closes the gap the map exposes.

    ⚠️ Coordination: card #10840 is in flight sweeping scripts/ gate scripts for bare-root population literals. Its target is the population constant region; this card's target is the recognizer. Different regions of a file it may or may not touch — the agent is told to re-check immediately before pushing and to say what it found.

    ⛔ The card is explicit that the dispatcher gate's silence is correct and must not change — under #9568 a chain with one runtime limb reduces to nothing on purpose. A PR that "fixes" this by loosening check-dispatcher-error-vocabulary has moved the defect rather than closed it, and would undo a deliberate, documented bound.

    Note the card's control row — const code = flag ? 'TERN_A' : 'TERN_B' producing 2 sites on the dispatcher side — is what makes the three zeros readable rather than a broken probe. Keep that discipline: any measurement in the PR needs its own positive control.

    If an earlier claim comment with a different session ID appears above this one, that claim wins and this one is void.


    Generated by Claude Code

  2. claude commented on Aug 21, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 10897,
      "status": "done",
      "branch": "claude/issue-10897-casing-local-initializer",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10914",
      "premise_still_valid": true,
      "summary": "TABLE, re-derived on origin/main 58563be069 by driving findViolations and deriveSites: every cell reproduces the card's measurement at 9faa9bc51d, nothing moved. stamp-site objlit and stamp-site assign = dispatcher 'no site, no unresolved' / casing 1 (fallback); local-init lowercase = no site, no unresolved / 0 BEFORE and 1 (local-fallback) AFTER; typed local (const code: string = ...) = same, 0 before / 1 after, so the typed spelling is confirmed broken and fixed; local-init UPPERCASE = 0 both sides, still out of the value space; CONTROL const code = flag ? 'TERN_A' : 'TERN_B' = 2 dispatcher sites (assignconst), which is what makes the zeros readable. I added three delegation rows the card did not have, and they set the boundary in the other direction: an ALL-literal initializer (bare literal 1 site, ternary 2 sites, all-literal chain 2 sites, lowercase included) IS reducible under #9568 and stays the dispatcher gate's, so the new recognizer deliberately does not reach it and one literal never gets two reporters. BLAST RADIUS across packages/**, 4393 files, comments masked with the gates' own maskComments: the new recognizer as shipped matches 0 sites. Two positive controls make that zero readable: the same anchor with an any-case literal matches 5, and any code-named local declaration matches 47. All 5 any-case hits judged individually and all are genuine authored defaults that are already compliant: 'USD' (a currency code, formula/template-engine.ts:75), 'UNKNOWN' (a Node errno fallback, metadata-fs/repository.ts:655), 'INTERNAL_ERROR' twice (plugin-auth admin-user-endpoints.ts:405 and set-initial-password.ts:87), 'IMPORT_ROW_FAILED' (rest/import-runner.ts:293, a file already in EXEMPT_FILES). So 0 new reds, and the casing gate's real-tree verdict is byte-identical before and after. DISPATCHER VERDICT DID NOT MOVE: 'check-dispatcher-error-vocabulary: OK - 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846).' is byte-identical before and after (diff clean), and its self-test is unchanged at 8 shapes + 102 assertions. The ALL-OR-NOTHING bound was not touched. The only edit in that file is comment-only: the three delegation enumerations #10762 had just corrected still recorded this position as owned by NOBODY and named #10897 as the open half, so leaving them would have recreated exactly the drift #10762 closed. That edit is named and evidenced in the PR body rather than slipped in. FIX: a sixth recognizer, local-fallback, anchoring on the declarator (const/let/var) with the annotation gap spelled [^=;\\n] (the spelling classfield already uses in the sibling gate) and the fifth pattern's gap class and tail verbatim. Self-test grew 29 -> 46 recognizer cases. COORDINATION, checked against all 11 open PRs immediately before pushing: #10906 (card #10840, the bare-root population sweep) does NOT touch either of my files - the warned-about overlap did not materialise. #10858 (the #10716 SSO rename) DOES touch scripts/check-error-code-casing.mjs but a different region, roughly 20 lines below mine: it empties KNOWN_LOWERCASE_CODES and parameterises partitionKnown. No textual conflict and no semantic one - this recognizer adds no registry entries, and after that rename the SSO defaults are SCREAMING and out of this pattern's value space.",
      "tests": "All at final commit 23e86a219b (merge base origin/main 58563be069). Gate set derived with `node scripts/pm/dispatch-gates.mjs` with no paths (it read 2 committed paths from the merge base itself), not from a hand-written list; all 7 matched families run, each exit code captured before any pipe and each verdict quoted from the gate's own printed line. pnpm check:error-code-casing EXIT=0 -> 'check-error-code-casing self-test: 46 recognizer case(s) + 4 registry case(s) pass.' and 'no unlisted lowercase error codes in 4393 scanned file(s) (ADR-0112).'  |  pnpm check:dispatcher-error-vocabulary EXIT=0 -> '--self-test: 8 shapes + 102 assertions OK' and 'OK - 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846).' (byte-identical to the pre-change run)  |  pnpm check:cross-package-test-inputs EXIT=0 -> 'OK: 13 package(s) read outside themselves, all declared...' (104 self-test cases)  |  pnpm check:entry-guard EXIT=0 -> '130 scripts/ file(s) - every entry guard goes through invoked-as.mjs' (52 self-test cases)  |  pnpm check:parse-guard EXIT=0 -> '129 scripts/ file(s) - every TypeScript parse goes through ts-parse.mjs' (46 self-test cases)  |  node scripts/check-ci-filter-parity.mjs EXIT=0 -> 'OK: all 82 declared cross-package glob(s) (71 unique) are covered'  |  node scripts/check-cross-package-test-inputs.mjs EXIT=0  |  node scripts/check-nul-bytes.mjs EXIT=0 -> 'no raw ASCII control bytes' (plus a direct grep -naP control-byte scan over both edited files: no hits). NEGATIVE CONTROL for #10853: `pnpm --filter @objectstack/definitely-not-a-package typecheck` EXIT=0 printing 'No projects matched the filters' - confirmed the zero-match-exits-0 trap; no verdict in this card rests on a --filter run. NEGATIVE-CASE CONTROL: all 10 reject-side self-test cases were checked as live probes - flipping the single property under test (codeName->code, message->code, UPPER->lower, all-literal->runtime-limbed, no-chain->chain, leap-blocked->direct, NOT_CODES 'ok'->a real code, D6 field->no field, opt-out->no reason, runaway->in bounds) takes each from 0 to 1. Every case, positive and negative, carries the error-shaped neighbour the filters require, so no zero comes from a missing neighbour instead of the recognizer - the first draft of these fixtures failed for exactly that reason and was rewritten. ABLATION, prediction written to a file BEFORE either run. No build or dist is involved (these gates are plain node scripts run from source), so there is no rebuild leg to report; each mutation is instead proven ON DISK by marker counts plus git diff --stat, never by an editing tool's exit code. LEG 1, delete the local-fallback recognizer: marker \"name: 'local-fallback'\" 1 -> 0, git diff --stat '4 deletions'. Predicted self-test red, exit 1, exactly 4 failures being the four positive local cases, with the stamp-site pair halves and every negative staying green. Observed exactly that. The real tree stayed GREEN under the mutation, which is why the real tree cannot be the control here and the fixture self-test has to be. LEG 2, admit a quote to the local-fallback gap class only (&\\s]{0,80}? -> &\\s']{0,80}?, markers 1/0 -> 0/1): predicted exit 1 with exactly 1 failure, \"an all-literal chain reduces too, and stays the dispatcher gate's\". Observed exactly that, proving the delegation boundary is enforced by the pattern and pinned by that case rather than merely asserted in a comment. RESTORE LEG after each: `git checkout claude/issue-10897-casing-local-initializer -- scripts/check-error-code-casing.mjs` then `git diff --quiet` clean (tree byte-identical), markers back to their pre-mutation values, self-test 46+4 green.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #10918: the dispatcher gate's declaredInitializers anchors on `const` alone, so a `let`-held error code is reduced by nobody even when every branch is a literal - measured 'no site, no unresolved' from both gates for `let code = 'REASSIGN_FALLBACK'; if (x) code = 'REASSIGN_OTHER'`, against a `const` control that produces 1 and 2 sites; the bare-reassignment shape (no declarator) recorded as the harder separable half. Unassigned, labels finding/tooling/domain:devx, no pm:queue."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions