Skip to content

[finding] the dispatcher gate's resolver anchors on const, so a let-held error code is reduced by nobody even when every branch is a literal #10918

Description

@claude

Found while closing #10897 (teaching check-error-code-casing to read the
our-default slot in a code local's initializer). Filed rather than absorbed:
it is the OTHER gate's resolver, it is a behaviour change to a deliberate bound,
and #10897 was explicitly scoped to the recognizer.

The shape

declaredInitializers in scripts/check-dispatcher-error-vocabulary.mjs
anchors its head regex on const alone:

const head = new RegExp(
  `\\b${exported ? 'export\\s+' : '(?:export\\s+)?'}const\\s+${name}\\s*(?::[^=;\\n]+)?=\\s*`,
  'g',
);

So a code held in a let reduces to nothing. resolveConstant answers null,
the codehelper branch reads that as "unreducible" and takes the documented
runtime-value bound — no site and no unresolved — even when the value is a
plain string literal sitting right there in the source.

That is not the bound #9568 wrote. Its words: "a value a source scan cannot
evaluate is out of reach; a value spelled out in every branch is not, whichever
shape it arrives through."
A let initialized to a literal is spelled out. It
is out of reach only because of how the anchor is spelled.

Measured, with a positive control on each side

Driving deriveSites and findViolations on origin/main at 58563be069,
one fixture per row, each a complete export function that stamps err.code = code and throws:

