diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index a79dc0337a..d0ded1b522 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -25,6 +25,39 @@ * see check-skill-id-lint.mjs). Without a gate that intent erodes one * well-meaning paragraph at a time. * + * ## What is covered — and the published catalog, which deliberately is not + * + * "The whole surface" above means the CEILINGS map below, which is an + * ENUMERATION, never a root glob: the pm-dispatch surface (SKILL.md, its + * references/, the per-lane job descriptions), the four other + * `.claude/skills/` playbook SKILL.md files, the dev-agent definition + * `.claude/agents/os-dev.md`, and the root `AGENTS.md`. Read a file's absence + * from the map as a fact to check, not an oversight to infer — `.claude/hooks/`, + * `.claude/settings.json` and `CLAUDE.md` carry no ceiling either. + * + * The **published** `skills/` catalog — the one that ships to customer projects + * — is deliberately OUTSIDE the ceiling. It is the omission worth stating + * because it is by far the larger surface: eleven published SKILL.md totalling + * ~10,400 lines, against ~3,700 covered here. Two reasons, both about the cost + * curve the ratchet prices rather than about size (#9923): + * + * - What the ratchet prices is a full-file token read paid PER SEAT SESSION + * and PER ROUTINE FIRE, which is what every covered file above costs. The + * published catalog is read by customer projects, not by this repo's seats + * — a different cost curve, and not the one this gate was built against. + * - The published catalog is already a governed, human-merge-only surface + * (Prime Directive #14), so growth there passes a human eye by + * construction — a control a ratchet would merely duplicate. Note this + * reason does not separate the two roots by itself: the covered files are + * governed at merge too. What no reviewer prices THERE is the recurring + * per-session read of reason one, which is why they still carry ceilings + * and the published catalog does not. + * + * Extending coverage to the published root is therefore a POLICY CHANGE, not a + * maintenance edit: it needs its own card and a maintainer's ruling, not a + * CEILINGS row added in passing. The self-test pins this boundary, because + * enforcement cannot — a published-root entry would run perfectly green. + * * ## The ratchet discipline (shrink-only, per file) * * - A ceiling may be LOWERED by any PR that shrinks its file — lowering is @@ -142,6 +175,12 @@ function selfTest() { ['all six lane job descriptions are covered', ['engine', 'services', 'cli', 'devx', 'skills', 'spec'].every((n) => CEILINGS.has(`.claude/skills/pm-dispatch/references/lanes/${n}.md`)), true], ['the other four skills are covered (#9473)', ['checklist-test', 'checklist-author', 'dogfood-verification', 'spec-property-retirement'].every((n) => CEILINGS.has(`.claude/skills/${n}/SKILL.md`)), true], ['root AGENTS.md is covered (#9792)', CEILINGS.has('AGENTS.md'), true], + // The boundary the header states, pinned (#9923). Enforcement cannot hold + // it: a ceiling on a real published SKILL.md runs green like any other row, + // so without this case the header paragraph could drift from the map + // silently. Extending coverage to the published catalog is a policy change + // — it lands with a maintainer ruling that also deletes this case. + ['the published skills/ catalog is deliberately uncovered', [...CEILINGS.keys()].some((k) => k.startsWith('skills/')), false], ]; let failed = 0; for (const [name, actual, expected] of cases) {