Repository navigation
fix(tooling): attribute a gate's declared hints per ROW, not per FILE - #19204
Merged
os-try-charles merged 2 commits intoSep 19, 2026
Merged
Conversation
The bare-root worklist compared a recorded refusal against what a gate declares in three places and all three missed one shape: the declaration pin was scoped by VERDICT (DECLARED-NARROWER only), contradictedRows is scoped by REACHABILITY, and every remaining pin asks the TREE, which a declaration does not move. A REFUSE-* row whose gate declares a genuinely narrower hint at that row's root fell between all three, forever, with --self-test green. The obstacle is the idiom, not the pin: hints are declared per FILE, one array beside the constants, so a file-level read cannot tell two rows at one root apart. Attributing a declared hint to a CONSTANT rather than to a FILE is information the ROOT_DIR_WATCH_HINTS idiom does not carry. So the attribution is recorded per row and pinned: every hint a gate declares at a row's bare root is either CLAIMED by that row's recorded spelling or explicitly DISCLAIMED in its `omits`, read back out of the gate's source every run, in both directions. No verdict is added, changed or withdrawn; 46 verdicts / 13 UNJUDGED / 14 CENSUS unchanged. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
`const <the rostered name> =` spelled out inside a template literal IS a declaration site to check:watch-hint-literal, which reads them out of source text — so the fixture gate handed that gate a COMPUTED declaration in a file that declares nothing, and it reds `every live declaration is a literal`. Measured, then fixed the way that gate's own probe helper already does it: interpolate the name. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
os-try-charles
marked this pull request as ready for review
September 19, 2026 13:59
os-try-charles
deleted the
claude/issue-15926-refuse-row-declaration-pin
branch
September 19, 2026 14:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Attributing a declared hint to a CONSTANT rather than to a FILE is information the
ROOT_DIR_WATCH_HINTSidiom does not carry today. Hints are declared per FILE — one array beside the constants — so a file-level read cannot tell two rows at one root apart, andcheck-driver-conformancecarries exactly two atpackages:DRIVERS_DIR, which ownspackages/drivers/**, andCASE_SETS_DIR, which owns nothing there. That is why the obvious pin reds a correct row on day one, and it is the finding this card produced rather than the patch.Part of #15926.
The gap, re-verified on the base of this branch
scripts/pm/bare-root-worklist.mjscompared a recorded refusal against what a gate declares in exactly three places, and all three missed the same shape:v.verdict !== 'DECLARED-NARROWER'thencontinue),contradictedRowsis scoped by reachability (r.covered), which a strictly narrower declaration never makes true,So a
REFUSE-*row whose gate declares a genuinely narrower hint at that row's root fell between all three, forever, with--self-testgreen. #15602 measured that happening:check-declaration-mirrorsdeclared both sides of its mirror, the row went on recordingREFUSE-UNSPELLABLE, and nothing anywhere reddened.Asking today's question — which
REFUSE-*rows have a gate declaring at that root — yields one row on this branch's base (4cb3b15f4), not two:check-declaration-mirrors.mjs SCRIPTS_DIR scriptsis recordedDECLARED-NARROWERonorigin/main— #15602 landed and re-decided it, so it is no longer aREFUSE-*row at all. Consequence for the pricing below: on today's real tree the new direction has zero genuine positives. Its whole live effect is to hold one attribution that was previously prose, and its RED end must be constructed. That does not weaken the case (the class is what ran green indefinitely), but it does mean "which existing rows go red" is answerable exactly: none, on any route.The three routes, priced
All three were priced before one was written, as the card required. Measurements are at
4cb3b15f4in worktreeobjectstack-issue-15926unless stated.Route 1 — attribute by COVERAGE
Ask whether any declared hint COVERS the population that row's constant names.
scripts/pm/bare-root-worklist.mjs, plus every gate that would have to export a walk to driveSPELLINGSentry)The mechanism is right —
packages/drivers/**does not coverpackages/spec/src/data/*-conformance.ts, soCASE_SETS_DIRstays green, and a declaration atpackages/spec/src/data/**would cover it and red. The cost is the input: this map holds a row's population as prose plus a number, most of these gates export no walk, and the file's own docblock says so per row ("its gate exports no walk to drive and reproducing the filter by hand would be the estimate this docblock refuses"). The recorded precedent for how expensive a machine-readable population is here isCENSUS_REFUSE_WIDE: 14 rows measured understrace -f -e trace=openatagainst a dedicated worktree at one pinned base, after a JS-levelfsshim was tried first and measured EMPTY.⇒ Pricing this route is doing it: one re-measurement per row, 46 of them, which is the second card the triage said was hiding inside the option. The unpriceable-ness is itself the measurement.
Route 2 — extend
omitsDOWNWARD ⭐ landed hereLet a
REFUSErow name the sibling declaration it is deliberately not claiming, the way aDECLARED-NARROWERrow names a hint it passes over.scripts/pm/bare-root-worklist.mjsThe DECLARATION a row describes, read from the gate's own SOURCEgoes 43 → 128 cases (a floor, measured on a run)scripts/check-driver-conformance.mjs CASE_SETS_DIR packages, and it goes green by RECORDING the attribution its ownwhyalready asserted. The other 34 recorded rows sit in gates that declare nothing at their root; the remaining 11 were already inside the pin.The 85 new cases break down as: 46 gate-source-readable + 46 attribution (the generalisation, one per recorded row), 22 the
DECLARED-NARROWERpair the pin already ran, 3 non-vacuity identities, 1omitsshape rule and 1 control proving that rule can FAIL, 1 "no refusal records a spelling", and 7 constructed-gate cases.What it buys is not new information — the
CASE_SETS_DIRrow'swhyhas said "Its sibling constant took the escape; this one has nothing honest to declare" since it was written. ⭐ It puts that already-recorded sentence under a pin that re-checks it every run, turning prose that happens to be true into a fact that must stay true.Route 3 — attribute in the IDIOM
Carry the constant beside the hint at the declaration site.
check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) ... all 158 of those literals admitted. Plusscripts/check-watch-hint-literal.mjsitself (939 lines, 72 self-test cases) and each declaring gate's own assertions over its array.(file, root)pairs carry two rows; 2 of those files declare)DECLARED-NARROWERrows at onceThe blocking mechanic is measured, not assumed:
literalHintsaccepts only a flat array of quoted string literals and returnsnullotherwise, whichauditSourceNameturns intook: false— "the declaration is COMPUTED, not a literal array". So any richer element shape reds every one of the 71 declarations until the parser, the roster and every gate's self-test move with them in one PR. A cheaper spelling (a parallel owner map beside the array) is a FIFTH rostered idiom with its own roster entry, its own gate and its own drift pin — a relocation of the blast radius, not a reduction.⛔ Not reached for as the "proper" repair. It is the right long-term shape and it is a card of its own.
What landed
Every hint a gate declares at a row's bare root is now attributed: either CLAIMED by that row's recorded spelling, or explicitly DISCLAIMED in its
omits, with the reason in itswhy. The comparison is a pure exported function (declarationAttribution), so the live rows and the fixture cases beside them cannot disagree about what an attribution is — the same reasoncontradictedRowsis pure.Three asymmetries the generalisation respects:
recorded is a subset of declareddirection stays scoped toDECLARED-NARROWER. ASPELLABLE-UNDECLAREDspelling is a claim about the TREE, not about the array — its verdict says the gate declares NOTHING for that population — so reading it as a claim on the declaration would red 11 correct rows. A case pins that direction: a gate that STARTS declaring exactly that spelling reds the row for a re-decision instead of quietly satisfying it.spellingat all, now asserted.⛔ The
CASE_SETS_DIRrow is not suppressed. An allowlist names a ROW to skip and nothing about the gate can make it fire again; this names a HINT, re-reads it out of the gate's source every run, and reds when either side moves. Proven below in both directions.⛔ No verdict is added, changed or withdrawn. The 13
UNJUDGEDrows are untouched.先红后绿 — both directions driven
RED, on the real map (ablation: delete the one
omits, restore fromHEAD, restore proven byte-identical):RED, from the GATE's side (ablation: move
check-driver-conformance's declaration to another subtree) — this is the half that separates an attribution from an allowlist:RED, on a CONSTRUCTED case — a
REFUSErow whose gate declares a narrower hint at that root and which attributes it to nothing. Driven inside--self-test, through the idiom's OWN parser (auditSourceover a gate source built for it), never a hand-fed hint array. Its hints are joined from segments at runtime, and its declaration NAME is assembled at runtime too:constplus the rostered name spelled out inside this file IS a declaration site tocheck:watch-hint-literal, which redsevery live declaration is a literalon it — measured, then repaired.GREEN, on today's real tree:
check-driver-conformance CASE_SETS_DIR packagesdoes not red, and the reason is the attribution judgement.All three pinned numbers unchanged from the base; assertion count only rises.
Gates
Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no hand-written path list) and reconciled with--ran:Every one exited 0. Named readings worth quoting:
pnpm check:pm-dispatch-gates—VERDICT command-exit 0, 611.7s, run detached underscripts/pm/os-verify-lock.shand waited on withtail --pid(held the lock 613s, waited 0s).pnpm check:watch-hint-literal— went RED first on this diff, for the declaration-name reason above, and is green after:71 declaration(s) across 4 rostered name(s) ... no unrostered spelling of the idiom in the tree.pnpm lint(eslint . --no-inline-config, the whole repo, not a narrowing) — EXIT 0.scripts/pm/bare-root-worklist.mjsis a.mjswith no.d.mtsmirror, the repo-roottsconfig.jsonsets neitherallowJsnorcheckJs, andturbo run typecheckis per-package while this diff touches no package. ⇒ no tsc program compiles this file — a measured absence, not an unrun lane.dispatch-gatesprints two families whose names contain "typecheck" ([finding] dispatch-gates 不枚举 type-check lane,却印出两个 typecheck 命名的门禁家族 —— grep 到「有」的读者会以为这一面被兜住了(PR #19168 实测:82 家族全绿而 CI 红) #19172); neither reads this path.Changeset
None,
skip-changeset. The diff is one repo-root tooling script underscripts/pm/, which no package'sfiles[]ships — nothing already published moves.Not in scope, noted
scripts/check-whole-set-label-write.mjs ROOTS scriptsis anUNJUDGEDrow whose gate DOES declare three hints atscripts. It owes a maintainer ruling under #15468 and is ⛔ none of this card's business — the new pin deliberately does not reach rows carrying no verdict. Worth knowing for whoever judges it: if it lands on a refusal, the pin will ask it to attribute those three.Generated by Claude Code