shape check:dispatcher-error-vocabulary check:error-code-casing
let code = 'REASSIGN_FALLBACK'; if (x) code = 'REASSIGN_OTHER'; no site, no unresolved 0
let code = 'REASSIGN_FALLBACK'; if (x) code = p?.code || 'reassigned_lower_failed'; no site, no unresolved 0
control: const code = 'local_direct_failed' 1 site (assignconst) 0
control: const code = flag ? 'tern_lower_a' : 'tern_lower_b' 2 sites (assignconst) 0
control: const code = p?.code || 'reassigned_lower_failed' no site 1 (local-fallback, after #10897)

Rows 3 and 4 are the control that makes rows 1 and 2 readable rather than a
broken probe: the identical shape spelled const does reduce, lowercase
included, and does produce sites. Row 5 is the casing-gate control on the other
side. Row 1 is the sharpest one — both of its values are literals, both
unregistered, and no gate says a word.

Two separable halves

  1. let / var declarations. The cheap half, and the one that matches
    [finding] check:dispatcher-error-vocabulary still cannot see a code held in a local ternary — sys-metadata-repository's live 403 NOT_CREATABLE / NOT_OVERRIDABLE #9568's stated intent: widen the declaredInitializers anchor. The reason to
    be careful rather than quick is that a let can be reassigned, so the set
    of values a let holds is not simply its initializer — reducing it to the
    initializer alone would report a value the program may not stamp, which is
    the same wrongness the ALL-OR-NOTHING rule ([finding] check:dispatcher-error-vocabulary still cannot see a code held in a local ternary — sys-metadata-repository's live 403 NOT_CREATABLE / NOT_OVERRIDABLE #9568) exists to refuse. The
    honest widening probably reduces a let only when the file contains no
    reassignment of it, and reports it as unresolved when it does — never
    silently, which is the bound this gate holds itself to everywhere else.

  2. Bare reassignment (code = p?.code || 'lit', no declarator) — row 2.
    Harder: no declaration to anchor on, and a textual scan has no scopes. Worth
    recording, not necessarily worth closing. Note [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's new local-fallback
    recognizer deliberately requires a declarator, so the casing gate does not
    reach this one either.

Extent today: not swept

I did not sweep the tree for live victims — that measurement belongs with
whoever takes this, since the right population depends on which half is being
closed. What is known: 47 code-named local declarations exist under
packages/** (measured for #10897's blast radius), and at least one of them is
a let (packages/metadata-protocol/src/protocol.ts:6551,
let code: unknown | null =). Whether any holds an unregistered literal is the
open question.

Search note

The issue-search endpoint is unavailable from an agent seat here ("sessions are
bound to their configured repositories"), so the duplicate check behind this
card is a title grep over the 100 most recently created open issues, not a full
search. Worth a second look before anyone starts.

Refs

#10897 / PR #10914 (where this was found) - #9568 (the ALL-OR-NOTHING reduction
and the bound this contradicts) - #9460 (the local-variable bound it refines) -
ADR-0112 D1

Generated by Claude Code


Generated by Claude Code

Activity

  1. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage (first-touch): promoted finding → pm:queue + domain:devx; rationale: measured with positive controls on both sides, and half 1 restores #9568's own stated bound ("a value spelled out in every branch is not out of reach") rather than widening it. Scope: half 1 only — widen declaredInitializers to let/var, reducing a let only when the file contains no reassignment of it, and reporting unresolved (never silent) when it does. Half 2 (bare reassignment, no declarator) stays recorded here, not queued — reopen it as its own card if a live victim is found. The extent sweep (47 code-named locals, which hold unregistered literals) belongs to the executing dev and its result goes in the PR body. Dedupe re-checked at triage: #10897 (casing-gate recognizer, in-flight) is the sibling, not a duplicate — different gate, and this card's rows 1–2 are invisible to both gates today. Type Task. Serial note: same script family as in-flight #10897/PR #10914 — serial after it lands. Size/model suggestion: M, mode:subagent, model: opus.


    Generated by Claude Code

  2. self-assigned this
    on Aug 23, 2026
  3. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    Claim: PM loop round R3
    Session: session_015ahemw8RcTgqtxrj15PEZx
    Branch: claude/issue-10918-let-held-error-code
    Worktree: objectstack-issue-10918
    Domain: domain:devx
    File surface: scripts/check-dispatcher-error-vocabulary.mjs + its self-test. ⛔ Not scripts/check-error-code-casing.mjs — that is the other gate and #10897 already scoped it (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (node scripts/pm/dispatch-gates.mjs --tier scripts/check-dispatcher-error-vocabulary.mjs run this round: "no path-derived mandate" ⇒ PM judgement. Judgement tier because half 1's correctness hinges on a reassignment-visibility question, not on the regex)
    Clause-②: no — a repo-internal gate's resolver. No schema, no accept/reject contract behaviour, no public surface.
    Serial constraints cleared: disjoint from everything live. #10019 (dispatched this round) → scripts/check-cross-package-test-inputs.mjs + possibly a new scripts/pm/ helper — different file. PR #11425 (awaiting human merge) → .claude/** + AGENTS.md. No open PR touches this gate.


    Premise re-measured this round at origin/main

    declaredInitializers is live at :516 (the card quotes the regex but not a line; the file is 1874 lines). ⛔ Re-derive the anchor's exact position — do not trust either the card's excerpt or this line number.

    ⚠️ The trap is named in the card and it is the whole difficulty

    Half 1 is not "widen the regex to accept let". The card is explicit about why that would be wrong:

    a let can be reassigned, so the set of values a let holds is not simply its initializer — reducing it to the initializer alone would report a value the program may not stamp, which is the same wrongness the ALL-OR-NOTHING rule (#9568) exists to refuse.

    ⇒ The honest widening reduces a let only when the file contains no reassignment of it, and reports unresolved when it does — never silently. ⛔ A patch that reduces every let to its initializer trades one silent wrongness for another and will be rejected on review.

    Scope

    ⭐ The card's fixture table already carries positive controls on both sides (rows 3–5). Keep that shape: a widening whose test does not also pin the const behaviour it must not disturb proves only half of what it claims.


    Generated by Claude Code

  4. claude commented on Aug 23, 2026

    @claude
    ContributorAuthor
    {
      "issue": 10918,
      "status": "done",
      "branch": "claude/issue-10918-let-held-error-code",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11445",
      "premise_still_valid": true,
      "head_sha": "6b67f12c",
      "base": "a41069ba379cd9fdc48d1a23fd4c366d0ebe068e",
      "files_changed": [
        "scripts/check-dispatcher-error-vocabulary.mjs"
      ],
      "line_budget": {
        "skills_touched": false,
        "diff": "+325 / -5, one file",
        "note": "No `skills/**` path in the diff, so no published-skill budget applies."
      },
      "summary": "Widened `declaredInitializers`' anchor from `const` to `const|let|var`, with the reassignment guard the card demands. Three states, all pinned: a `let`/`var` the file never reassigns reduces exactly as the `const` spelling does (SITES); one the file does reassign is NOT reduced and, when an initializer reduces to literals, is REPORTED as `unresolved` (reason `reassigned`); a mutable local with no literal anywhere is untouched and stays the #9460 runtime-value bound (silent). The third conjunct is load-bearing and was chosen from a measurement, not from the card: `unresolved` findings are pushed unconditionally by `reconcile` and have NO discharge route in the declaration table, so a rule of `let + reassignment => unresolved` would have turned `packages/metadata-protocol/src/protocol.ts` (`let code: unknown | null = null`) into an undischargeable red on main. Half 2 (bare reassignment, no declarator) is NOT built; it is recorded in the gate header's published bounds as unowned by both gates.",
      "tests": "All at head 6b67f12c. (1) `node scripts/check-dispatcher-error-vocabulary.mjs --self-test` => its own verdict line: 'check-dispatcher-error-vocabulary --self-test: 8 shapes + 143 assertions OK (vocabulary + #9098 door typing)', exit 0 (captured to a file before tail, not through a pipe). Baseline MEASURED, not recalled: origin/main's copy of the same script run as `--self-test` in this same worktree prints '8 shapes + 102 assertions OK', so this adds 41 assertions. (2) Full gate `node scripts/check-dispatcher-error-vocabulary.mjs` => 'check-dispatcher-error-vocabulary: OK - 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846)', exit 0. (3) REVERSE VERIFICATION, direction predicted before running = self-test turns RED and every `const`-path control stays green; observed exactly that. Ablation reverted the anchor ALONE (`}(?:const|let|var)\\s+${name}` -> `}const\\s+${name}`) on the committed tree; mutation proven on disk by anchored grep counts on both spellings, not by an editor exit code: widened anchor 1 -> 0, const-only anchor 0 -> 1, `git diff --stat` 1 file / 1 insertion / 1 deletion. No build/dist leg exists (plain .mjs, executed from source, no `exports` indirection). 13 assertions failed: the 7 `let`/`var`/`export let` reduction pins, the 2 reassigned-report pins ('a reassigned `let` holding a literal was not reported: []'), the 2 shadowed-declaration pins, `reassignedMutableLiteral`, and `declaredInitializers did not read a `let` initializer`. ZERO control assertions failed - the `const` rows, the #9460 runtime-value rows and the reassignment-detector hazard pins were all green under ablation, which is what makes the 13 readable. RESTORE LEG ran via `trap ... EXIT INT TERM`, printed RESTORED, and was verified independently: `git status --porcelain` empty and the self-test back to 143 OK. (4) Gate floor, all exit 0, each read from its own printed verdict line - see `gates`.",
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; script took its own changeset from the merge base). stderr: \"gate list derived from the tree of 'objectstack-ai/objectstack' at commit 6b67f12c\"; change set = 1 path, three-dot semantics vs merge base a41069ba3. 139 families discovered across 26 workflow files; 7 matched.",
        "run_locally_green": [
          "pnpm check:cross-package-test-inputs -> 'All 104 self-test cases passed.' / 'OK: 14 package(s) read outside themselves, all declared'",
          "pnpm check:dispatcher-error-vocabulary -> 'OK - 21 unregistered code-stamping site(s), all classified'",
          "pnpm check:entry-guard -> 'check:entry-guard: 138 scripts/ file(s) - every entry guard goes through invoked-as.mjs'",
          "pnpm check:parse-guard -> exit 0",
          "pnpm check:pnpm-filter-targets -> '40 assertions' self-test + '120/148 --filter occurrence(s) ... resolve'",
          "node scripts/check-ci-filter-parity.mjs -> 'OK: all 89 declared cross-package glob(s) (77 unique) are covered'",
          "node scripts/check-cross-package-test-inputs.mjs -> 'OK: 14 package(s) read outside themselves, all declared'",
          "node scripts/check-nul-bytes.mjs -> 'OK (scanned 6418 text file(s) ... no raw ASCII control bytes)' (not derived; run because the diff edits a file)"
        ],
        "not_run_locally": "None of the derived floor. The 5 changeset-triggered families do not apply: `skip-changeset` applied to PR #11445 instead (repo-internal CI gate, releases nothing) via the additive POST endpoint, read back after the labeler settled - see `deviations`.",
        "note": "`node scripts/check-ci-filter-parity.mjs` first exited 1 with ERR_MODULE_NOT_FOUND 'yaml' - the worktree had no node_modules yet, not a code failure. Ran `pnpm install --frozen-lockfile --ignore-scripts` through `scripts/pm/os-verify-lock.sh -c` ('VERDICT command-exit 0 - held the lock 17s - waited 0s') and it then exited 0."
      },
      "fixture_table_rerun": {
        "measured_at": "origin/main a41069ba379cd9fdc48d1a23fd4c366d0ebe068e (card measured at 58563be069)",
        "harness": "deriveSites() + findViolations() driven per fixture, one complete `export function` each, stamping `err.code = code` and throwing - the card's own shape",
        "verdict": "all five rows reproduce EXACTLY as the card recorded them. PM assumption confirmed.",
        "rows": [
          {
            "row": 1,
            "shape": "let code = 'REASSIGN_FALLBACK'; if (x) code = 'REASSIGN_OTHER';",
            "before": {
              "sites": [],
              "unresolved": [],
              "casing": []
            },
            "after": {
              "sites": [],
              "unresolved": [
                "code/assignconst/reassigned"
              ],
              "casing": []
            }
          },
          {
            "row": 2,
            "shape": "let code = 'REASSIGN_FALLBACK'; if (x) code = p?.code || 'reassigned_lower_failed';",
            "before": {
              "sites": [],
              "unresolved": [],
              "casing": []
            },
            "after": {
              "sites": [],
              "unresolved": [
                "code/assignconst/reassigned"
              ],
              "casing": []
            }
          },
          {
            "row": 3,
            "shape": "control: const code = 'local_direct_failed'",
            "before": {
              "sites": [
                "local_direct_failed(assignconst)"
              ],
              "unresolved": [],
              "casing": []
            },
            "after": {
              "sites": [
                "local_direct_failed(assignconst)"
              ],
              "unresolved": [],
              "casing": []
            },
            "control": "UNCHANGED"
          },
          {
            "row": 4,
            "shape": "control: const code = flag ? 'tern_lower_a' : 'tern_lower_b'",
            "before": {
              "sites": [
                "tern_lower_a(assignconst)",
                "tern_lower_b(assignconst)"
              ],
              "unresolved": [],
              "casing": []
            },
            "after": {
              "sites": [
                "tern_lower_a(assignconst)",
                "tern_lower_b(assignconst)"
              ],
              "unresolved": [],
              "casing": []
            },
            "control": "UNCHANGED"
          },
          {
            "row": 5,
            "shape": "control: const code = p?.code || 'reassigned_lower_failed' (casing-gate control)",
            "before": {
              "sites": [],
              "unresolved": [],
              "casing": [
                "reassigned_lower_failed(local-fallback)"
              ]
            },
            "after": {
              "sites": [],
              "unresolved": [],
              "casing": [
                "reassigned_lower_failed(local-fallback)"
              ]
            },
            "control": "UNCHANGED"
          }
        ]
      },
      "half1_extent_measurement": {
        "question": "Does any `let`/`var`-held error code under packages/** hold an UNREGISTERED LITERAL - i.e. is the gap live or latent?",
        "answer": "LATENT. Zero live victims.",
        "let_var_code_named_declarations_under_packages": 6,
        "of_those_actually_stamped_via_dot_code_equals_same_ident": 1,
        "the_one": "packages/metadata-protocol/src/protocol.ts:6753 - `let code: unknown | null = null;` - 3 declarations of `code` in the file, reassigned=true. Initializer is `null`, not a literal, so it reduces to nothing in any design and falls in state 3 (silent). Reports nothing before or after.",
        "the_other_five_never_reach_a_stamp": [
          "packages/cli/src/commands/meta/resync.ts:122 let exitCode = 0",
          "packages/cli/src/commands/migrate/meta.ts:550 let exitCode = 0",
          "packages/plugins/plugin-email/src/email-service.ts:625 let encodedAttachments",
          "packages/plugins/plugin-hono-server/src/adapter.ts:408 let streamEncoder",
          "packages/spec/scripts/lib/escape-mdx.ts:65 let inCode = false"
        ],
        "card_line_drift": "The card cites protocol.ts:6551; it is at :6753 on a41069ba. File confirmed, line stale.",
        "card_47_figure": "The card's 47 is `code`-named LOCAL declarations of all kinds (measured for #10897); the `let`/`var` subset of code-ish names is 6.",
        "tree_wide_before_after": {
          "scan": "the gate's own walkSources set - 1913 non-test files under packages/, 292 registered codes",
          "sites": "21 -> 21, sets IDENTICAL (0 lost, 0 gained)",
          "unresolved": "0 -> 0, sets IDENTICAL",
          "zone1_rule4": "No currently-reported site stops being reported. Verified as a set difference, not as a count."
        }
      },
      "deviations": [
        "PM assumption 1 (`declaredInitializers` at :516 in a 1874-line file) RE-DERIVED and confirmed at a41069ba: `516:export function declaredInitializers(...)`, `wc -l` 1874.",
        "PM assumption 2 (the card's fixture table still reproduces) RE-RUN and confirmed - all five rows, before touching anything. See `fixture_table_rerun`.",
        "PM assumption 3 (does reporting `unresolved` have consequences?) MEASURED and it does, and the answer changed the design. `reconcile()` pushes one `unresolved-constant` finding per entry UNCONDITIONALLY, and `main()` exits 1 on any finding - so an `unresolved` is a RED with NO discharge route: unlike a site, it cannot be classified away in packages/runtime/src/dispatcher-error-vocabulary.ts, only fixed in the product source. Consequence: the naive reading of the ruling (`let` + reassignment => unresolved) would have turned protocol.ts:6753 red on main for a genuinely runtime-valued local, i.e. exactly the 'every domain field named `code` is a finding' pressure #9460/#9568 refuse. So the reportable class carries a THIRD conjunct - at least one written initializer must itself reduce to literals. That keeps the ruling's substance ('never silently': a code visibly spelled out is always reported) while leaving the runtime-value bound untouched. Measured, not assumed: unresolved is 0 -> 0 tree-wide.",
        "ONE behaviour change beyond the two classes the card names, declared because it is not a pure widening: a name declared BOTH `const` and `let` in one file (a shadowing declaration in an inner scope) previously reduced to the `const` alone and produced a site; it now answers null and is REPORTED as unresolved. That is the same refusal #9568 already pins for two `const`s that reduce differently - dropping the mutable declarator and reducing the rest would be the first-wins guess the two-drivers hazard rules out. Pinned by two new assertions. It costs nothing in this tree (site sets identical), and it reports MORE, not less, so it is not the Zone 1 rule 4 weakening.",
        "Zone 1 rule 1 (file surface) HELD: the diff is exactly `scripts/check-dispatcher-error-vocabulary.mjs` and its in-file self-test. `scripts/check-error-code-casing.mjs` was READ (to drive the row-5 control through its `findViolations`) and NOT modified.",
        "Zone 1 rule 3 (half 2 out of scope) HELD: not built. Recorded in the gate header's published bounds as unowned by both gates. See `open_questions` for the cheapness judgement the ruling asked for.",
        "No changeset written; `skip-changeset` applied to PR #11445 via the additive POST /issues/11445/labels endpoint (POST returned ['skip-changeset']) and READ BACK after the labeler settled - see `label_readback`.",
        "`pnpm install` was needed in the worktree for `check-ci-filter-parity.mjs` (imports `yaml`); run through scripts/pm/os-verify-lock.sh, 17s held, 0s waited.",
        "No narrowing of any check was needed - every derived family ran in full. No sleeps, no background watchers, no CI waiting (report delivered at draft-PR time per the standing clause)."
      ],
      "open_questions": [
        {
          "question": "Half 2 (bare reassignment with no declarator) - the ruling asked me to report rather than build if it turns out cheap. It is NOT cheap, and the reason is now measurable rather than speculative.",
          "options": [
            "A - leave half 2 unowned, as recorded in the gate header's bounds (what this PR does).",
            "B - read reassignment RHS values and union them with the initializer, closing rows 1-2 as SITES rather than as `unresolved`.",
            "C - file a fresh card for half 2 with a live victim attached, if one ever appears."
          ],
          "recommendation": "A, with C as the trigger. B is the only route that would turn rows 1-2 into sites, and it needs SCOPES: the union is only sound if every reassignment the scan sees rebinds the SAME binding, which a textual file-wide scan cannot know - the gate already refuses exactly this guess repo-wide (two-drivers) and in-file (two declarations). It would also have to reduce every RHS or reduce nothing (ALL-OR-NOTHING), so row 2 (`code = p?.code || 'lit'`) reduces to nothing anyway and stays unresolved under B too. Against that cost, the measured population is zero: not one `let`/`var`-held code in packages/** holds an unregistered literal today. Half 1 was worth closing because it is a one-token anchor plus a guard; half 2 is a scope analysis for a population of zero."
        }
      ],
      "out_of_scope_findings": [],
      "label_readback": "Read back after the size labeler ran: GET /issues/11445/labels => ['size/m', 'skip-changeset']. The additive POST survived; no group PUT overwrote it."
    }

    Generated by Claude Code

  5. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    ACCEPT — PR #11445 · reviewer of record: devx@objectstack (session_015ahemw8RcTgqtxrj15PEZx)

    Checklist passed. Fixes #10918, draft, base main, 1 file. CI by latest-run-per-name: 26 distinct, 0 bad.

    ⭐ The Zone-2 question I asked changed the design — which is the point of asking it

    I asked whether reporting unresolved has downstream consequences, saying I had not checked. It does, and the answer moved the shape of the fix:

    reconcile() pushes one unresolved-constant finding unconditionally, and main() exits 1 on any finding — so an unresolved is a RED with no discharge route: unlike a site, it cannot be classified away in packages/runtime/src/dispatcher-error-vocabulary.ts, only fixed in the product source.

    ⇒ The naive reading of the card's own ruling (let + reassignment ⇒ unresolved) would have turned packages/metadata-protocol/src/protocol.ts red on main for a genuinely runtime-valued local — "exactly the 'every domain field named code is a finding' pressure #9460/#9568 refuse."

    So the reportable class gained a third conjunct: at least one written initializer must itself reduce to literals. That preserves the ruling's substance — never silently, a code visibly spelled out is always reported — while leaving the runtime-value bound intact. Implemented as reassignedInFile (:619) and reassignedMutableLiteral (:652), both documented at the call site.

    ⚠️ Note what this means about the card: its half-1 prescription, applied literally, would have broken main. The dev found that by measuring the consequence rather than implementing the instruction.

    Zone 1's hardest rule verified the right way

    Rule 4 was "do not weaken the gate — if some currently-reported site stops being reported, that is a regression." Verified as a set difference, not a count:

    tree-wide (the gate's own walkSources: 1913 non-test files, 292 registered codes)
      sites      21 -> 21   sets IDENTICAL  (0 lost, 0 gained)
      unresolved  0 ->  0   sets IDENTICAL
    

    A count match would have been satisfiable by losing one site and gaining another. A set match is not. And this is independently gated: lint.yml:1267 runs pnpm check:dispatcher-error-vocabulary, so CI re-derives the same claim on the PR — the tree-wide reading does not rest on the dev's word.

    Reverse verification with the ablation isolated to one token

    Direction predicted before running. The ablation reverted the anchor alone ((?:const|let|var)\s+ → const\s+) on the committed tree, with mutation proven by anchored grep counts in both directions (widened 1→0, const-only 0→1) rather than an editor's exit code. Result: 13 assertions failed, ZERO control assertions failed — the const rows, the #9460 runtime-value rows, and the reassignment-detector hazard pins all stayed green. That zero is what makes the 13 readable. Restore ran via trap … EXIT INT TERM, verified by git status --porcelain empty and the self-test back to 143 OK.

    Self-test baseline measured rather than recalled: origin/main's copy prints 102 assertions in the same worktree, the branch prints 143 — a real +41, not a claimed one.

    My assumptions 1 and 2: both confirmed, one with drift

    The extent question is answered: LATENT, zero live victims

    6 let/var code-named declarations under packages/**; only 1 actually reaches a .code = stamp; its initializer is null, not a literal, so it reduces to nothing under any design and stays silent. The other five never reach a stamp (exitCode, encodedAttachments, streamEncoder, inCode).

    That is the difference between closing a live gap and closing a latent one, and it was worth measuring before the patch rather than after.

    One extra behaviour change — declared, not slipped in

    A name declared both const and let in one file (inner-scope shadowing) previously reduced to the const alone and produced a site; it now answers null and reports unresolved. Justified as the same refusal #9568 already pins for two consts that reduce differently — dropping the mutable declarator would be the first-wins guess the two-drivers hazard exists to rule out. It reports more, not less, so it is not a rule-4 weakening, and it costs nothing in this tree (site sets identical). Pinned by two new assertions and named in the deviations rather than left for review to notice.

    Half 2 correctly not built — and the refusal is now measured

    Ruling 3 said report rather than build if it turned out cheap. It is not cheap, and the reason is no longer speculative: closing rows 1–2 as sites requires scopes — the union of reassignment RHS values is sound only if every reassignment rebinds the same binding, which a textual file-wide scan cannot know, and which this gate already refuses repo-wide (two-drivers) and in-file (two declarations). Under ALL-OR-NOTHING, row 2 (code = p?.code || 'lit') reduces to nothing and stays unresolved even then. Against that: population zero.

    "Half 1 was worth closing because it is a one-token anchor plus a guard; half 2 is a scope analysis for a population of zero."

    I accept A with C as the trigger — leave half 2 unowned, recorded in the gate header's published bounds, and file a fresh card if a live victim ever appears. ⛔ Not carried as an open item on this card.

    Composition note

    +325/-5 is large for a one-token anchor. Measured: 163 of 325 added lines are comment/docblock — consistent with this file's existing density and with the repo's gate-script convention of documenting the bound at the call site. The rest is the guard plus 41 self-test assertions.

    skip-changeset is correct (repo-internal CI gate, releases nothing) and was read back after the size labeler settled: ['size/m','skip-changeset'].

    Landing

    scripts/** is not governed ⇒ normal queue path once CI settles. pm:dispatched stays until MERGED.


    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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions