diff --git a/scripts/pm/bare-root-worklist.mjs b/scripts/pm/bare-root-worklist.mjs index 598569c0ef..b0e53de10a 100644 --- a/scripts/pm/bare-root-worklist.mjs +++ b/scripts/pm/bare-root-worklist.mjs @@ -113,6 +113,27 @@ const POPULATION_CONSTANT = /^(?:[A-Z0-9_]*_ROOTS?|[A-Z0-9_]*_DIRS?|POPULATION|[ * Every percentage below was measured on the tree, not estimated: numerator is * the files the gate's own walk filter admits, denominator the tracked files * under the subtree a declaration would name. + * + * ⚠️ A row's numbers date from the pass that WROTE that row, and the tree grows + * under all of them — so they are not comparable across rows, and a denominator + * here that disagrees with today's `trackedFiles()` is a stale reading, not a + * different population. The failure this warns against is the one that produced + * the `check-declaration-mirrors` row: its `why` was copied from the row above + * it and was wrong in BOTH terms (a recursive extension filter recorded as + * top-level-only, 2 files recorded as 115), and `--self-test` cannot catch it — + * it audits keys and verdicts, never what a `why` SAYS, which is correct, since + * a prose assertion cannot be mechanised. Only re-measuring catches this class. + * ⛔ So never carry a sibling's numbers into a new row, and ⛔ never refresh a + * denominator alone: pairing today's denominator with an older numerator mints a + * ratio nothing ever measured, which is this defect wearing fresher digits. + * + * Re-derived on 2026-08-25 from each gate's own exported walk, and current as of + * that tree: the two `scripts` rows (`corpusFiles()`, `mirrorFiles()`) and the + * three `check:runner-env-posture` rows (`collectFiles()`); the + * `check:skills-token-ratchet` row re-measured unchanged at 11 of 50. Every + * other row still carries the numbers from the pass that wrote it, because its + * gate exports no walk to drive and reproducing the filter by hand would be the + * estimate this docblock refuses. */ const TRIAGE = new Map([ // ── Taken: a strictly narrower subtree ──────────────────────────────────── @@ -179,12 +200,21 @@ const TRIAGE = new Map([ }], ['check:ratchet-remedy-authority SCRIPTS_DIR scripts', { verdict: 'REFUSE-UNSPELLABLE', - why: 'reads the TOP LEVEL of the root only, and only two extensions — 115 of 226 (51%). The ' - + 'idiom has no non-recursive spelling: a subtree hint claims every nested directory too', + why: 'reads the TOP LEVEL of the root only, and only two extensions (`.mjs` and `.mts`) — 144 ' + + 'of 261 (55%), re-derived from the gate own corpusFiles() walk. The idiom has no ' + + 'non-recursive spelling: a subtree hint claims every nested directory too', }], ['scripts/check-declaration-mirrors.mjs SCRIPTS_DIR scripts', { verdict: 'REFUSE-UNSPELLABLE', - why: 'same top-level-only shape, 115 of 226 (51%)', + why: 'a RECURSIVE walk admitted by EXTENSION — every `scripts/**/*.d.mts`, 2 of 261 (0.77%), ' + + 'read from the gate own mirrorFiles(). NOT the shape of the row above it, and measured ' + + 'here rather than inherited from it: mirrorFiles() descends into every nested directory ' + + 'and its own docblock says so. What cannot be spelled here is the EXTENSION filter, not a ' + + 'non-recursive walk — `scripts/**` is spellable and TRUE of this walk, and refused anyway ' + + 'because it would name this gate for 261 files to reach 2. Same class as the ' + + 'check:driver-conformance CASE_SETS_DIR and check:skills-token-ratchet SKILLS_DIR rows ' + + 'below, so lifting the row-above non-recursive limit would leave this one exactly as ' + + 'refused', }], // ── Refused: the population is a filter the idiom cannot spell ──────────── ['check:driver-conformance CASE_SETS_DIR packages', { @@ -275,16 +305,16 @@ const TRIAGE = new Map([ }], ['check:runner-env-posture SCANNED_ROOTS packages', { verdict: 'REFUSE-UNSPELLABLE', - why: 'non-test source beneath a `src` SEGMENT — 1757 of 5049 (35%). The segment is what makes ' + why: 'non-test source beneath a `src` SEGMENT — 1794 of 5185 (35%). The segment is what makes ' + 'this unspellable rather than merely wide: `packages/**/src/**` is the true population and ' + 'collapseHint reduces it to `packages`, so the only spellable claim also names every ' - + 'package manifest, changelog, fixture and the 2658 test files this gate deliberately skips. ' + + 'package manifest, changelog, fixture and the 2746 test files this gate deliberately skips. ' + 'Its nearest neighbour check:authz-resolver is REFUSE-WIDE at a similar 39% because ITS ' + 'population really is every non-test source under the root; this one is not', }], ['check:runner-env-posture SCANNED_ROOTS examples', { verdict: 'REFUSE-UNSPELLABLE', - why: '150 of 240 (63%), the same `src`-segment filter, refused with its packages half rather ' + why: '150 of 241 (62%), the same `src`-segment filter, refused with its packages half rather ' + 'than split: declaring the smaller root would name the gate on example cards and stay ' + 'silent on the package cards where product source actually lives', }], diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index e3bf812ece..5effadc0ec 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -224,6 +224,17 @@ export const CEILINGS = new Map([ // MERGED-tracking line, so that ceiling stays at 82 — no re-wrap, no cut. ['.claude/skills/pm-dispatch/references/release-aftercare.md', 58], ['.claude/skills/pm-dispatch/references/seat-post-protocol.md', 105], + // Per-repo「真绿」跑法索引 — the canonical test invocation, the gates a CI-log + // grep cannot see, and the local preflight, one fact per line per repo. Added + // by the protocol-text family PR, which could not carry its own ceiling: that + // dispatch declared a closed file surface (SKILL.md + os-dev.md + the new page + // + one pointer line) with stop-on-breach, and this script sat outside it. A + // pm-dispatch references file is read per seat session like every entry above, + // so its absence here was a coverage gap, not the header's deliberate omission + // (that one is the published `skills/` catalog, and only it). Set at the landed + // line count read from this ratchet's own run — headroom 0, same convention as + // the entries above. + ['.claude/skills/pm-dispatch/references/true-green.md', 34], // Lane job descriptions (maintainer ruling 2026-08-19: per-lane PM job // descriptions move from seat-post prose into versioned skill references). // Set at landed line counts (headroom 0, same convention as above). diff --git a/scripts/pm/ci-failure.mjs b/scripts/pm/ci-failure.mjs index 2895d413e3..ccff7f5922 100644 --- a/scripts/pm/ci-failure.mjs +++ b/scripts/pm/ci-failure.mjs @@ -291,13 +291,26 @@ import { describeProbe, needsRepoProbe, parseRemaining, + resolveSweepRepo, } from './check-half-states.mjs'; import { PROXY_FLAG, PROXY_REARM_GUARD, proxyRearmPlan } from './check-governed-merges.mjs'; import { isEntrypoint } from '../invoked-as.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(HERE, '..', '..'); -const OWNER_REPO = process.env.PM_SWEEP_REPO ?? 'objectstack-ai/objectstack'; +// Which board this file reads, resolved by the sweeper's own resolver rather +// than by a hardcoded default of its own: `PM_SWEEP_REPO` -> `GITHUB_REPOSITORY` +// (what Actions sets to the repo the workflow is INSTALLED IN) -> the literal +// default, a seat's terminal. The default the line used to carry is the one +// shape that reads as correct in every review — on a runner the override is +// normally unset, so a verbatim copy of this file in a sibling repo would read +// OBJECTSTACK's Actions API and report about THIS repo's CI while its caller +// believed it was reading its own. A wrong-repo answer is indistinguishable from +// a right one by its shape; only the run ids give it away, and nobody checks run +// ids against a repo they did not doubt. Same resolution order, same reasoning +// and same refusal below as `check-half-states.mjs` — see `resolveSweepRepo`. +const SWEEP_REPO = resolveSweepRepo(process.env); +const OWNER_REPO = SWEEP_REPO.repo; const API = 'https://api.github.com'; const TOKEN = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? ''; @@ -2427,6 +2440,21 @@ if (!invokedDirectly) { } else if (process.argv.includes('--help') || process.argv.includes('-h')) { console.log(usageText(readFileSync(fileURLToPath(import.meta.url), 'utf8'))); } else { + // A malformed sweep target is bad usage, refused BEFORE any request — the + // transport probe's second stage is already a repo-scoped read of this very + // string. Silently falling back to the default would read a board nobody + // asked for and render a confident report about it. The two offline branches + // above are exempt by construction: they make no request and must stay + // runnable in any container, whatever the environment carries. + if (!SWEEP_REPO.valid) { + console.error( + `ci-failure: ${SWEEP_REPO.source}=${JSON.stringify(SWEEP_REPO.repo)} is not an ` + + '`owner/name` repository. Refusing to fall back to a different board — a report about ' + + 'the wrong repo reads exactly like a report about this one.', + ); + process.exit(EXIT_UNDETERMINED); + } + // Transport before credentials: behind the session proxy an unproxied fetch // answers 401 on every endpoint, and the probe below would classify that as a // dead credential. The flag only takes effect at process start.