Skip to content

fix(tooling): attribute a gate's declared hints per ROW, not per FILE - #19204

Merged
os-try-charles merged 2 commits into
mainfrom
claude/issue-15926-refuse-row-declaration-pin
Sep 19, 2026
Merged

os-try-charles merged 2 commits into
mainfrom
claude/issue-15926-refuse-row-declaration-pin

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

Attributing a declared hint to a CONSTANT rather than to a FILE is information the ROOT_DIR_WATCH_HINTS idiom 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, and check-driver-conformance carries exactly two at packages: DRIVERS_DIR, which owns packages/drivers/**, and CASE_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.mjs compared a recorded refusal against what a gate declares in exactly three places, and all three missed the same shape:

  • the declaration pin was scoped by verdict (v.verdict !== 'DECLARED-NARROWER' then continue),
  • contradictedRows is scoped by reachability (r.covered), which a strictly narrower declaration never makes true,
  • every remaining pin asks the tree, which a declaration does not move.

So a REFUSE-* row whose gate declares a genuinely narrower hint at that row's root fell between all three, forever, with --self-test green. #15602 measured that happening: check-declaration-mirrors declared both sides of its mirror, the row went on recording REFUSE-UNSPELLABLE, and nothing anywhere reddened.

⚠️ A correction to the dispatch's own measurement

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:

DECLARES  REFUSE-UNSPELLABLE  scripts/check-driver-conformance.mjs CASE_SETS_DIR packages  ->  ["packages/drivers/**"]
total: 1

check-declaration-mirrors.mjs SCRIPTS_DIR scripts is recorded DECLARED-NARROWER on origin/main — #15602 landed and re-decided it, so it is no longer a REFUSE-* 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 4cb3b15f4 in worktree objectstack-issue-15926 unless stated.

Route 1 — attribute by COVERAGE

Ask whether any declared hint COVERS the population that row's constant names.

term measured
files scripts/pm/bare-root-worklist.mjs, plus every gate that would have to export a walk to drive
lines not closable — see below
new pins ~46 attribution + ~138 population pins (each population is a new claim about the tree, and this file's own rule is that an unpinned claim is the allowlist rot it refuses by name, so each owes a LIVE/PRECISE/COMPLETE triple like every SPELLINGS entry)
rows green to red cannot be stated without doing the route

The mechanism is right — packages/drivers/** does not cover packages/spec/src/data/*-conformance.ts, so CASE_SETS_DIR stays green, and a declaration at packages/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 is CENSUS_REFUSE_WIDE: 14 rows measured under strace -f -e trace=openat against a dedicated worktree at one pinned base, after a JS-level fs shim 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 omits DOWNWARD ⭐ landed here

Let a REFUSE row name the sibling declaration it is deliberately not claiming, the way a DECLARED-NARROWER row names a hint it passes over.

term measured
files 1 — scripts/pm/bare-root-worklist.mjs
lines +347 / −37 (384 changed); the executable part is about 120 lines, the rest is the docblock this file requires of a widening
new pins battery The DECLARATION a row describes, read from the gate's own SOURCE goes 43 → 128 cases (a floor, measured on a run)
rows green to red zero, named: the only recorded row the widening newly reaches is scripts/check-driver-conformance.mjs CASE_SETS_DIR packages, and it goes green by RECORDING the attribution its own why already 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-NARROWER pair the pin already ran, 3 non-vacuity identities, 1 omits shape 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_DIR row's why has 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.

term measured
files 65 files declare, 71 declarations, 158 hint literals — corroborated by the gate's own pass line: check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) ... all 158 of those literals admitted. Plus scripts/check-watch-hint-literal.mjs itself (939 lines, 72 self-test cases) and each declaring gate's own assertions over its array.
lines 158 hint entries rewritten at minimum; the four rostered names occur 405 times across 73 tracked files, 71 of those being the declaration statements themselves
new pins the parser's own cases, plus one per gate carrying two population constants at one root (measured: 3 (file, root) pairs carry two rows; 2 of those files declare)
rows green to red during the transition, all 71 declarations and, in this file, the declaration pin on all 11 DECLARED-NARROWER rows at once

The blocking mechanic is measured, not assumed: literalHints accepts only a flat array of quoted string literals and returns null otherwise, which auditSourceName turns into ok: 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 its why. 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 reason contradictedRows is pure.

Three asymmetries the generalisation respects:

  • the recorded is a subset of declared direction stays scoped to DECLARED-NARROWER. A SPELLABLE-UNDECLARED spelling 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.
  • a refusal records no spelling at all, now asserted.
  • a record cannot both claim and disclaim one hint.

⛔ The CASE_SETS_DIR row 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 UNJUDGED rows are untouched.

先红后绿 — both directions driven

RED, on the real map (ablation: delete the one omits, restore from HEAD, restore proven byte-identical):

BEFORE marker count: 1
AFTER marker count: 0
RED LEG EXIT=1
failures: 1
x self-test: ...and every hint that gate declares at packages is ATTRIBUTED by
  "scripts/check-driver-conformance.mjs CASE_SETS_DIR packages" -- UNATTRIBUTED:
  [packages/drivers/**], named as omitted but NOT declared: [none], both claimed
  and disclaimed: [none].
HEAD blob 8476b244... == disk blob 8476b244...   git diff HEAD: empty

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:

GATE-MOVED LEG EXIT=1
failures: 3
ATTRIBUTED by "scripts/check-driver-conformance.mjs CASE_SETS_DIR packages"
ATTRIBUTED by "scripts/check-driver-conformance.mjs DRIVERS_DIR packages"
NOT DECLARED: packages/drivers/**
named as omitted but NOT declared: [packages/drivers/**]
restore: disk blob == HEAD blob, git status clean, self-test back to EXIT 0

RED, on a CONSTRUCTED case — a REFUSE row 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 (auditSource over 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: const plus the rostered name spelled out inside this file IS a declaration site to check:watch-hint-literal, which reds every live declaration is a literal on it — measured, then repaired.

GREEN, on today's real tree: check-driver-conformance CASE_SETS_DIR packages does not red, and the reason is the attribution judgement.

node scripts/pm/bare-root-worklist.mjs --self-test  -> EXIT 0
81 live row(s) - 46 recorded verdict(s) - 13 UNJUDGED - 14 CENSUS

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:

Run reconciliation -- 28 derived, 28 run, 0 NOT-MEASURED, 0 UNRUN.
EXIT CODES -- all 28 accounted famil(ies) carry one.

Every one exited 0. Named readings worth quoting:

  • pnpm check:pm-dispatch-gates — VERDICT command-exit 0, 611.7s, run detached under scripts/pm/os-verify-lock.sh and waited on with tail --pid (held the lock 613s, waited 0s). ⚠️ It is not a timeout-shaped NOT MEASURED; it is a reading.
  • 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.
  • Type check: scripts/pm/bare-root-worklist.mjs is a .mjs with no .d.mts mirror, the repo-root tsconfig.json sets neither allowJs nor checkJs, and turbo run typecheck is per-package while this diff touches no package. ⇒ no tsc program compiles this file — a measured absence, not an unrun lane. dispatch-gates prints 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 under scripts/pm/, which no package's files[] ships — nothing already published moves.

Not in scope, noted

scripts/check-whole-set-label-write.mjs ROOTS scripts is an UNJUDGED row whose gate DOES declare three hints at scripts. 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

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants