Repository navigation
[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
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
-
let/vardeclarations. The cheap half, and the one that matches
[finding]check:dispatcher-error-vocabularystill cannot see a code held in a local ternary —sys-metadata-repository's live 403NOT_CREATABLE/NOT_OVERRIDABLE#9568's stated intent: widen thedeclaredInitializersanchor. The reason to
be careful rather than quick is that aletcan be reassigned, so the set
of values aletholds 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-vocabularystill cannot see a code held in a local ternary —sys-metadata-repository's live 403NOT_CREATABLE/NOT_OVERRIDABLE#9568) exists to refuse. The
honest widening probably reduces aletonly 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. -
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 aconst code = …initializer is still seen by no gate #10897's newlocal-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
huangyiirene commentedon Aug 21, 2026
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 — widendeclaredInitializerstolet/var, reducing aletonly 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 (47code-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
os-steve commentedon Aug 23, 2026
Claim: PM loop round R3
Session:
session_015ahemw8RcTgqtxrj15PEZxBranch:
claude/issue-10918-let-held-error-codeWorktree:
objectstack-issue-10918Domain:
domain:devxFile surface:
scripts/check-dispatcher-error-vocabulary.mjs+ its self-test. ⛔ Notscripts/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.mjsrun 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 newscripts/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/maindeclaredInitializersis 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.Half 1 is not "widen the regex to accept
let". The card is explicit about why that would be wrong:⇒ The honest widening reduces a
letonly when the file contains no reassignment of it, and reportsunresolvedwhen it does — never silently. ⛔ A patch that reduces everyletto its initializer trades one silent wrongness for another and will be rejected on review.Scope
let/vardeclarations) is in scope, with the reassignment guard above.const code = …initializer is still seen by no gate #10897'slocal-fallbackrecognizer deliberately requires a declarator. If you conclude half 2 is cheap after all, report it — do not build it.code-named local declarations exist underpackages/**, at least one is alet(packages/metadata-protocol/src/protocol.ts:6551). Whether any holds an unregistered literal is open — answer it.⭐ 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
constbehaviour it must not disturb proves only half of what it claims.Generated by Claude Code
claude commentedon Aug 23, 2026
{ "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
os-steve commentedon Aug 23, 2026
ACCEPT — PR #11445 · reviewer of record:
devx@objectstack(session_015ahemw8RcTgqtxrj15PEZx)Checklist passed.
Fixes #10918, draft, basemain, 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
unresolvedhas downstream consequences, saying I had not checked. It does, and the answer moved the shape of the fix:⇒ The naive reading of the card's own ruling (
let+ reassignment ⇒unresolved) would have turnedpackages/metadata-protocol/src/protocol.tsred onmainfor a genuinely runtime-valued local — "exactly the 'every domain field namedcodeis 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) andreassignedMutableLiteral(:652), both documented at the call site.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:
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:1267runspnpm 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 — theconstrows, the #9460 runtime-value rows, and the reassignment-detector hazard pins all stayed green. That zero is what makes the 13 readable. Restore ran viatrap … EXIT INT TERM, verified bygit status --porcelainempty 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
declaredInitializersat:516, file 1874 lines — re-derived, confirmed.a41069ba(card measured at58563be069). Re-run before touching anything, which is the only order in which that check means something.protocol.ts:6551; the declaration is at:6753. And its "47" figure countscode-named locals of all kinds (from [finding] check-error-code-casing's ||/?? fallback recognizer anchors on the STAMP SITE only, so our authored default in aconst code = …initializer is still seen by no gate #10897's blast radius) — thelet/varsubset is 6.The extent question is answered: LATENT, zero live victims
6
let/varcode-named declarations underpackages/**; only 1 actually reaches a.code =stamp; its initializer isnull, 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
constandletin one file (inner-scope shadowing) previously reduced to theconstalone and produced a site; it now answers null and reportsunresolved. Justified as the same refusal #9568 already pins for twoconsts 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 staysunresolvedeven then. Against that: population 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/-5is 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-changesetis 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:dispatchedstays until MERGED.Generated by Claude Code