fix(pm): recognise a population declared as a lowercase object property - #17113
Merged
os-trump merged 4 commits intoSep 9, 2026
Merged
Conversation
`bare-root-worklist.mjs` recognised a gate's declared population by the NAME of the `const` holding it, so a population spelled as an object property — `roots: [...]` inside a config literal — could never match. The auditor built to find undeclared bare-root populations was blind to one that was declared in plain sight. Measured on 91f65c4: the property shape adds exactly 3 rows, including the one this card was filed from. Dispositions follow in the next commits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
`scripts/pm/bare-root-worklist.mjs` recognised a gate's declared population by the NAME of the `const` holding it, uppercase only, so a population spelled as an object property could never match. `check-doc-route-spelling.mjs` declares `roots: ['content/docs', 'skills']` inside `const REAL_CONFIG`, so its `skills` root was invisible to the very instrument built to find undeclared roots — not a different defect, the same one spelled differently. `POPULATION_PROPERTY` mirrors `POPULATION_CONSTANT` term for term, the camelCase hump doing the work the literal underscore does there; the bare singular `root`/`dir` stay out for the reason `ROOT`/`DIR` do. Dispositions for the three rows it surfaces, measured on 91f65c4: - check-doc-route-spelling.mjs declares `skills/**` (46 of 47 tracked files), the idiom two sibling doc gates already carry at this root; the row is now REACHABLE and the family stops being Silent for skills-only cards. - the two check-console-injection.mjs rows are `join()` path components in a gate that never walks the root; recorded in UNJUDGED with measured notes, which is what a widening pass owes and all it may write. - the UNJUDGED shared base moved 66e68ad -> 91f65c4, re-read rather than re-judged; one cited line number corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…c-route-spelling-population-shape
Review finding on #17113: the comment beside the property scan claimed the leading character class kept BOTH `a ? roots : dirs` and a `foo.roots:` member expression from reading as a declaration. Only the second is true — the `?` is followed by a space and `\s` is in the class, so the ternary matches while the tight `a?roots:dirs` does not, the inversion of what a reader expects. A `roots:` label statement matches too. The comment now states that as a measurement, and the severity is stated with it rather than assumed: `sweep` admits a literal only when a span CONTAINS it, so a span is an inclusion filter and a spurious one can only ADD a row, never hide one. Measured three ways, and bounded on the whole tree by this widening's own row-set diff (3 surfaced, 0 gone, unchanged by this commit). Six new self-test cases pin all of it: the member-expression exclusion that holds, the ternary and label matches recorded as known rather than accidental, and the admission direction in both directions plus the span-order tie-break. ⛔ The regex is NOT tightened. That is a second change to what this recogniser matches and would owe its own full row-set diff — the ride-along this file refuses by name for the `const` half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
os-trump
marked this pull request as ready for review
September 9, 2026 11:03
os-trump
enabled auto-merge
September 9, 2026 11:03
os-trump
deleted the
claude/issue-17057-doc-route-spelling-population-shape
branch
September 9, 2026 11:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #17057
The defect
scripts/pm/bare-root-worklist.mjs— the auditor whose job is to find undeclaredbare-root populations in repo gates — recognised a gate's declared population by
the name of the
constholding it, uppercase only:scripts/check-doc-route-spelling.mjsdeclares its population as a lowercaseobject property instead —
roots: ['content/docs', 'skills']insideconst REAL_CONFIG = { … }— so itsskillsroot was invisible to the veryinstrument built to find it. Not a different defect; the same one, spelled
differently.
Both premises re-verified on
origin/main91f65c4eabefore any edit.What this PR does — fix (1), widen the recognition
A new
POPULATION_PROPERTYmirrorsPOPULATION_CONSTANTterm for term, withthe camelCase hump doing the work the literal underscore does there:
[A-Z0-9_]*_ROOTS?[a-z][A-Za-z0-9]*Roots?scanRoots,docRoot[A-Z0-9_]*_DIRS?[a-z][A-Za-z0-9]*Dirs?searchDirs[A-Z0-9_]*_SCOPE[a-z][A-Za-z0-9]*ScopelintScopeROOTS·DIRSroots·dirsPOPULATIONpopulationThe bare singular
root/dirstay out, for the reasonROOT/DIRstayout of the uppercase form and with more force at this casing:
rootis thecommonest property name in this tree for one repo-root path fragment, the exact
thing the restriction exists to exclude.
populationSpansnow returns both shapes as one list, so no caller holds aroster of them. The
constscanner is byte-for-byte unchanged — the propertyvalue is delimited by a separate depth-tracking scanner — so the before/after
below has exactly one variable in it.
Fix (2) (renaming the instance to
const SCAN_ROOTS) was refused, per thecard's ruling: it makes today green and leaves the auditor's count silently
wrong in a direction no reader of its output can infer.
The measurement, and the positive control
Exactly 3 newly-surfaced rows, 0 gone. Full row-set diff:
⭐ The positive control the card fixed in advance holds: under the widened
recognition the sweep re-finds
check-doc-route-spelling.mjs's ownroots:.The instrument is not answering zero because it cannot answer yes. All three
rows are production config, none in a self-test fixture (
maskSelfTestsblanksthe fixture copy of the same literal at :787+). 3 ≤ 3, so this PR carries the
dispositions.
The PM's line-anchored grep lead (22 hits over
scripts/**, one productioninstance) under-counted by construction, as it said it would: it names one
production instance, the widened auditor finds three.
Dispositions for the three rows
1.
check-doc-route-spelling.mjs roots skills→ DECLARED, now REACHABLE.The gate gains
const ROOT_DIR_WATCH_HINTS = ['skills/**'], the idiomcheck-corpus-claim-drift.mjsandcheck-doc-authoring.mjsalready carry atthis same root for this same reason —
content/docscarries a separator andreaches the hint set on its own,
skillsdoes not. This closes the card'ssecond half: the family no longer sits
Silentfor a skills-only card.Measured: of the 47 files tracked under
skills/on91f65c4eathe walk admits46 (every
.md/.mdxat any depth, nothing skipped), so the declarationover-names by exactly one
.jsonfile — 97.9% precise. Pinned toREAL_CONFIG.rootsin both directions by four new self-test cases in theWalk wiringbattery, so neither side can drift.2 & 3. The two
check-console-injection.mjsrows → UNJUDGED, with measured notes.Both literals are
join()path components at :940-:941(
path.join(ROOT, 'packages', 'console', 'dist')and…, 'packages', 'spec'));the gate never walks that root — it opens an index file, a stamp, that dist's
own
assetschildren (non-recursive) and the spec package's manifest and builtJS. ⛔ This PR does not write
TRIAGEverdicts for them:UNJUDGED's owndocblock binds a recogniser-widening pass to record the measurement and leave
the judgement to the maintainer, which is exactly what #15468's widening did
before it. Recording them is what keeps
--self-testhonest rather than green.The UNJUDGED shared base moved
66e68adc6→91f65c4ea. The bucket pins itsentries to ONE base, so a second widening cannot append without re-reading every
note already there — that cost is the pin's design, not an obstacle. Seven of the
eleven inherited entries name gate sources byte-identical across those two
commits (compared by blob id); of the two files that moved, the live-db-isolation
gate changed comment prose with no net line movement, and the doc-frontmatter
gate grew above the line its note cites — the one note whose text this pass
corrected (
:436→:446). Nopopulationboolean and no recorded reason wasotherwise rewritten.
Prove the change can fail — ablation on the exact instrument
Predicted before running: neutering
POPULATION_PROPERTYalone must return thesweep to the
origin/mainbaseline and drop all three rows.78/44 under ablation is the pristine
origin/mainbaseline to the row — andit is measured with the new
skills/**declaration still on disk, so thedeclaration alone moves nothing here; the recogniser is what surfaces the rows.
Mutation and restore ran under
trap … EXIT INT TERMwith absolute paths;restoration is proven by an empty
git diff HEADand blob equality againstthe HEAD blob, never by an exit code.
was a silent no-op — a
perl -0pisubstitution that matched nothing and exited0. The script's own on-disk landing check caught it and refused to publish a
reading; no number from that attempt was used. The run above is the re-anchored
one.
What was measured, and what was not
Derived gates — 33 derived / 33 run / 0 NOT-MEASURED / 0 UNRUN, reconciled by
the tool rather than by arithmetic over my own list:
Derived with
--repo objectstack-ai/objectstack(it holds against thischeckout's remote) on a tree merged up to
origin/main f87fdf359, so no STALETREE caveat applies. Change set: exactly the two files below.
Also run, because the derivation flags their silence as evidence in neither
direction (their artifact roster sits under a directory one of my paths is in):
check-published-list-mirrors.mjs(+--self-test),check:pm-label-desc-cap,check:console-injection— all exit 0.Repo-wide lint: run in full, no narrowing claimed.
pnpm lint(
eslint . --no-inline-config) exit 0 in 83s atdc2cba197(and 81s at5e5df1e13before the review fix). Scoped--format jsonover the two changed files: 2 files, 0 errors, 0 warnings.Control characters:
pnpm check:nul-bytesgreen, plus a directgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over both changed files — no match.Not measured / not owed, stated rather than implied:
git grepover*.test.ts/*.spec.tsis empty); their test suites are their--self-testentry points, both in the 33 above and both green.
always-runs tail sit outside the derived total by construction. CI runs them.
Changeset
skip-changeset, and the reason is read off the tree rather than asserted: thewhole diff is repo-local
scripts/**, rootpackage.jsonisprivate: true,and no package's
filesarray namesscripts/(checked across every workspacemanifest). The label is already applied to this PR.
Clause-②: no. No exported symbol was added to a published package, no key to
a published payload, and no
packages/spec/src/**path is touched.Review round 2 — a comment that claimed more than the regex delivers
The finding is real and reproduced independently. The comment beside the
property scan asserted that the leading character class kept both
a ? roots : dirsand afoo.roots:member expression from reading as adeclaration. Calling
populationSpansdirectly:const y = { a: cfg.roots };.is not in the classconst x = a ? roots : dirs;?is followed by a space and\sis in the classconst x = a?roots:dirs;roots: for (const a of b) { break roots; }const CFG = { roots: ['skills'] };Taken: option 1 (correct the comment) plus the pins. The regex is NOT tightened.
⭐ One correction to the review's severity premise, measured rather than argued.
The review reasoned that a spurious span marks a region as declaring a
population, so a literal inside it "drops out of the worklist" — a false
negative, the swallowing direction.
sweepdoes the opposite: it admits aliteral only when a span contains it (
if (!span) continue), which makes aspan an inclusion filter. Measured three ways, by splicing synthetic families
over throwaway fixtures (removed under a trap;
git statusclean after):So a spurious span can only ADD a row — the loud direction, where the
UNJUDGEDset-equality coupling forces someone to judge it — and can never hideone; the third reading shows it cannot even steal a row's key, because the
constant half is pushed first and
findtakes the first match. The whole-treebound is unchanged and re-measured on this head: 3 surfaced, 0 gone.
That does not soften the finding — a comment that misdescribes its own regex is
exactly what this repo spent the morning deleting — but it does decide the fix.
Why not tighten. Excluding the ternary needs a look-behind past whitespace
for a
?; excluding the label form needs real parsing and cannot be done atall. So a tightening would (a) be a second change to what this recogniser
matches, owing its own full row-set diff proving it surfaces nothing and hides
nothing — the ride-along
propertyValueEnd's docblock refuses by name for theconsthalf — and (b) still leave the label form matching, so it would not evenretire the caveat. The comment now states the bound instead of promising a
guarantee.
Six new self-test cases pin every part of it so it cannot drift back: the
member-expression exclusion that does hold; the ternary match and its tight-
spelling inversion; the label match; and the admission direction in both
directions plus the span-order tie-break.
⛔ RECORDED, not accidents to be quietly tightened awayis written beside them.Re-verified on this head:
--self-testgreen, row-set diff unchanged(3 surfaced / 0 gone), 33 derived / 33 run / 0 NOT-MEASURED / 0 UNRUN,
pnpm lintgreen, scoped eslint 2 files / 0 errors / 0 warnings.exit 3PREREQUISITE NOT MET results, because the worktree had been torn downand recreated without
pnpm install. Those are NOT MEASURED, never passes —pnpm installthen a full re-run of all 33 is what the numbers above come from.Files
scripts/pm/bare-root-worklist.mjs— the widening, its self-test pins, thethree dispositions and the re-measured UNJUDGED base.
scripts/check-doc-route-spelling.mjs— theskills/**declaration and itsfour coupling pins.
Generated by Claude Code