fix(pm): route the artifact-roster classifier through the file's own resolver - #13862

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier
Aug 31, 2026
Merged

fix(pm): route the artifact-roster classifier through the file's own resolver#13862
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13520

artifactOnlySilence decides "every declared literal collapses to a tracked FILE". It did that with trackedFiles.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:

readerhow it answered
hintCovers — the covering rulefollows a dropped extension through MODULE_SPECIFIER_EXTENSIONS (#12514)
extensionlessModuleTargetnames the file such a specifier resolves to (#12299)
artifactOnlySilencetrackedFiles.has(collapseHint(h)) — followed neither

The file already refuses exactly this drift elsewhere, in its own words: extractWatchHints was denied the same widening because it would hand the file "a SECOND answer to a question extensionlessModuleTarget already 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:

  1. One owner.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. artifactOnlySilence asks 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.
  2. The parameter that made the mistake possible is gone. The predicate took a bare Set of files — an input that cannot answer the question. It now takes the watchHintTree bundle and refuses a bare set loudly, because that is precisely the shape that mis-categorises silently.
  3. A class guard over the live fleet, at family grain (self-test): for every discovered family, artifactOnlySilence returns a roster exactly when every declared literal of that family names exactly one tracked file under hintCovers, and the roster IS those files. Both sides are computed by different code — a full sweep of the tracked corpus through hintCovers on one side, the membership test plus resolver on the other. The day someone teaches hintCovers a 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; only coversYourPath reads the card's paths):

beforeafter
artifact-roster families3039 (+9, 0 lost)
existing rosters whose membership or dir moved0
declared literals where the covering rule and the classifier disagree400
families where the covering rule and the classifier disagree90

The nine that move, each named by the sweep with the file its specifier resolves to:

check:api-surface packages/spec/scripts/lib/{dist-freshness,sharded-artifacts}
check:dual-source-exports packages/spec/scripts/lib/dist-freshness
check:export-origins packages/spec/scripts/lib/{export-origins-layout,sharded-artifacts}
check:exported-any packages/spec/scripts/lib/dist-freshness
check:meta-url-spelling packages/spec/src/{kernel,meta-spelling}/... (3)
check:react-blocks packages/spec/src/ui/react-blocks + 3 (no common directory)
check:react-declaration-parity packages/spec/src/ui/{react-blocks,component.zod,page.zod}
check:spec-changes packages/spec/src/{kernel,migrations}/... + scripts/lib (4)
check:strictness-ledger packages/spec/scripts/lib/{strictness-ledger,strictness-ledger-doc}

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-blocks and check:react-declaration-parity are outside the packages/spec/scripts/lib cluster 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: extensionlessModuleTarget refuses 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 bucket 29 of those 123 to 34 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"3 to 5.

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, null exactly. Proven by ablation — restoring the shipped per-literal rule inside artifactOnlySilence, with the mutation confirmed on disk and the restore confirmed by blob hash:

  • a roster of extensionless specifiers is an artifact ROSTER, not an ordinary silence
  • the category is pinned by its contents — the exact resolved file list
  • under the directory those files really sit in, not the one the specifier stops short at
  • and for a card in that directory it is the INVERTED silence
  • a roster mixing both spellings resolves to the same category
  • the fleet-wide family-grain agreement (the class guard above)

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 declaredFileTarget against hintCovers — 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 :2628 and :2746):

  • :2628"No spelling of collapseHint can fix this, which is why the branch is here" — inside globInNonFinalSegment's docblock, about a glob in a non-final segment mangling under collapse-by-deletion, in hintCovers. Not this branch, not this predicate.
  • :2746"a question extensionlessModuleTarget already owns" — inside the "Why COMPARISON and not EXTRACTION" argument, about refusing to teach extractWatchHints to 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

  • hintCovers already follows the dropped extension (hintCovers reads an extensionless module specifier as a filesystem path, so 9 packages/spec gate 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 --residue output: the dead-hint sweep marked every literal of check:export-origins as 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.
  • derive built a third copy of the tracked-file set (new Set(swept)) beside the one watchHintTree had 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 is private: true and declares no files array; nothing under scripts/ is published from any package, and this tool is internal PM tooling invoked by lint.yml. This PR releases nothing, which is the label's own criterion.

Generated by Claude Code


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Verification — commands, and each gate's own verdict line

Final commit 4eaee4a0e. Every heavy run went through scripts/pm/os-verify-lock.sh; exit codes were captured before any pipe.

The standing instrument for this file, at the final head

pnpm check:pm-dispatch-gates
-> dispatch-gates self-test: 1060 cases pass.
-> os-verify-lock: VERDICT command-exit 0 · held the lock 368s · waited 0s

The ablation — the category pin proven, not asserted. No build leg exists: dispatch-gates.mjs is a plain .mjs module loaded from source, so there is no dist/ for a mutation to fail to reach. The mutation restores the shipped per-literal rule inside artifactOnlySilence and nothing else.

mutation on disk anchor count 1 before; injected 1 (want 1), deleted 0 (want 0)
blob e307514375f49c4bee66609c7763292a38bb325b -> 5391ff5e324c25af1b4139efa89017e9997c77c9
result dispatch-gates self-test: 7 of 1060 case(s) failed
a roster spelled as extensionless module specifiers is an artifact ROSTER...
...and the CATEGORY is pinned by its contents: the tracked files those specifiers name
...under the directory those files really sit in...
...and for a card in that directory it is the INVERTED silence...
a roster mixing both spellings of the same claim resolves to the same category
the roster classifier and the covering rule agree about every family in the fleet (192 families)
...non-vacuously: the fleet carries families whose roster is named only through a dropped extension
restore git checkout HEAD -- ABSOLUTE_PATH under a trap; hash back to e307514375f4...,
git diff HEAD empty, git status --porcelain empty

Gate union — derived, never hand-listed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths, so the script reads its own changed set off the merge base; re-derived unchanged after git fetch origin main. 14 families, all green:

check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pm-dispatch-gates · check:pnpm-filter-targets · check:watch-hint-literal · check-ci-filter-parity (OK: all 129 declared cross-package glob(s) ... are covered) · check-cross-package-test-inputs · check-self-test-wired · check-shard-attestation (2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s))

Outside the derivation, because no path derivation names it: pnpm check:ratchet-remedy-authority — exit 0. Also pnpm check:nul-bytes — exit 0.

NOT MEASURED, and not red.check-test-completeness exits 3 with PREREQUISITE NOT MET — it grades a saved turbo run test log and none was named; its own text says the family list names it with no argument and the local reading is NOT MEASURED. check-ci-filter-parity and check-shard-attestation first exited 1 with PREREQUISITE NOT MET — the dependency yaml is not installed (the worktree had no node_modules yet); after pnpm install both exit 0, as recorded above.

The script's own suite is its --self-test, run above. git grep over test files for dispatch-gates finds only two prose mentions inside comments (packages/cli, packages/create-objectstack) — neither executes it.

Live output, before vs after, on node scripts/pm/dispatch-gates.mjs --residue packages/spec/scripts/lib/dist-freshness.ts: rosters inside the silent bucket 29 of those 123 to 34 of those 123, inverted rosters 3 to 5, and check:export-origins gains the inverted-silence note it should always have had.

Generated by Claude Code


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 15:43
@os-project-manager
os-project-manager added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 52ef672Aug 31, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13520-artifact-only-silence-classifier branch August 31, 2026 16:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(pm): route the artifact-roster classifier through the file's own resolver - #13862

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier
Aug 31, 2026
Merged

fix(pm): route the artifact-roster classifier through the file's own resolver#13862
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13520

artifactOnlySilence decides "every declared literal collapses to a tracked FILE". It did that with trackedFiles.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:

readerhow it answered
hintCovers — the covering rulefollows a dropped extension through MODULE_SPECIFIER_EXTENSIONS (#12514)
extensionlessModuleTargetnames the file such a specifier resolves to (#12299)
artifactOnlySilencetrackedFiles.has(collapseHint(h)) — followed neither

The file already refuses exactly this drift elsewhere, in its own words: extractWatchHints was denied the same widening because it would hand the file "a SECOND answer to a question extensionlessModuleTarget already 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:

  1. One owner.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. artifactOnlySilence asks 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.
  2. The parameter that made the mistake possible is gone. The predicate took a bare Set of files — an input that cannot answer the question. It now takes the watchHintTree bundle and refuses a bare set loudly, because that is precisely the shape that mis-categorises silently.
  3. A class guard over the live fleet, at family grain (self-test): for every discovered family, artifactOnlySilence returns a roster exactly when every declared literal of that family names exactly one tracked file under hintCovers, and the roster IS those files. Both sides are computed by different code — a full sweep of the tracked corpus through hintCovers on one side, the membership test plus resolver on the other. The day someone teaches hintCovers a 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; only coversYourPath reads the card's paths):

beforeafter
artifact-roster families3039 (+9, 0 lost)
existing rosters whose membership or dir moved0
declared literals where the covering rule and the classifier disagree400
families where the covering rule and the classifier disagree90

The nine that move, each named by the sweep with the file its specifier resolves to:

check:api-surface packages/spec/scripts/lib/{dist-freshness,sharded-artifacts}
check:dual-source-exports packages/spec/scripts/lib/dist-freshness
check:export-origins packages/spec/scripts/lib/{export-origins-layout,sharded-artifacts}
check:exported-any packages/spec/scripts/lib/dist-freshness
check:meta-url-spelling packages/spec/src/{kernel,meta-spelling}/... (3)
check:react-blocks packages/spec/src/ui/react-blocks + 3 (no common directory)
check:react-declaration-parity packages/spec/src/ui/{react-blocks,component.zod,page.zod}
check:spec-changes packages/spec/src/{kernel,migrations}/... + scripts/lib (4)
check:strictness-ledger packages/spec/scripts/lib/{strictness-ledger,strictness-ledger-doc}

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-blocks and check:react-declaration-parity are outside the packages/spec/scripts/lib cluster 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: extensionlessModuleTarget refuses 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 bucket 29 of those 123 to 34 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"3 to 5.

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, null exactly. Proven by ablation — restoring the shipped per-literal rule inside artifactOnlySilence, with the mutation confirmed on disk and the restore confirmed by blob hash:

  • a roster of extensionless specifiers is an artifact ROSTER, not an ordinary silence
  • the category is pinned by its contents — the exact resolved file list
  • under the directory those files really sit in, not the one the specifier stops short at
  • and for a card in that directory it is the INVERTED silence
  • a roster mixing both spellings resolves to the same category
  • the fleet-wide family-grain agreement (the class guard above)

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 declaredFileTarget against hintCovers — 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 :2628 and :2746):

  • :2628"No spelling of collapseHint can fix this, which is why the branch is here" — inside globInNonFinalSegment's docblock, about a glob in a non-final segment mangling under collapse-by-deletion, in hintCovers. Not this branch, not this predicate.
  • :2746"a question extensionlessModuleTarget already owns" — inside the "Why COMPARISON and not EXTRACTION" argument, about refusing to teach extractWatchHints to 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

  • hintCovers already follows the dropped extension (hintCovers reads an extensionless module specifier as a filesystem path, so 9 packages/spec gate 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 --residue output: the dead-hint sweep marked every literal of check:export-origins as 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.
  • derive built a third copy of the tracked-file set (new Set(swept)) beside the one watchHintTree had 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 is private: true and declares no files array; nothing under scripts/ is published from any package, and this tool is internal PM tooling invoked by lint.yml. This PR releases nothing, which is the label's own criterion.

Generated by Claude Code


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Verification — commands, and each gate's own verdict line

Final commit 4eaee4a0e. Every heavy run went through scripts/pm/os-verify-lock.sh; exit codes were captured before any pipe.

The standing instrument for this file, at the final head

pnpm check:pm-dispatch-gates
-> dispatch-gates self-test: 1060 cases pass.
-> os-verify-lock: VERDICT command-exit 0 · held the lock 368s · waited 0s

The ablation — the category pin proven, not asserted. No build leg exists: dispatch-gates.mjs is a plain .mjs module loaded from source, so there is no dist/ for a mutation to fail to reach. The mutation restores the shipped per-literal rule inside artifactOnlySilence and nothing else.

mutation on disk anchor count 1 before; injected 1 (want 1), deleted 0 (want 0)
blob e307514375f49c4bee66609c7763292a38bb325b -> 5391ff5e324c25af1b4139efa89017e9997c77c9
result dispatch-gates self-test: 7 of 1060 case(s) failed
a roster spelled as extensionless module specifiers is an artifact ROSTER...
...and the CATEGORY is pinned by its contents: the tracked files those specifiers name
...under the directory those files really sit in...
...and for a card in that directory it is the INVERTED silence...
a roster mixing both spellings of the same claim resolves to the same category
the roster classifier and the covering rule agree about every family in the fleet (192 families)
...non-vacuously: the fleet carries families whose roster is named only through a dropped extension
restore git checkout HEAD -- ABSOLUTE_PATH under a trap; hash back to e307514375f4...,
git diff HEAD empty, git status --porcelain empty

Gate union — derived, never hand-listed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths, so the script reads its own changed set off the merge base; re-derived unchanged after git fetch origin main. 14 families, all green:

check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pm-dispatch-gates · check:pnpm-filter-targets · check:watch-hint-literal · check-ci-filter-parity (OK: all 129 declared cross-package glob(s) ... are covered) · check-cross-package-test-inputs · check-self-test-wired · check-shard-attestation (2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s))

Outside the derivation, because no path derivation names it: pnpm check:ratchet-remedy-authority — exit 0. Also pnpm check:nul-bytes — exit 0.

NOT MEASURED, and not red.check-test-completeness exits 3 with PREREQUISITE NOT MET — it grades a saved turbo run test log and none was named; its own text says the family list names it with no argument and the local reading is NOT MEASURED. check-ci-filter-parity and check-shard-attestation first exited 1 with PREREQUISITE NOT MET — the dependency yaml is not installed (the worktree had no node_modules yet); after pnpm install both exit 0, as recorded above.

The script's own suite is its --self-test, run above. git grep over test files for dispatch-gates finds only two prose mentions inside comments (packages/cli, packages/create-objectstack) — neither executes it.

Live output, before vs after, on node scripts/pm/dispatch-gates.mjs --residue packages/spec/scripts/lib/dist-freshness.ts: rosters inside the silent bucket 29 of those 123 to 34 of those 123, inverted rosters 3 to 5, and check:export-origins gains the inverted-silence note it should always have had.

Generated by Claude Code


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 15:43
@os-project-manager
os-project-manager added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 52ef672Aug 31, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13520-artifact-only-silence-classifier branch August 31, 2026 16:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): route the artifact-roster classifier through the file's own resolver - #13862

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier
Aug 31, 2026
Merged

fix(pm): route the artifact-roster classifier through the file's own resolver#13862
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13520

artifactOnlySilence decides "every declared literal collapses to a tracked FILE". It did that with trackedFiles.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:

readerhow it answered
hintCovers — the covering rulefollows a dropped extension through MODULE_SPECIFIER_EXTENSIONS (#12514)
extensionlessModuleTargetnames the file such a specifier resolves to (#12299)
artifactOnlySilencetrackedFiles.has(collapseHint(h)) — followed neither

The file already refuses exactly this drift elsewhere, in its own words: extractWatchHints was denied the same widening because it would hand the file "a SECOND answer to a question extensionlessModuleTarget already 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:

  1. One owner.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. artifactOnlySilence asks 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.
  2. The parameter that made the mistake possible is gone. The predicate took a bare Set of files — an input that cannot answer the question. It now takes the watchHintTree bundle and refuses a bare set loudly, because that is precisely the shape that mis-categorises silently.
  3. A class guard over the live fleet, at family grain (self-test): for every discovered family, artifactOnlySilence returns a roster exactly when every declared literal of that family names exactly one tracked file under hintCovers, and the roster IS those files. Both sides are computed by different code — a full sweep of the tracked corpus through hintCovers on one side, the membership test plus resolver on the other. The day someone teaches hintCovers a 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; only coversYourPath reads the card's paths):

beforeafter
artifact-roster families3039 (+9, 0 lost)
existing rosters whose membership or dir moved0
declared literals where the covering rule and the classifier disagree400
families where the covering rule and the classifier disagree90

The nine that move, each named by the sweep with the file its specifier resolves to:

check:api-surface packages/spec/scripts/lib/{dist-freshness,sharded-artifacts}
check:dual-source-exports packages/spec/scripts/lib/dist-freshness
check:export-origins packages/spec/scripts/lib/{export-origins-layout,sharded-artifacts}
check:exported-any packages/spec/scripts/lib/dist-freshness
check:meta-url-spelling packages/spec/src/{kernel,meta-spelling}/... (3)
check:react-blocks packages/spec/src/ui/react-blocks + 3 (no common directory)
check:react-declaration-parity packages/spec/src/ui/{react-blocks,component.zod,page.zod}
check:spec-changes packages/spec/src/{kernel,migrations}/... + scripts/lib (4)
check:strictness-ledger packages/spec/scripts/lib/{strictness-ledger,strictness-ledger-doc}

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-blocks and check:react-declaration-parity are outside the packages/spec/scripts/lib cluster 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: extensionlessModuleTarget refuses 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 bucket 29 of those 123 to 34 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"3 to 5.

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, null exactly. Proven by ablation — restoring the shipped per-literal rule inside artifactOnlySilence, with the mutation confirmed on disk and the restore confirmed by blob hash:

  • a roster of extensionless specifiers is an artifact ROSTER, not an ordinary silence
  • the category is pinned by its contents — the exact resolved file list
  • under the directory those files really sit in, not the one the specifier stops short at
  • and for a card in that directory it is the INVERTED silence
  • a roster mixing both spellings resolves to the same category
  • the fleet-wide family-grain agreement (the class guard above)

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 declaredFileTarget against hintCovers — 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 :2628 and :2746):

  • :2628"No spelling of collapseHint can fix this, which is why the branch is here" — inside globInNonFinalSegment's docblock, about a glob in a non-final segment mangling under collapse-by-deletion, in hintCovers. Not this branch, not this predicate.
  • :2746"a question extensionlessModuleTarget already owns" — inside the "Why COMPARISON and not EXTRACTION" argument, about refusing to teach extractWatchHints to 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

  • hintCovers already follows the dropped extension (hintCovers reads an extensionless module specifier as a filesystem path, so 9 packages/spec gate 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 --residue output: the dead-hint sweep marked every literal of check:export-origins as 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.
  • derive built a third copy of the tracked-file set (new Set(swept)) beside the one watchHintTree had 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 is private: true and declares no files array; nothing under scripts/ is published from any package, and this tool is internal PM tooling invoked by lint.yml. This PR releases nothing, which is the label's own criterion.

Generated by Claude Code


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Verification — commands, and each gate's own verdict line

Final commit 4eaee4a0e. Every heavy run went through scripts/pm/os-verify-lock.sh; exit codes were captured before any pipe.

The standing instrument for this file, at the final head

pnpm check:pm-dispatch-gates
-> dispatch-gates self-test: 1060 cases pass.
-> os-verify-lock: VERDICT command-exit 0 · held the lock 368s · waited 0s

The ablation — the category pin proven, not asserted. No build leg exists: dispatch-gates.mjs is a plain .mjs module loaded from source, so there is no dist/ for a mutation to fail to reach. The mutation restores the shipped per-literal rule inside artifactOnlySilence and nothing else.

mutation on disk anchor count 1 before; injected 1 (want 1), deleted 0 (want 0)
blob e307514375f49c4bee66609c7763292a38bb325b -> 5391ff5e324c25af1b4139efa89017e9997c77c9
result dispatch-gates self-test: 7 of 1060 case(s) failed
a roster spelled as extensionless module specifiers is an artifact ROSTER...
...and the CATEGORY is pinned by its contents: the tracked files those specifiers name
...under the directory those files really sit in...
...and for a card in that directory it is the INVERTED silence...
a roster mixing both spellings of the same claim resolves to the same category
the roster classifier and the covering rule agree about every family in the fleet (192 families)
...non-vacuously: the fleet carries families whose roster is named only through a dropped extension
restore git checkout HEAD -- ABSOLUTE_PATH under a trap; hash back to e307514375f4...,
git diff HEAD empty, git status --porcelain empty

Gate union — derived, never hand-listed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths, so the script reads its own changed set off the merge base; re-derived unchanged after git fetch origin main. 14 families, all green:

check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pm-dispatch-gates · check:pnpm-filter-targets · check:watch-hint-literal · check-ci-filter-parity (OK: all 129 declared cross-package glob(s) ... are covered) · check-cross-package-test-inputs · check-self-test-wired · check-shard-attestation (2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s))

Outside the derivation, because no path derivation names it: pnpm check:ratchet-remedy-authority — exit 0. Also pnpm check:nul-bytes — exit 0.

NOT MEASURED, and not red.check-test-completeness exits 3 with PREREQUISITE NOT MET — it grades a saved turbo run test log and none was named; its own text says the family list names it with no argument and the local reading is NOT MEASURED. check-ci-filter-parity and check-shard-attestation first exited 1 with PREREQUISITE NOT MET — the dependency yaml is not installed (the worktree had no node_modules yet); after pnpm install both exit 0, as recorded above.

The script's own suite is its --self-test, run above. git grep over test files for dispatch-gates finds only two prose mentions inside comments (packages/cli, packages/create-objectstack) — neither executes it.

Live output, before vs after, on node scripts/pm/dispatch-gates.mjs --residue packages/spec/scripts/lib/dist-freshness.ts: rosters inside the silent bucket 29 of those 123 to 34 of those 123, inverted rosters 3 to 5, and check:export-origins gains the inverted-silence note it should always have had.

Generated by Claude Code


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 15:43
@os-project-manager
os-project-manager added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 52ef672Aug 31, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13520-artifact-only-silence-classifier branch August 31, 2026 16:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): route the artifact-roster classifier through the file's own resolver - #13862

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier
Aug 31, 2026
Merged

fix(pm): route the artifact-roster classifier through the file's own resolver#13862
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13520

artifactOnlySilence decides "every declared literal collapses to a tracked FILE". It did that with trackedFiles.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:

readerhow it answered
hintCovers — the covering rulefollows a dropped extension through MODULE_SPECIFIER_EXTENSIONS (#12514)
extensionlessModuleTargetnames the file such a specifier resolves to (#12299)
artifactOnlySilencetrackedFiles.has(collapseHint(h)) — followed neither

The file already refuses exactly this drift elsewhere, in its own words: extractWatchHints was denied the same widening because it would hand the file "a SECOND answer to a question extensionlessModuleTarget already 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:

  1. One owner.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. artifactOnlySilence asks 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.
  2. The parameter that made the mistake possible is gone. The predicate took a bare Set of files — an input that cannot answer the question. It now takes the watchHintTree bundle and refuses a bare set loudly, because that is precisely the shape that mis-categorises silently.
  3. A class guard over the live fleet, at family grain (self-test): for every discovered family, artifactOnlySilence returns a roster exactly when every declared literal of that family names exactly one tracked file under hintCovers, and the roster IS those files. Both sides are computed by different code — a full sweep of the tracked corpus through hintCovers on one side, the membership test plus resolver on the other. The day someone teaches hintCovers a 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; only coversYourPath reads the card's paths):

beforeafter
artifact-roster families3039 (+9, 0 lost)
existing rosters whose membership or dir moved0
declared literals where the covering rule and the classifier disagree400
families where the covering rule and the classifier disagree90

The nine that move, each named by the sweep with the file its specifier resolves to:

check:api-surface packages/spec/scripts/lib/{dist-freshness,sharded-artifacts}
check:dual-source-exports packages/spec/scripts/lib/dist-freshness
check:export-origins packages/spec/scripts/lib/{export-origins-layout,sharded-artifacts}
check:exported-any packages/spec/scripts/lib/dist-freshness
check:meta-url-spelling packages/spec/src/{kernel,meta-spelling}/... (3)
check:react-blocks packages/spec/src/ui/react-blocks + 3 (no common directory)
check:react-declaration-parity packages/spec/src/ui/{react-blocks,component.zod,page.zod}
check:spec-changes packages/spec/src/{kernel,migrations}/... + scripts/lib (4)
check:strictness-ledger packages/spec/scripts/lib/{strictness-ledger,strictness-ledger-doc}

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-blocks and check:react-declaration-parity are outside the packages/spec/scripts/lib cluster 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: extensionlessModuleTarget refuses 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 bucket 29 of those 123 to 34 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"3 to 5.

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, null exactly. Proven by ablation — restoring the shipped per-literal rule inside artifactOnlySilence, with the mutation confirmed on disk and the restore confirmed by blob hash:

  • a roster of extensionless specifiers is an artifact ROSTER, not an ordinary silence
  • the category is pinned by its contents — the exact resolved file list
  • under the directory those files really sit in, not the one the specifier stops short at
  • and for a card in that directory it is the INVERTED silence
  • a roster mixing both spellings resolves to the same category
  • the fleet-wide family-grain agreement (the class guard above)

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 declaredFileTarget against hintCovers — 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 :2628 and :2746):

  • :2628"No spelling of collapseHint can fix this, which is why the branch is here" — inside globInNonFinalSegment's docblock, about a glob in a non-final segment mangling under collapse-by-deletion, in hintCovers. Not this branch, not this predicate.
  • :2746"a question extensionlessModuleTarget already owns" — inside the "Why COMPARISON and not EXTRACTION" argument, about refusing to teach extractWatchHints to 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

  • hintCovers already follows the dropped extension (hintCovers reads an extensionless module specifier as a filesystem path, so 9 packages/spec gate 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 --residue output: the dead-hint sweep marked every literal of check:export-origins as 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.
  • derive built a third copy of the tracked-file set (new Set(swept)) beside the one watchHintTree had 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 is private: true and declares no files array; nothing under scripts/ is published from any package, and this tool is internal PM tooling invoked by lint.yml. This PR releases nothing, which is the label's own criterion.

Generated by Claude Code


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Verification — commands, and each gate's own verdict line

Final commit 4eaee4a0e. Every heavy run went through scripts/pm/os-verify-lock.sh; exit codes were captured before any pipe.

The standing instrument for this file, at the final head

pnpm check:pm-dispatch-gates
-> dispatch-gates self-test: 1060 cases pass.
-> os-verify-lock: VERDICT command-exit 0 · held the lock 368s · waited 0s

The ablation — the category pin proven, not asserted. No build leg exists: dispatch-gates.mjs is a plain .mjs module loaded from source, so there is no dist/ for a mutation to fail to reach. The mutation restores the shipped per-literal rule inside artifactOnlySilence and nothing else.

mutation on disk anchor count 1 before; injected 1 (want 1), deleted 0 (want 0)
blob e307514375f49c4bee66609c7763292a38bb325b -> 5391ff5e324c25af1b4139efa89017e9997c77c9
result dispatch-gates self-test: 7 of 1060 case(s) failed
a roster spelled as extensionless module specifiers is an artifact ROSTER...
...and the CATEGORY is pinned by its contents: the tracked files those specifiers name
...under the directory those files really sit in...
...and for a card in that directory it is the INVERTED silence...
a roster mixing both spellings of the same claim resolves to the same category
the roster classifier and the covering rule agree about every family in the fleet (192 families)
...non-vacuously: the fleet carries families whose roster is named only through a dropped extension
restore git checkout HEAD -- ABSOLUTE_PATH under a trap; hash back to e307514375f4...,
git diff HEAD empty, git status --porcelain empty

Gate union — derived, never hand-listed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths, so the script reads its own changed set off the merge base; re-derived unchanged after git fetch origin main. 14 families, all green:

check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pm-dispatch-gates · check:pnpm-filter-targets · check:watch-hint-literal · check-ci-filter-parity (OK: all 129 declared cross-package glob(s) ... are covered) · check-cross-package-test-inputs · check-self-test-wired · check-shard-attestation (2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s))

Outside the derivation, because no path derivation names it: pnpm check:ratchet-remedy-authority — exit 0. Also pnpm check:nul-bytes — exit 0.

NOT MEASURED, and not red.check-test-completeness exits 3 with PREREQUISITE NOT MET — it grades a saved turbo run test log and none was named; its own text says the family list names it with no argument and the local reading is NOT MEASURED. check-ci-filter-parity and check-shard-attestation first exited 1 with PREREQUISITE NOT MET — the dependency yaml is not installed (the worktree had no node_modules yet); after pnpm install both exit 0, as recorded above.

The script's own suite is its --self-test, run above. git grep over test files for dispatch-gates finds only two prose mentions inside comments (packages/cli, packages/create-objectstack) — neither executes it.

Live output, before vs after, on node scripts/pm/dispatch-gates.mjs --residue packages/spec/scripts/lib/dist-freshness.ts: rosters inside the silent bucket 29 of those 123 to 34 of those 123, inverted rosters 3 to 5, and check:export-origins gains the inverted-silence note it should always have had.

Generated by Claude Code


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 15:43
@os-project-manager
os-project-manager added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 52ef672Aug 31, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13520-artifact-only-silence-classifier branch August 31, 2026 16:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(pm): route the artifact-roster classifier through the file's own resolver - #13862

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier
Aug 31, 2026
Merged

fix(pm): route the artifact-roster classifier through the file's own resolver#13862
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13520

artifactOnlySilence decides "every declared literal collapses to a tracked FILE". It did that with trackedFiles.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:

readerhow it answered
hintCovers — the covering rulefollows a dropped extension through MODULE_SPECIFIER_EXTENSIONS (#12514)
extensionlessModuleTargetnames the file such a specifier resolves to (#12299)
artifactOnlySilencetrackedFiles.has(collapseHint(h)) — followed neither

The file already refuses exactly this drift elsewhere, in its own words: extractWatchHints was denied the same widening because it would hand the file "a SECOND answer to a question extensionlessModuleTarget already 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:

  1. One owner.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. artifactOnlySilence asks 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.
  2. The parameter that made the mistake possible is gone. The predicate took a bare Set of files — an input that cannot answer the question. It now takes the watchHintTree bundle and refuses a bare set loudly, because that is precisely the shape that mis-categorises silently.
  3. A class guard over the live fleet, at family grain (self-test): for every discovered family, artifactOnlySilence returns a roster exactly when every declared literal of that family names exactly one tracked file under hintCovers, and the roster IS those files. Both sides are computed by different code — a full sweep of the tracked corpus through hintCovers on one side, the membership test plus resolver on the other. The day someone teaches hintCovers a 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; only coversYourPath reads the card's paths):

beforeafter
artifact-roster families3039 (+9, 0 lost)
existing rosters whose membership or dir moved0
declared literals where the covering rule and the classifier disagree400
families where the covering rule and the classifier disagree90

The nine that move, each named by the sweep with the file its specifier resolves to:

check:api-surface packages/spec/scripts/lib/{dist-freshness,sharded-artifacts}
check:dual-source-exports packages/spec/scripts/lib/dist-freshness
check:export-origins packages/spec/scripts/lib/{export-origins-layout,sharded-artifacts}
check:exported-any packages/spec/scripts/lib/dist-freshness
check:meta-url-spelling packages/spec/src/{kernel,meta-spelling}/... (3)
check:react-blocks packages/spec/src/ui/react-blocks + 3 (no common directory)
check:react-declaration-parity packages/spec/src/ui/{react-blocks,component.zod,page.zod}
check:spec-changes packages/spec/src/{kernel,migrations}/... + scripts/lib (4)
check:strictness-ledger packages/spec/scripts/lib/{strictness-ledger,strictness-ledger-doc}

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-blocks and check:react-declaration-parity are outside the packages/spec/scripts/lib cluster 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: extensionlessModuleTarget refuses 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 bucket 29 of those 123 to 34 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"3 to 5.

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, null exactly. Proven by ablation — restoring the shipped per-literal rule inside artifactOnlySilence, with the mutation confirmed on disk and the restore confirmed by blob hash:

  • a roster of extensionless specifiers is an artifact ROSTER, not an ordinary silence
  • the category is pinned by its contents — the exact resolved file list
  • under the directory those files really sit in, not the one the specifier stops short at
  • and for a card in that directory it is the INVERTED silence
  • a roster mixing both spellings resolves to the same category
  • the fleet-wide family-grain agreement (the class guard above)

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 declaredFileTarget against hintCovers — 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 :2628 and :2746):

  • :2628"No spelling of collapseHint can fix this, which is why the branch is here" — inside globInNonFinalSegment's docblock, about a glob in a non-final segment mangling under collapse-by-deletion, in hintCovers. Not this branch, not this predicate.
  • :2746"a question extensionlessModuleTarget already owns" — inside the "Why COMPARISON and not EXTRACTION" argument, about refusing to teach extractWatchHints to 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

  • hintCovers already follows the dropped extension (hintCovers reads an extensionless module specifier as a filesystem path, so 9 packages/spec gate 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 --residue output: the dead-hint sweep marked every literal of check:export-origins as 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.
  • derive built a third copy of the tracked-file set (new Set(swept)) beside the one watchHintTree had 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 is private: true and declares no files array; nothing under scripts/ is published from any package, and this tool is internal PM tooling invoked by lint.yml. This PR releases nothing, which is the label's own criterion.

Generated by Claude Code


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Verification — commands, and each gate's own verdict line

Final commit 4eaee4a0e. Every heavy run went through scripts/pm/os-verify-lock.sh; exit codes were captured before any pipe.

The standing instrument for this file, at the final head

pnpm check:pm-dispatch-gates
-> dispatch-gates self-test: 1060 cases pass.
-> os-verify-lock: VERDICT command-exit 0 · held the lock 368s · waited 0s

The ablation — the category pin proven, not asserted. No build leg exists: dispatch-gates.mjs is a plain .mjs module loaded from source, so there is no dist/ for a mutation to fail to reach. The mutation restores the shipped per-literal rule inside artifactOnlySilence and nothing else.

mutation on disk anchor count 1 before; injected 1 (want 1), deleted 0 (want 0)
blob e307514375f49c4bee66609c7763292a38bb325b -> 5391ff5e324c25af1b4139efa89017e9997c77c9
result dispatch-gates self-test: 7 of 1060 case(s) failed
a roster spelled as extensionless module specifiers is an artifact ROSTER...
...and the CATEGORY is pinned by its contents: the tracked files those specifiers name
...under the directory those files really sit in...
...and for a card in that directory it is the INVERTED silence...
a roster mixing both spellings of the same claim resolves to the same category
the roster classifier and the covering rule agree about every family in the fleet (192 families)
...non-vacuously: the fleet carries families whose roster is named only through a dropped extension
restore git checkout HEAD -- ABSOLUTE_PATH under a trap; hash back to e307514375f4...,
git diff HEAD empty, git status --porcelain empty

Gate union — derived, never hand-listed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths, so the script reads its own changed set off the merge base; re-derived unchanged after git fetch origin main. 14 families, all green:

check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pm-dispatch-gates · check:pnpm-filter-targets · check:watch-hint-literal · check-ci-filter-parity (OK: all 129 declared cross-package glob(s) ... are covered) · check-cross-package-test-inputs · check-self-test-wired · check-shard-attestation (2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s))

Outside the derivation, because no path derivation names it: pnpm check:ratchet-remedy-authority — exit 0. Also pnpm check:nul-bytes — exit 0.

NOT MEASURED, and not red.check-test-completeness exits 3 with PREREQUISITE NOT MET — it grades a saved turbo run test log and none was named; its own text says the family list names it with no argument and the local reading is NOT MEASURED. check-ci-filter-parity and check-shard-attestation first exited 1 with PREREQUISITE NOT MET — the dependency yaml is not installed (the worktree had no node_modules yet); after pnpm install both exit 0, as recorded above.

The script's own suite is its --self-test, run above. git grep over test files for dispatch-gates finds only two prose mentions inside comments (packages/cli, packages/create-objectstack) — neither executes it.

Live output, before vs after, on node scripts/pm/dispatch-gates.mjs --residue packages/spec/scripts/lib/dist-freshness.ts: rosters inside the silent bucket 29 of those 123 to 34 of those 123, inverted rosters 3 to 5, and check:export-origins gains the inverted-silence note it should always have had.

Generated by Claude Code


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 15:43
@os-project-manager
os-project-manager added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 52ef672Aug 31, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13520-artifact-only-silence-classifier branch August 31, 2026 16:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): route the artifact-roster classifier through the file's own resolver - #13862

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier
Aug 31, 2026
Merged

fix(pm): route the artifact-roster classifier through the file's own resolver#13862
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13520

artifactOnlySilence decides "every declared literal collapses to a tracked FILE". It did that with trackedFiles.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:

readerhow it answered
hintCovers — the covering rulefollows a dropped extension through MODULE_SPECIFIER_EXTENSIONS (#12514)
extensionlessModuleTargetnames the file such a specifier resolves to (#12299)
artifactOnlySilencetrackedFiles.has(collapseHint(h)) — followed neither

The file already refuses exactly this drift elsewhere, in its own words: extractWatchHints was denied the same widening because it would hand the file "a SECOND answer to a question extensionlessModuleTarget already 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:

  1. One owner.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. artifactOnlySilence asks 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.
  2. The parameter that made the mistake possible is gone. The predicate took a bare Set of files — an input that cannot answer the question. It now takes the watchHintTree bundle and refuses a bare set loudly, because that is precisely the shape that mis-categorises silently.
  3. A class guard over the live fleet, at family grain (self-test): for every discovered family, artifactOnlySilence returns a roster exactly when every declared literal of that family names exactly one tracked file under hintCovers, and the roster IS those files. Both sides are computed by different code — a full sweep of the tracked corpus through hintCovers on one side, the membership test plus resolver on the other. The day someone teaches hintCovers a 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; only coversYourPath reads the card's paths):

beforeafter
artifact-roster families3039 (+9, 0 lost)
existing rosters whose membership or dir moved0
declared literals where the covering rule and the classifier disagree400
families where the covering rule and the classifier disagree90

The nine that move, each named by the sweep with the file its specifier resolves to:

check:api-surface packages/spec/scripts/lib/{dist-freshness,sharded-artifacts}
check:dual-source-exports packages/spec/scripts/lib/dist-freshness
check:export-origins packages/spec/scripts/lib/{export-origins-layout,sharded-artifacts}
check:exported-any packages/spec/scripts/lib/dist-freshness
check:meta-url-spelling packages/spec/src/{kernel,meta-spelling}/... (3)
check:react-blocks packages/spec/src/ui/react-blocks + 3 (no common directory)
check:react-declaration-parity packages/spec/src/ui/{react-blocks,component.zod,page.zod}
check:spec-changes packages/spec/src/{kernel,migrations}/... + scripts/lib (4)
check:strictness-ledger packages/spec/scripts/lib/{strictness-ledger,strictness-ledger-doc}

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-blocks and check:react-declaration-parity are outside the packages/spec/scripts/lib cluster 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: extensionlessModuleTarget refuses 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 bucket 29 of those 123 to 34 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"3 to 5.

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, null exactly. Proven by ablation — restoring the shipped per-literal rule inside artifactOnlySilence, with the mutation confirmed on disk and the restore confirmed by blob hash:

  • a roster of extensionless specifiers is an artifact ROSTER, not an ordinary silence
  • the category is pinned by its contents — the exact resolved file list
  • under the directory those files really sit in, not the one the specifier stops short at
  • and for a card in that directory it is the INVERTED silence
  • a roster mixing both spellings resolves to the same category
  • the fleet-wide family-grain agreement (the class guard above)

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 declaredFileTarget against hintCovers — 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 :2628 and :2746):

  • :2628"No spelling of collapseHint can fix this, which is why the branch is here" — inside globInNonFinalSegment's docblock, about a glob in a non-final segment mangling under collapse-by-deletion, in hintCovers. Not this branch, not this predicate.
  • :2746"a question extensionlessModuleTarget already owns" — inside the "Why COMPARISON and not EXTRACTION" argument, about refusing to teach extractWatchHints to 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

  • hintCovers already follows the dropped extension (hintCovers reads an extensionless module specifier as a filesystem path, so 9 packages/spec gate 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 --residue output: the dead-hint sweep marked every literal of check:export-origins as 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.
  • derive built a third copy of the tracked-file set (new Set(swept)) beside the one watchHintTree had 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 is private: true and declares no files array; nothing under scripts/ is published from any package, and this tool is internal PM tooling invoked by lint.yml. This PR releases nothing, which is the label's own criterion.

Generated by Claude Code


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Verification — commands, and each gate's own verdict line

Final commit 4eaee4a0e. Every heavy run went through scripts/pm/os-verify-lock.sh; exit codes were captured before any pipe.

The standing instrument for this file, at the final head

pnpm check:pm-dispatch-gates
-> dispatch-gates self-test: 1060 cases pass.
-> os-verify-lock: VERDICT command-exit 0 · held the lock 368s · waited 0s

The ablation — the category pin proven, not asserted. No build leg exists: dispatch-gates.mjs is a plain .mjs module loaded from source, so there is no dist/ for a mutation to fail to reach. The mutation restores the shipped per-literal rule inside artifactOnlySilence and nothing else.

mutation on disk anchor count 1 before; injected 1 (want 1), deleted 0 (want 0)
blob e307514375f49c4bee66609c7763292a38bb325b -> 5391ff5e324c25af1b4139efa89017e9997c77c9
result dispatch-gates self-test: 7 of 1060 case(s) failed
a roster spelled as extensionless module specifiers is an artifact ROSTER...
...and the CATEGORY is pinned by its contents: the tracked files those specifiers name
...under the directory those files really sit in...
...and for a card in that directory it is the INVERTED silence...
a roster mixing both spellings of the same claim resolves to the same category
the roster classifier and the covering rule agree about every family in the fleet (192 families)
...non-vacuously: the fleet carries families whose roster is named only through a dropped extension
restore git checkout HEAD -- ABSOLUTE_PATH under a trap; hash back to e307514375f4...,
git diff HEAD empty, git status --porcelain empty

Gate union — derived, never hand-listed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths, so the script reads its own changed set off the merge base; re-derived unchanged after git fetch origin main. 14 families, all green:

check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pm-dispatch-gates · check:pnpm-filter-targets · check:watch-hint-literal · check-ci-filter-parity (OK: all 129 declared cross-package glob(s) ... are covered) · check-cross-package-test-inputs · check-self-test-wired · check-shard-attestation (2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s))

Outside the derivation, because no path derivation names it: pnpm check:ratchet-remedy-authority — exit 0. Also pnpm check:nul-bytes — exit 0.

NOT MEASURED, and not red.check-test-completeness exits 3 with PREREQUISITE NOT MET — it grades a saved turbo run test log and none was named; its own text says the family list names it with no argument and the local reading is NOT MEASURED. check-ci-filter-parity and check-shard-attestation first exited 1 with PREREQUISITE NOT MET — the dependency yaml is not installed (the worktree had no node_modules yet); after pnpm install both exit 0, as recorded above.

The script's own suite is its --self-test, run above. git grep over test files for dispatch-gates finds only two prose mentions inside comments (packages/cli, packages/create-objectstack) — neither executes it.

Live output, before vs after, on node scripts/pm/dispatch-gates.mjs --residue packages/spec/scripts/lib/dist-freshness.ts: rosters inside the silent bucket 29 of those 123 to 34 of those 123, inverted rosters 3 to 5, and check:export-origins gains the inverted-silence note it should always have had.

Generated by Claude Code


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 15:43
@os-project-manager
os-project-manager added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 52ef672Aug 31, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13520-artifact-only-silence-classifier branch August 31, 2026 16:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(pm): route the artifact-roster classifier through the file's own resolver - #13862

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier
Aug 31, 2026
Merged

fix(pm): route the artifact-roster classifier through the file's own resolver#13862
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13520

artifactOnlySilence decides "every declared literal collapses to a tracked FILE". It did that with trackedFiles.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:

readerhow it answered
hintCovers — the covering rulefollows a dropped extension through MODULE_SPECIFIER_EXTENSIONS (#12514)
extensionlessModuleTargetnames the file such a specifier resolves to (#12299)
artifactOnlySilencetrackedFiles.has(collapseHint(h)) — followed neither

The file already refuses exactly this drift elsewhere, in its own words: extractWatchHints was denied the same widening because it would hand the file "a SECOND answer to a question extensionlessModuleTarget already 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:

  1. One owner.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. artifactOnlySilence asks 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.
  2. The parameter that made the mistake possible is gone. The predicate took a bare Set of files — an input that cannot answer the question. It now takes the watchHintTree bundle and refuses a bare set loudly, because that is precisely the shape that mis-categorises silently.
  3. A class guard over the live fleet, at family grain (self-test): for every discovered family, artifactOnlySilence returns a roster exactly when every declared literal of that family names exactly one tracked file under hintCovers, and the roster IS those files. Both sides are computed by different code — a full sweep of the tracked corpus through hintCovers on one side, the membership test plus resolver on the other. The day someone teaches hintCovers a 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; only coversYourPath reads the card's paths):

beforeafter
artifact-roster families3039 (+9, 0 lost)
existing rosters whose membership or dir moved0
declared literals where the covering rule and the classifier disagree400
families where the covering rule and the classifier disagree90

The nine that move, each named by the sweep with the file its specifier resolves to:

check:api-surface packages/spec/scripts/lib/{dist-freshness,sharded-artifacts}
check:dual-source-exports packages/spec/scripts/lib/dist-freshness
check:export-origins packages/spec/scripts/lib/{export-origins-layout,sharded-artifacts}
check:exported-any packages/spec/scripts/lib/dist-freshness
check:meta-url-spelling packages/spec/src/{kernel,meta-spelling}/... (3)
check:react-blocks packages/spec/src/ui/react-blocks + 3 (no common directory)
check:react-declaration-parity packages/spec/src/ui/{react-blocks,component.zod,page.zod}
check:spec-changes packages/spec/src/{kernel,migrations}/... + scripts/lib (4)
check:strictness-ledger packages/spec/scripts/lib/{strictness-ledger,strictness-ledger-doc}

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-blocks and check:react-declaration-parity are outside the packages/spec/scripts/lib cluster 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: extensionlessModuleTarget refuses 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 bucket 29 of those 123 to 34 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"3 to 5.

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, null exactly. Proven by ablation — restoring the shipped per-literal rule inside artifactOnlySilence, with the mutation confirmed on disk and the restore confirmed by blob hash:

  • a roster of extensionless specifiers is an artifact ROSTER, not an ordinary silence
  • the category is pinned by its contents — the exact resolved file list
  • under the directory those files really sit in, not the one the specifier stops short at
  • and for a card in that directory it is the INVERTED silence
  • a roster mixing both spellings resolves to the same category
  • the fleet-wide family-grain agreement (the class guard above)

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 declaredFileTarget against hintCovers — 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 :2628 and :2746):

  • :2628"No spelling of collapseHint can fix this, which is why the branch is here" — inside globInNonFinalSegment's docblock, about a glob in a non-final segment mangling under collapse-by-deletion, in hintCovers. Not this branch, not this predicate.
  • :2746"a question extensionlessModuleTarget already owns" — inside the "Why COMPARISON and not EXTRACTION" argument, about refusing to teach extractWatchHints to 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

  • hintCovers already follows the dropped extension (hintCovers reads an extensionless module specifier as a filesystem path, so 9 packages/spec gate 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 --residue output: the dead-hint sweep marked every literal of check:export-origins as 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.
  • derive built a third copy of the tracked-file set (new Set(swept)) beside the one watchHintTree had 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 is private: true and declares no files array; nothing under scripts/ is published from any package, and this tool is internal PM tooling invoked by lint.yml. This PR releases nothing, which is the label's own criterion.

Generated by Claude Code


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Verification — commands, and each gate's own verdict line

Final commit 4eaee4a0e. Every heavy run went through scripts/pm/os-verify-lock.sh; exit codes were captured before any pipe.

The standing instrument for this file, at the final head

pnpm check:pm-dispatch-gates
-> dispatch-gates self-test: 1060 cases pass.
-> os-verify-lock: VERDICT command-exit 0 · held the lock 368s · waited 0s

The ablation — the category pin proven, not asserted. No build leg exists: dispatch-gates.mjs is a plain .mjs module loaded from source, so there is no dist/ for a mutation to fail to reach. The mutation restores the shipped per-literal rule inside artifactOnlySilence and nothing else.

mutation on disk anchor count 1 before; injected 1 (want 1), deleted 0 (want 0)
blob e307514375f49c4bee66609c7763292a38bb325b -> 5391ff5e324c25af1b4139efa89017e9997c77c9
result dispatch-gates self-test: 7 of 1060 case(s) failed
a roster spelled as extensionless module specifiers is an artifact ROSTER...
...and the CATEGORY is pinned by its contents: the tracked files those specifiers name
...under the directory those files really sit in...
...and for a card in that directory it is the INVERTED silence...
a roster mixing both spellings of the same claim resolves to the same category
the roster classifier and the covering rule agree about every family in the fleet (192 families)
...non-vacuously: the fleet carries families whose roster is named only through a dropped extension
restore git checkout HEAD -- ABSOLUTE_PATH under a trap; hash back to e307514375f4...,
git diff HEAD empty, git status --porcelain empty

Gate union — derived, never hand-listed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths, so the script reads its own changed set off the merge base; re-derived unchanged after git fetch origin main. 14 families, all green:

check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pm-dispatch-gates · check:pnpm-filter-targets · check:watch-hint-literal · check-ci-filter-parity (OK: all 129 declared cross-package glob(s) ... are covered) · check-cross-package-test-inputs · check-self-test-wired · check-shard-attestation (2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s))

Outside the derivation, because no path derivation names it: pnpm check:ratchet-remedy-authority — exit 0. Also pnpm check:nul-bytes — exit 0.

NOT MEASURED, and not red.check-test-completeness exits 3 with PREREQUISITE NOT MET — it grades a saved turbo run test log and none was named; its own text says the family list names it with no argument and the local reading is NOT MEASURED. check-ci-filter-parity and check-shard-attestation first exited 1 with PREREQUISITE NOT MET — the dependency yaml is not installed (the worktree had no node_modules yet); after pnpm install both exit 0, as recorded above.

The script's own suite is its --self-test, run above. git grep over test files for dispatch-gates finds only two prose mentions inside comments (packages/cli, packages/create-objectstack) — neither executes it.

Live output, before vs after, on node scripts/pm/dispatch-gates.mjs --residue packages/spec/scripts/lib/dist-freshness.ts: rosters inside the silent bucket 29 of those 123 to 34 of those 123, inverted rosters 3 to 5, and check:export-origins gains the inverted-silence note it should always have had.

Generated by Claude Code


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 15:43
@os-project-manager
os-project-manager added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 52ef672Aug 31, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13520-artifact-only-silence-classifier branch August 31, 2026 16:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(pm): route the artifact-roster classifier through the file's own resolver - #13862

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier
Aug 31, 2026
Merged

fix(pm): route the artifact-roster classifier through the file's own resolver#13862
os-project-manager merged 2 commits into
mainfrom
claude/issue-13520-artifact-only-silence-classifier

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#13520

artifactOnlySilence decides "every declared literal collapses to a tracked FILE". It did that with trackedFiles.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:

readerhow it answered
hintCovers — the covering rulefollows a dropped extension through MODULE_SPECIFIER_EXTENSIONS (#12514)
extensionlessModuleTargetnames the file such a specifier resolves to (#12299)
artifactOnlySilencetrackedFiles.has(collapseHint(h)) — followed neither

The file already refuses exactly this drift elsewhere, in its own words: extractWatchHints was denied the same widening because it would hand the file "a SECOND answer to a question extensionlessModuleTarget already 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:

  1. One owner.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. artifactOnlySilence asks 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.
  2. The parameter that made the mistake possible is gone. The predicate took a bare Set of files — an input that cannot answer the question. It now takes the watchHintTree bundle and refuses a bare set loudly, because that is precisely the shape that mis-categorises silently.
  3. A class guard over the live fleet, at family grain (self-test): for every discovered family, artifactOnlySilence returns a roster exactly when every declared literal of that family names exactly one tracked file under hintCovers, and the roster IS those files. Both sides are computed by different code — a full sweep of the tracked corpus through hintCovers on one side, the membership test plus resolver on the other. The day someone teaches hintCovers a 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; only coversYourPath reads the card's paths):

beforeafter
artifact-roster families3039 (+9, 0 lost)
existing rosters whose membership or dir moved0
declared literals where the covering rule and the classifier disagree400
families where the covering rule and the classifier disagree90

The nine that move, each named by the sweep with the file its specifier resolves to:

check:api-surface packages/spec/scripts/lib/{dist-freshness,sharded-artifacts}
check:dual-source-exports packages/spec/scripts/lib/dist-freshness
check:export-origins packages/spec/scripts/lib/{export-origins-layout,sharded-artifacts}
check:exported-any packages/spec/scripts/lib/dist-freshness
check:meta-url-spelling packages/spec/src/{kernel,meta-spelling}/... (3)
check:react-blocks packages/spec/src/ui/react-blocks + 3 (no common directory)
check:react-declaration-parity packages/spec/src/ui/{react-blocks,component.zod,page.zod}
check:spec-changes packages/spec/src/{kernel,migrations}/... + scripts/lib (4)
check:strictness-ledger packages/spec/scripts/lib/{strictness-ledger,strictness-ledger-doc}

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-blocks and check:react-declaration-parity are outside the packages/spec/scripts/lib cluster 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: extensionlessModuleTarget refuses 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 bucket 29 of those 123 to 34 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"3 to 5.

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, null exactly. Proven by ablation — restoring the shipped per-literal rule inside artifactOnlySilence, with the mutation confirmed on disk and the restore confirmed by blob hash:

  • a roster of extensionless specifiers is an artifact ROSTER, not an ordinary silence
  • the category is pinned by its contents — the exact resolved file list
  • under the directory those files really sit in, not the one the specifier stops short at
  • and for a card in that directory it is the INVERTED silence
  • a roster mixing both spellings resolves to the same category
  • the fleet-wide family-grain agreement (the class guard above)

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 declaredFileTarget against hintCovers — 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 :2628 and :2746):

  • :2628"No spelling of collapseHint can fix this, which is why the branch is here" — inside globInNonFinalSegment's docblock, about a glob in a non-final segment mangling under collapse-by-deletion, in hintCovers. Not this branch, not this predicate.
  • :2746"a question extensionlessModuleTarget already owns" — inside the "Why COMPARISON and not EXTRACTION" argument, about refusing to teach extractWatchHints to 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

  • hintCovers already follows the dropped extension (hintCovers reads an extensionless module specifier as a filesystem path, so 9 packages/spec gate 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 --residue output: the dead-hint sweep marked every literal of check:export-origins as 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.
  • derive built a third copy of the tracked-file set (new Set(swept)) beside the one watchHintTree had 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 is private: true and declares no files array; nothing under scripts/ is published from any package, and this tool is internal PM tooling invoked by lint.yml. This PR releases nothing, which is the label's own criterion.

Generated by Claude Code


Generated by Claude Code

…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
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

Verification — commands, and each gate's own verdict line

Final commit 4eaee4a0e. Every heavy run went through scripts/pm/os-verify-lock.sh; exit codes were captured before any pipe.

The standing instrument for this file, at the final head

pnpm check:pm-dispatch-gates
-> dispatch-gates self-test: 1060 cases pass.
-> os-verify-lock: VERDICT command-exit 0 · held the lock 368s · waited 0s

The ablation — the category pin proven, not asserted. No build leg exists: dispatch-gates.mjs is a plain .mjs module loaded from source, so there is no dist/ for a mutation to fail to reach. The mutation restores the shipped per-literal rule inside artifactOnlySilence and nothing else.

mutation on disk anchor count 1 before; injected 1 (want 1), deleted 0 (want 0)
blob e307514375f49c4bee66609c7763292a38bb325b -> 5391ff5e324c25af1b4139efa89017e9997c77c9
result dispatch-gates self-test: 7 of 1060 case(s) failed
a roster spelled as extensionless module specifiers is an artifact ROSTER...
...and the CATEGORY is pinned by its contents: the tracked files those specifiers name
...under the directory those files really sit in...
...and for a card in that directory it is the INVERTED silence...
a roster mixing both spellings of the same claim resolves to the same category
the roster classifier and the covering rule agree about every family in the fleet (192 families)
...non-vacuously: the fleet carries families whose roster is named only through a dropped extension
restore git checkout HEAD -- ABSOLUTE_PATH under a trap; hash back to e307514375f4...,
git diff HEAD empty, git status --porcelain empty

Gate union — derived, never hand-listed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths, so the script reads its own changed set off the merge base; re-derived unchanged after git fetch origin main. 14 families, all green:

check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pm-dispatch-gates · check:pnpm-filter-targets · check:watch-hint-literal · check-ci-filter-parity (OK: all 129 declared cross-package glob(s) ... are covered) · check-cross-package-test-inputs · check-self-test-wired · check-shard-attestation (2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s))

Outside the derivation, because no path derivation names it: pnpm check:ratchet-remedy-authority — exit 0. Also pnpm check:nul-bytes — exit 0.

NOT MEASURED, and not red.check-test-completeness exits 3 with PREREQUISITE NOT MET — it grades a saved turbo run test log and none was named; its own text says the family list names it with no argument and the local reading is NOT MEASURED. check-ci-filter-parity and check-shard-attestation first exited 1 with PREREQUISITE NOT MET — the dependency yaml is not installed (the worktree had no node_modules yet); after pnpm install both exit 0, as recorded above.

The script's own suite is its --self-test, run above. git grep over test files for dispatch-gates finds only two prose mentions inside comments (packages/cli, packages/create-objectstack) — neither executes it.

Live output, before vs after, on node scripts/pm/dispatch-gates.mjs --residue packages/spec/scripts/lib/dist-freshness.ts: rosters inside the silent bucket 29 of those 123 to 34 of those 123, inverted rosters 3 to 5, and check:export-origins gains the inverted-silence note it should always have had.

Generated by Claude Code


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 15:43
@os-project-manager
os-project-manager added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 52ef672Aug 31, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13520-artifact-only-silence-classifier branch August 31, 2026 16:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude