Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions scripts/pm/bare-root-worklist.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 ────────────────────────────────────
Expand DownExpand Up@@ -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', {
Expand DownExpand Up@@ -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',
}],
Expand Down
11 changes: 11 additions & 0 deletions scripts/pm/check-skill-line-ratchet.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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).
Expand Down
30 changes: 29 additions & 1 deletion scripts/pm/ci-failure.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 ?? '';

Expand DownExpand Up@@ -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.
Expand Down
Loading