Uh oh!
There was an error while loading. Please reload this page.
fix(pm): follow a final-segment basename glob in hintCovers, and stop the residue naming a move that never happened - #13783
Merged
os-project-manager merged 1 commit intoAug 31, 2026
Conversation
…aming a move that never happened `collapseHint` collapses a glob by deleting it. That is a sound reduction when nothing but glob characters follow (`packages/**` -> `packages`), and a SPLICE when a literal does: `.changeset/*.md` -> `.changeset/.md`, a path no tree can hold. So the hint matched nothing BY CONSTRUCTION while reading as an ordinary literal, against 548 tracked changesets. `zeroSegmentForms` recorded this exact species one branch over and left it; it is the same defect, splicing inside a segment rather than across a separator. The second half is why it was worth a card. `deepestTrackedPrefix`, `unreachableClass` and `unreachableReason` all reasoned from `collapseHint` UNCONDITIONALLY, including for the hints `hintCovers` had already stopped judging that way. A pattern-judged hint can never equal its own collapsed splice (`.changeset` is not `.changeset/.md`), so "the tree stops at X; the layout moved under it" was the only reachable sentence for the whole shape class -- a specific wrong cause, printed under the heading that tells a reader to go chase it. Repairing the matcher alone would have retired today's five instances and left the derivation that mints them intact. - `globCarriesLiteralSuffix` -- a `*` in the final segment with a literal behind it. A literal in FRONT of the glob is not this case. - `judgedAsPattern` -- the two splice shapes as one question, because two call sites need the same answer and a second copy of it is the drift this file refuses everywhere else. - `comparedForm` -- the form `hintCovers` ACTUALLY judged the hint by. The three reason branches now mean what they say for both comparison modes. Measured at 4301f78 over 191 families x 749 distinct hints x 7588 tracked files: one hint in the whole fleet carries the shape; (hint, file) pairs 35275 -> 35823 and (gate, file) pairs 140716 -> 143456, ZERO lost; residue notes asserting a layout move for it 5 -> 0. ZERO cards gain a family and zero lose one -- all five owners also declare the bare `.changeset`, so the brief was already naming them through the sibling literal. 548 (family, card) pairs re-attribute to a more precise key in the one owner that spells the glob first. `?`, `+` and character classes are deliberately NOT admitted (zero live instances) and the self-test reds on their arrival. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
This was referenced Aug 31, 2026
os-project-manager
marked this pull request as ready for review
August 31, 2026 12:16
Uh oh!
There was an error while loading. Please reload this page.
os-project-manager
deleted the
claude/issue-13448-hintcovers-basename-glob
branch
August 31, 2026 12:37
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#13448
hintCoverscould not follow a final-segment basename glob, and the residueprinter then stated a specific wrong cause for the deadness it produced. Both
halves land here, in one change, as the triage ruling requires.
The defect, one line each
Half 1 — coverage.
collapseHintcollapses a glob by deleting it. That is asound reduction when nothing but glob characters follow (
packages/**->packages,packages/client*->packages/client). When a literal follows theglob it is a splice:
.changeset/*.md->.changeset/.md, a path no tree canhold. The hint matched nothing BY CONSTRUCTION while reading as an ordinary literal.
zeroSegmentForms' docblock recorded this exact species one branch over(
scripts/*.d.mts) and left it as "a different species"; it is the same defect,splicing inside a segment rather than across a separator.
Half 2 — the false reason.
deepestTrackedPrefix,unreachableClassandunreachableReasonall reasoned fromcollapseHintunconditionally, includingfor hints
hintCovershad already stopped judging that way. A pattern-judged hintcan never equal its own collapsed splice (
.changesetis not.changeset/.md), sodeepest !== collapseHint(hint)was always true for the whole shape class, and"the tree stops at
.changeset; the layout moved under it" was the onlyreachable sentence. The layout never moved. A reader acting on that string goes
hunting a directory rename that never happened.
The fix
scripts/pm/dispatch-gates.mjs— three new exported predicates, four call sites:globCarriesLiteralSuffix(hint)— a*in the final segment with a literalbehind it. What decides it is a literal behind the glob, never one in front:
scripts/check-*.mjsyes,scripts/check-*no.judgedAsPattern(hint)=globInNonFinalSegment(hint) || globCarriesLiteralSuffix(hint)— the two splice shapes asked as one question, because two call sites need the
same answer and a second copy of it is the drift this file refuses everywhere else.
hintCoversroutes on it into the existingzeroSegmentForms/triggerCoversbranch: not a second matching language, the one already there for this exact job.
comparedForm(hint)— the formhintCoversactually judged the hint by:collapseHintfor a collapse-judged hint, the literal prefix for apattern-judged one.
deepestTrackedPrefixwalks it,unreachableClasscomparesagainst it, and
unreachableReasongets a branch that speaks in it.The new reason is checkable by the reader, which is the bar a triage lead has to
clear:
...and the exception stays exactly that narrow — a pattern whose own literal prefix
really is gone still reads as a move:
A2.1 — the changeset count, re-measured at my own ref
The number moves with every merged PR, which is exactly why it must not be quoted
from anyone:
c42bc8ee64301f7846+ the fixFour readings on four days over one directory is itself part of the cost: a silent
zero over a population that grows with every merge is the one you notice last.
A2.2 — per-shape census, before/after, rather than "widen until it looks glob-ish"
Every distinct hint in the fleet, classified by glob shape and measured against the
tracked corpus. 191 families x 749 distinct hints x 7588 tracked files at
4301f7846, before = the unfixed tool run from anorigin/mainworktree over theidentical corpus:
**(trailing)*(trailing)*with a literal SUFFIX — the defectpackages/client*, the DECIDED trade)?,+or a character class, anywhere in the fleetExactly one hint in the whole fleet carries the missed shape.
**/prefixes,mid-segment wildcards and whole-
**segments were already routed throughtriggerCoversand are untouched. There are zero hints carrying?,+or acharacter class, so those are deliberately not admitted:
collapseHintneverdeleted them, so they are an ordinary literal that fails to match — the missing-lead
direction this file errs in — not a mangle. Admitting them would be a fabricated-lead
widening bought for no live instance; the self-test reds on their arrival instead.
Blast radius, both directions:
The
ROOT_DIR_WATCH_HINTSidiom is bit-for-bit unchanged —packages/*5631,examples/*243,skills/**50,content/**442,scripts/**299,packages/**5631, all identical before and after — and the DECIDED
packages/client*partial-segment trade is a TRAILING glob, so it is not this case and stays pinned in
both directions.
A2.3 — which cards gain which families: ZERO, and that is the finding
The card and the triage ruling both expected the matched column to move —
"Correcting
hintCoversmakes those families match more cards — a live changeto which gates a dispatch brief names, not a display change", and triage's
⇒ **这不是显示缺陷,是覆盖缺陷。** 今天派发简报**漏点**了本该跑的 changeset 门族.Measured over every tracked file read as a one-file card surface: 0 cards gain a
family, 0 cards lose one.
The reason is only visible in the census: all five owners of
.changeset/*.mdalsodeclare the bare literal
.changeset, whichhintCovershas always matchedagainst every file beneath it. A card touching a changeset was already being told all
five families — through the sibling literal, not through the glob.
.changeset.changeset.changeset/*.mdcheck:changeset-gate-self-testsscripts/check-adr-0087-registration.mjsscripts/check-changeset-no-major.mjsscripts/check-empty-changeset.mjsscripts/pm/release-rehearsal-clone.mjs --self-testOne thing does move, and it is a printed key, not a verdict: 548 (family, card)
pairs re-attribute from
.changesetto.changeset/*.md, all of them inrelease-rehearsal-clone.mjs --self-test, the one owner that spells the glob beforethe bare literal. Same family, same
gate sourceprovenance, strictly more precisekey — the pattern that actually matches rather than the directory it sits in. Every
other owner keeps the exact key it printed. Disclosed rather than buried because
coveringKey's docblock prices re-attribution explicitly.So the live cost of this defect was never an under-named brief. It was the false
reason — 5 residue notes asserting a directory rename -> 0 — plus a single point of
failure nobody could see: the moment any of those five gates spells its population as
the glob alone, its coverage vanishes silently. That is not hypothetical.
scripts/*.d.mtsis the second, already-live instance (0 -> 4 tracked files), and itwas pinned in this file's own self-test as "still dead" until this change.
against 548 tracked changesets, and the residue really did name a false cause; both
are repaired here. What the measurement falsifies is the blast-radius claim the p1
rationale rested on. Worth re-reading the priority against that, which is a call for
triage and not for me.
A2.4 —
unreachableReasonis a reason SET, and its reasons did not line upNot a single hard-coded string: four branches (extensionless-module target / no
tracked first segment / tree-has-it-but-too-generic / layout moved). The mismatch is
one level up —
hintCovershas two comparison modes and the reason set coveredone. Every branch derived from
collapseHint, so for the pattern-judged shapes therenderer was reasoning about a string the comparison never looked at, and one branch
was therefore unreachable while another was unconditional.
That is why the repair is
comparedFormand not a reworded branch: reword it and thenext hint of this shape mints the same falsehood. A2.4's own test — "a reader
following the stated reason now finds the real cause" — is pinned rather than
asserted: the new sentence hands the reader the exact
git ls-filesinvocation thatreproduces the emptiness, and the self-test asserts the string contains it.
Tests
31 new self-test cases, in two blocks beside the ones they extend. The two pins that
previously asserted the defect are flipped rather than deleted, so the change of
mind is legible:
'the final-segment spelling of the same population is still dead'becomes
'the collapse ... is still a splice'+'...so the hint is judged as a pattern instead'+'...and now reaches every one of the files it names'.Read from the real corpus, not a fixture — a fixture cannot show that the tree
still holds the population the trap needs, and this one grows daily. Both directions
are pinned: the reach, the refusals (
packages/client*sibling still refused, theROOT_DIR_WATCH_HINTSidiom untouched,?/+/[…]still collapse-judged), and afleet-wide ratchet that reds if a
?,+or character-class hint ever arrives.Verification
Exit codes captured before any pipe; each verdict quoted from the line the gate
itself printed. Run on a shared, contended box (a docs dev server, two sibling agents'
gate runs), so heavy runs went through
scripts/pm/os-verify-lock.sh.The family was re-derived after the final commit, from the tool's own answer and
not from a hand-written list:
The whole union then ran at head
d56daafea, withgit status --shortempty atthe start and
HEADunchanged at the end (both recorded in the run log), so the greenbelow is about the tree this PR actually lands.
check:pm-dispatch-gates(the edited script's own suite)✓ dispatch-gates self-test: 1048 cases pass.scripts/pm/bare-root-worklist.mjs --self-test(the only importer ofhintCovers/collapseHint)OK self-test: 51 live row(s), 43 unreachable as spelled … every one of 9 distinct spelling(s) pinned LIVE, PRECISE and COMPLETE … in hintCovers' own termscheck:nul-bytesOK (scanned 7581 text file(s) … no raw ASCII control bytes)check:watch-hint-literal34 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the treecheck:entry-guard189 scripts/ file(s) — every entry guard goes through invoked-as.mjscheck:agent-test-spellingcheck:bash32-floor153 cases pass+24 tracked shell file(s)check:cli-command-ids39 cases pass+304 command-id literal(s) across 109 file(s)check:cross-package-test-inputsAll 117 self-test cases passed.+24 package(s) read outside themselves, all declaredcheck:parse-guardcheck:pnpm-filter-targets54+40assertions,140/179 --filter occurrence(s)resolvenode scripts/check-ci-filter-parity.mjsall 129 declared cross-package glob(s) (92 unique) are coverednode scripts/check-cross-package-test-inputs.mjsnode scripts/check-self-test-wired.mjsevery one of the 154 script(s) CI runs that ship a --self-test has that self-test run by CInode scripts/check-shard-attestation.mjs2 aggregate gate(s) count 3 declared leg(s)node scripts/check-test-completeness.mjspnpm lint(eslint . --no-inline-config, repo-wide)eslint --format json scripts/pm/dispatch-gates.mjserrorCount: 0,warningCount: 0check-test-completenessis NOT MEASURED, not red. Exit 3 is its documentedprerequisite branch, and the gate says so in its own words, naming this exact arrival:
CI passes it the teed log, so CI's reading is a real one and this PR does not change it.
Repo-wide
pnpm lintwas run in full rather than narrowed, so no narrowingargument is owed. For completeness, the invariance that would have backed one is real
and stated by the config itself (
eslint.config.mjslines 327-328: "never enablestype-aware linting (no
parserOptions.project, no typed@typescript-eslintrules)for ANY file"), so a one-file
.mjsedit cannot move an untouched file's verdict.Heavy runs went through
scripts/pm/os-verify-lock.shon a shared, contended box (adocs dev server plus two sibling agents' gate runs). Wall-clock figures are therefore
shared-box seconds, not idle-box ones:
check:pm-dispatch-gatesheld the lock 5m43safter waiting 2m06s.
Reverse verification
Both halves withheld in one leg, from the committed state, so the restore has a
real reference point. The two pin sets are disjoint by name, so attribution is
unambiguous without a second leg.
The mutation is proved on disk before anything is measured — exact-string
replacement, each of the four anchors required to hit exactly once, then counted
again after. A zero-hit edit exits 0 and leaves an unmutated file producing healthy
output, which reads exactly like a successful ablation:
RED, in the expected direction:
Restore proved by OBSERVED STATE, not by an exit code:
The restore leg is
git checkout HEAD -- ABSOLUTE_PATH, never a baregit checkout -- PATH(that reads from the index, which the mutation may havetouched) and never a relative path in the trap (a cwd that is no longer the repo root
silently leaves the tree mutated). An empty hash is treated as FAILURE, not as
"nothing to compare".
Does the fix change its own derived family? No.
The tool being fixed is the tool that derives this PR's own gate list, so it was run
from both sides on the same input path — an
origin/mainworktree (unfixed) andthis branch (fixed):
14 commands from each,
diffempty. Expected, and worth stating rather than assuming:the only coverage that moves is
.changeset/*.md, and this PR's diff contains no.changeset/path.Scope
One file:
scripts/pm/dispatch-gates.mjs. ⛔scripts/check-doc-anchors.mjsisuntouched — that is #13449's landing site, dispatched in the same wave to a different
dev. No governed surface, no
packages/spec/src/**, nocontent/docs/releases/.scripts/publishes nothing, so there is no changeset.Check Changesetinpr-automation.ymlcarries no path filter and no docs-only exemption — it enforces onevery PR and exempts only on the
skip-changesetlabel — so the label is applied hereand read back rather than waited for.
Generated by Claude Code