Uh oh!
There was an error while loading. Please reload this page.
fix(pm): route the artifact-roster classifier through the file's own resolver - #13862
Conversation
…resolver `artifactOnlySilence` decided "every declared literal is a tracked FILE" with `trackedFiles.has(collapseHint(h))` — a private, weaker copy of a question this file already has an owner for. `hintCovers` follows a dropped extension through MODULE_SPECIFIER_EXTENSIONS and `extensionlessModuleTarget` names the file such a specifier resolves to; this predicate followed neither, so a family whose roster is extensionless import targets failed the every-literal test and printed as an ordinary silence — a different fact, and one that reads as a clearance. The repair is the single owner, not a list of the families it got wrong: `declaredFileTarget` is now the one answer to "the tracked FILE this declared literal names", composed from the membership test and the resolver, with patterns refused up front. `artifactOnlySilence` asks it and holds no rule of its own, and takes the `watchHintTree` bundle rather than a bare file set — refusing the bare set loudly, since that is exactly the input that mis-categorises silently. Measured over the whole fleet at 16c3601 (192 families, 754 distinct hints, 7605 tracked files): artifact-roster families 30 -> 39 (+9, ZERO lost), zero existing rosters whose membership or directory moved, and literals where the covering rule and the classifier disagree 40 -> 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
…olver The first spelling of the fleet-wide agreement case compared `declaredFileTarget` against `hintCovers`. The ablation that put the old per-literal rule back inside `artifactOnlySilence` left the resolver untouched, so that case was GREEN against the exact bug it exists to catch — a guard on the owner does not hold the caller to it. Restated at family grain over the classifier's own output: 5 of 1060 cases red under the ablation before, 6 after, and the sixth is the one that generalises past the fixture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Verification — commands, and each gate's own verdict lineFinal commit The standing instrument for this file, at the final head The ablation — the category pin proven, not asserted. No build leg exists: Gate union — derived, never hand-listed.
Outside the derivation, because no path derivation names it: NOT MEASURED, and not red. The script's own suite is its Live output, before vs after, on Generated by Claude Code Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13520
artifactOnlySilencedecides "every declared literal collapses to a tracked FILE". It did that withtrackedFiles.has(collapseHint(h))— which resolves nothing — so a family whose whole roster is extensionless import targets (packages/spec/scripts/lib/dist-freshness, while the tree holds.../dist-freshness.ts) failed the every-literal test and printed as an ordinary silence, which this file's own header calls "a different fact."The failure direction is the point: it threw nothing and printed no error. It returned a coherent, plausible, wrong category.
Why this class cannot recur under another gate name
The family prohibition is explicit, and this delivery is not a table of names — no gate name appears anywhere in the diff.
The defect was never "six families are missing". It was one predicate holding a private, weaker copy of a rule this file already has an owner for. Three places ask a form of "does this literal name a tracked file", and they answered it three ways:
hintCovers— the covering ruleMODULE_SPECIFIER_EXTENSIONS(#12514)extensionlessModuleTargetartifactOnlySilencetrackedFiles.has(collapseHint(h))— followed neitherThe file already refuses exactly this drift elsewhere, in its own words:
extractWatchHintswas denied the same widening because it would hand the file "a SECOND answer to a questionextensionlessModuleTargetalready owns — the drift this file refuses everywhere else." The roster classifier was that second answer, and nothing held it to the first.So the repair is structural, in three parts:
declaredFileTarget(hint, tree)is now the single answer to "the tracked FILE this declared literal names" — the membership test and the resolver composed once, with patterns refused up front.artifactOnlySilenceasks it and holds no rule of its own. Every spelling that owner learns is learned by the classifier in the same edit, for every family at once.Setof files — an input that cannot answer the question. It now takes thewatchHintTreebundle and refuses a bare set loudly, because that is precisely the shape that mis-categorises silently.artifactOnlySilencereturns a roster exactly when every declared literal of that family names exactly one tracked file underhintCovers, and the roster IS those files. Both sides are computed by different code — a full sweep of the tracked corpus throughhintCoverson one side, the membership test plus resolver on the other. The day someone teacheshintCoversa further spelling and forgets this reader, or reintroduces a private test in the classifier, it reds — for whatever family happens to carry it.Blast radius, measured across the WHOLE family set before widening
192 discovered families x 754 distinct hints x 7605 tracked files, at
16c3601d2. Measured with the shipped rule and the new one side by side over every family, not only the silent ones (roster-vs-not is path-independent; onlycoversYourPathreads the card's paths):dirmovedThe nine that move, each named by the sweep with the file its specifier resolves to:
The card named six; the sweep it asked for found nine.#13520 was explicit that nobody had swept for other rosters made of extensionless specifiers — this is that sweep, and
check:meta-url-spelling,check:react-blocksandcheck:react-declaration-parityare outside thepackages/spec/scripts/libcluster the card described.Nothing is lost and no existing roster moves, because resolution is the identity on a literal that already spells its file. The widening cannot reach a declared POPULATION:
extensionlessModuleTargetrefuses any hint the tree has as a prefix, so a directory literal never resolves, and a pattern is refused before either branch — measured, 18 pattern-judged hints in the fleet, 0 of which collapse to a tracked file, so that refusal is structural rather than lucky.One live-output reading, on a card touching
packages/spec/scripts/lib/dist-freshness.ts: rosters inside the silent bucket29 of those 123to34 of those 123, and the inverted ones — where a roster sits in a directory the card edits, the shape the issue says "reads as a clearance and is not" —3to5.The regression pins the CATEGORY
Six new cases, none of which is satisfiable by "did not crash" or "still classifies". Each names the bucket and its contents: which tracked files, under which directory, and for the negatives,
nullexactly. Proven by ablation — restoring the shipped per-literal rule insideartifactOnlySilence, with the mutation confirmed on disk and the restore confirmed by blob hash:Negatives, so the widening cannot become a second defect pointing the other way: a declared directory beside resolvable specifiers is still a population; an unresolvable specifier is still the unreachable species; a pattern whose collapse would land on a tracked file is refused; a bare file set is refused rather than answered.
The ablation corrected the guard
The first spelling of the class guard compared
declaredFileTargetagainsthintCovers— and it was GREEN against the exact bug it exists to catch, because the ablation left the resolver untouched: a guard on the owner does not hold the caller to it. Restated over the classifier's own output at family grain it reds, naming all nine families. That correction is its own commit.A2.2 — the file's own comments do NOT endorse this fix, and here is what they are about
Re-derived on this tree (line numbers moved; triage read
:2628and:2746)::2628"No spelling ofcollapseHintcan fix this, which is why the branch is here" — insideglobInNonFinalSegment's docblock, about a glob in a non-final segment mangling under collapse-by-deletion, inhintCovers. Not this branch, not this predicate.:2746"a questionextensionlessModuleTargetalready owns" — inside the "Why COMPARISON and not EXTRACTION" argument, about refusing to teachextractWatchHintsto follow an extension. Also not this predicate.⇒ Neither comment is about
artifactOnlySilence. Both are about neighbouring branches, so the file does not endorse this fix by describing this defect. What the second one does carry is the general principle the repair rests on — one owner per question, never a second answer — and it happens to state it about the very resolver this predicate was not using. That is a stronger warrant than a description would have been: the file had already refused this drift on principle, in a place where the principle cost it a feature, and then carried the drift in this predicate anyway. Cited in the new docblock as principle, not as a description of the defect.Other measured findings
hintCoversalready follows the dropped extension (hintCoversreads an extensionless module specifier as a filesystem path, so 9packages/specgate families can never be MATCHED to a change set — silent under-derivation on every dispatch #12514), so the contradiction was printed on one line of the--residueoutput: the dead-hint sweep marked every literal ofcheck:export-originsas reaching the tree while this predicate declined to call the family a roster. The two disagreeing instruments the issue describes were both inside this file.derivebuilt a third copy of the tracked-file set (new Set(swept)) beside the onewatchHintTreehad already built, under a comment saying a second read "would be a second answer to a question this run has already asked once". Removed; the bundle is now held in a name and handed down.artifactOnlyNote's sentence changed one word: "all N declared literal(s) name tracked FILES". A literal may name its file through a dropped extension, so "are tracked FILES" stopped being true. No hint text is printed by that note, so no printed hint moves.Verification
Commands and their own verdict lines are in the PR discussion; the gate union was re-derived from the merge base with
scripts/pm/dispatch-gates.mjs(no hand-written path list) and re-run after the final commit.Changeset
skip-changeset. The diff is one file,scripts/pm/dispatch-gates.mjs. The repo root isprivate: trueand declares nofilesarray; nothing underscripts/is published from any package, and this tool is internal PM tooling invoked bylint.yml. This PR releases nothing, which is the label's own criterion.Generated by Claude Code
Generated by Claude Code