From 0db857828a96eabba440c34d16c00af320d7d7bc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:44:28 +0000 Subject: [PATCH 1/5] tooling(pm): admit a same-PR cross-file move in the skill line ratchet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-file line ratchet is shrink-only, which prices a FACT MOVING TO ITS SINGLE-SOURCE FILE as though it were growth: the destination sits at headroom 0 like every other entry, so consolidating a misplaced per-repo reading needed a per-instance maintainer ruling even though the corpus shrank on net. Adds the declared cross-file move as the one raise an author may take alone, under the three ruled conditions: the destination's raise is at most the net decrease of the sources it names, the map-wide total does not increase, and the declaration cites the ruling that authorises it. The declaration is in-tree data carrying each participant's pre-move ceiling, so the arithmetic is computable from the working tree alone — no baseline ref a shallow CI checkout can fail to provide, and no answer that evaporates once the move has landed. Verdict lines and exit codes are unchanged; the map is empty in this commit. Part of #14685 Part of #13597 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- scripts/pm/check-skill-line-ratchet.mjs | 251 ++++++++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index 95a1a82b67..31c8100bfc 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -70,10 +70,70 @@ * of raising the roof. * - The headroom between a file's count and its ceiling is the budget for * ordinary rule edits between compressions; it is deliberately small. + * - A same-PR CROSS-FILE MOVE is the one raise an author may take without a + * per-instance ruling — under the three conditions in the section below, and + * never as a way to grow the corpus. * * Missing file or empty read is RED, never a pass (#4690: a gate that cannot * find its input must fail, not skip). * + * ## The one raise an author may take alone: a same-PR CROSS-FILE MOVE + * + * Maintainer ruling, 2026-09-03, adopting the measured option A of the skills + * optimization programme's third decision batch — verbatim and untranslated: + * 「同意」, and the adopted text in the ruling comment's own words, which every + * declaration below cites as {@link RULING_CITATION}: + * + * the per-file line ratchet admits a cross-file move in one PR when the + * destination's allowance rises by no more than the source's net decrease and + * total lines do not increase, with the ruling cited in the ratchet comment + * + * The defect it repairs is structural rather than a hardship claim: a FACT can + * be in the wrong FILE. A per-repo gate reading written into one lane charter is + * read by that lane and missed by the other six, and its single source is + * `references/platform-readings.md`. Consolidating it there REDUCES the corpus — + * one copy, one reader path — yet under a shrink-only per-file ceiling it was + * impossible without a per-instance maintainer ruling, because the destination + * sits at headroom 0 like every other entry. The ratchet was pricing a move as + * though it were growth, and the file's own remedy sentence — move narrative out + * rather than raise the roof — was the very act it could not price. + * + * ⛔ It is NOT a general raise, and it is not a re-wrap: deletion at the source + * pays, restatement does not. Three conditions, all checked below against + * {@link CROSS_FILE_MOVES}: + * + * (a) the destination's raise is at most the NET DECREASE of the sources that + * move declares, per move; + * (b) the sum of ceilings over the whole map does not increase; + * (c) the raised entry declares the ruling that authorises it. + * + * ### What this gate can see, and what stays a reviewer's job + * + * The script reads the WORKING TREE and its own map. It never sees the PR diff, + * so it cannot know that a raise happened at all, still less that a decrease + * landed in the same PR. What it can do — and now does — is hold a DECLARED move + * to its own arithmetic: every participant records the ceiling it carried BEFORE + * the move, so the raise, the decreases and the map-wide total are computable + * from the tree alone. Deterministic, hermetic, and available in a shallow CI + * checkout — which is why the declaration is in-tree data and not a + * `git show origin/main:` baseline diff. A baseline mode would read a + * ref that a shallow clone need not have and that a sibling's fetch moves under + * the run, and it would answer nothing at all once the move had landed. + * + * What stays with the reviewer is COMPLETENESS: that each recorded `was` is the + * value on `origin/main`, and that no OTHER entry moved in the same PR. Both are + * one `git diff` away in the raising PR and neither is inferable from a tree. It + * is the bar this map has always used — every ceiling in it is an author's + * number, read at review against the diff — so a declaration weakens nothing: it + * is the first raise path here that a machine can check at all. + * + * ⚠️ (b) is belt and braces, not a second independent control. Over declarations + * whose participants are disjoint — which {@link crossFileMoveTotalVerdict} + * requires — it follows from (a) summed over the moves. It is written out + * because it is the ruling's own wording, and because it is the condition that + * would still be right if (a) were ever loosened. + * + * * ## The max-line-length rule (#11106) — what makes a LINE a unit again * * The ratchet counts LINES while the paragraph above prices a per-session token @@ -539,6 +599,44 @@ export const CEILINGS = new Map([ ['CLAUDE.md', 86], ]); +/** + * The ruling that authorises a cross-file move, spelled once. Every declaration + * in {@link CROSS_FILE_MOVES} must name it; a raise that names nothing is a + * raise, not a move, and the header's escape hatch (a maintainer ruling quoted + * in the raising PR) is the only other way one lands. + */ +export const RULING_CITATION = '#14685 item 5 (comment 5520452691)'; + +/** + * Declared same-PR CROSS-FILE MOVES — the raise path the header section above + * authorises. Keyed by DESTINATION (the entry whose ceiling rose); one + * declaration per destination, and no path appears twice across the map. + * + * Each declaration carries the ceilings its participants held BEFORE the move, + * which is what makes the whole arithmetic computable from this file alone: + * + * ruling the authorising ruling — must name {@link RULING_CITATION} + * was the destination's ceiling before the raise + * sources [path, ceiling-before-the-move] for every file that paid for it + * + * ⛔ NOT somewhere to record a raise no move paid for. A declaration is a CLAIM + * about an arithmetic, and {@link crossFileMoveVerdict} re-derives that + * arithmetic from the live map on every run rather than trusting the prose + * beside it. + * + * ⚠️ A declaration keeps working after its PR lands, and the two directions are + * deliberately asymmetric. Lowering the destination later is always legitimate, + * so a destination that has fallen back to or below its `was` reads as PAID + * DOWN and passes. Raising a SOURCE later is the loophole the move mechanism + * would otherwise open — the destination keeps the lines it was given while the + * files that paid for them grow back, and the corpus is up on net with the + * move's warrant silently spent — so the sum is re-checked against the sources' + * pre-move values for as long as the declaration stands. That red belongs to + * the later raise, not to this one, and its author is holding the ruling that + * has to account for it. + */ +export const CROSS_FILE_MOVES = new Map([]); + /** * Per-file MAX TABLE ROW BYTES — the second ratchet, and the only metered thing * in this file that is not a line count (#11947). @@ -686,6 +784,82 @@ function countLines(text) { return text.length === 0 ? 0 : text.split('\n').length - (text.endsWith('\n') ? 1 : 0); } +/** + * One declared cross-file move, re-derived from the live ceiling map. + * + * Conditions (a) and (c) of the ruling; (b) is map-wide and lives in + * {@link crossFileMoveTotalVerdict}. Every failure names the arithmetic it read, + * because the author's next move is to correct one of those numbers and a + * verdict that hides them cannot be acted on. + * + * @param {string} dest destination path — a key of `ceilings` + * @param {{ruling?: string, was?: number, sources?: Array<[string, number]>}} move + * @param {Map} ceilings + */ +export function crossFileMoveVerdict(dest, move, ceilings) { + const bad = (msg) => ({ ok: false, msg }); + if (!ceilings.has(dest)) { + return bad(`cross-file move declares ${dest} as its destination, which carries no ceiling — red, not a skip (#4690). A move can only land between files this map covers.`); + } + if (!String(move?.ruling ?? '').includes(RULING_CITATION)) { + return bad(`cross-file move into ${dest} cites no authorising ruling — it must name "${RULING_CITATION}". An undeclared raise is a raise, and raising a ceiling otherwise requires a maintainer ruling quoted in the raising PR.`); + } + if (!Number.isInteger(move.was) || move.was <= 0) { + return bad(`cross-file move into ${dest} records no usable pre-move ceiling ("was"), so its raise cannot be measured — red, not a skip (#4690).`); + } + const sources = move.sources ?? []; + if (sources.length === 0) { + return bad(`cross-file move into ${dest} names no source file. The raise is paid by deletion somewhere else in this map, and a move with nothing to pay it is a raise.`); + } + for (const [src, wasSrc] of sources) { + if (src === dest) return bad(`cross-file move into ${dest} names itself as a source; a file cannot pay its own raise.`); + if (!ceilings.has(src)) return bad(`cross-file move into ${dest} names source ${src}, which carries no ceiling — red, not a skip (#4690).`); + if (!Number.isInteger(wasSrc) || wasSrc <= 0) return bad(`cross-file move into ${dest} records no usable pre-move ceiling for source ${src}.`); + } + const raise = ceilings.get(dest) - move.was; + const parts = sources.map(([src, wasSrc]) => `${src} ${wasSrc}→${ceilings.get(src)}`).join(', '); + if (raise <= 0) { + return { ok: true, msg: `cross-file move into ${dest} is paid down: its ceiling is ${ceilings.get(dest)}, at or below the ${move.was} it moved from (sources: ${parts}).` }; + } + const decrease = sources.reduce((sum, [src, wasSrc]) => sum + (wasSrc - ceilings.get(src)), 0); + if (raise > decrease) { + return bad(`cross-file move into ${dest} raises its ceiling ${move.was}→${ceilings.get(dest)} (+${raise}) against a net source decrease of ${decrease} (${parts}). A move pays for itself by DELETION at the source; re-wrapping does not pay, and a raise the sources do not cover is an ordinary raise, which needs a maintainer ruling quoted in the raising PR.`); + } + return { ok: true, msg: `cross-file move into ${dest}: +${raise} (${move.was}→${ceilings.get(dest)}) against a net source decrease of ${decrease} (${parts}); authorised by ${RULING_CITATION}.` }; +} + +/** + * Condition (b): the sum of ceilings over the whole map does not increase. + * + * Computed as the map-wide sum with every declared participant restored to its + * pre-move value, minus the map-wide sum today — untouched entries cancel, so + * the difference is exactly the declared participants' net movement. Disjoint + * participants are REQUIRED, and that is what keeps the reduction honest: a + * source named by two declarations would have its single decrease counted twice + * by (a) and once here, and the two conditions would stop agreeing. + * + * @param {Map}>} moves + * @param {Map} ceilings + */ +export function crossFileMoveTotalVerdict(moves, ceilings) { + const seen = new Map(); + let net = 0; + for (const [dest, move] of moves) { + for (const [path, was] of [[dest, move.was], ...(move.sources ?? [])]) { + if (seen.has(path)) { + return { ok: false, msg: `${path} is a participant in two cross-file moves (${seen.get(path)} and ${dest}). Declarations must be disjoint: one file's single decrease cannot pay for two raises, and counting it twice is how a total that grew reads as a total that did not.` }; + } + seen.set(path, dest); + if (!ceilings.has(path) || !Number.isInteger(was)) continue; + net += ceilings.get(path) - was; + } + } + if (net > 0) { + return { ok: false, msg: `the declared cross-file moves raise the map's total by ${net} lines. The ruling admits a move only while total lines do not increase: pay the difference by deleting more at a source, or take the raise the ordinary way — a maintainer ruling quoted in the raising PR.` }; + } + return { ok: true, msg: `declared cross-file moves: ${moves.size}, total ceilings ${net === 0 ? 'unchanged' : `down ${-net} lines`}.` }; +} + // ───────────────────────────────────────────────────────────────────────────── // The max-line-length rule (#11106). See the header for why 120 and why bytes. // ───────────────────────────────────────────────────────────────────────────── @@ -1046,6 +1220,22 @@ function run() { } console.log(`✓ check-skill-line-ratchet: ${v.msg}`); } + for (const [dest, move] of CROSS_FILE_MOVES) { + const mv = crossFileMoveVerdict(dest, move, CEILINGS); + if (!mv.ok) { + failed++; + console.error(`✗ check-skill-line-ratchet: ${mv.msg}`); + } else { + console.log(`✓ check-skill-line-ratchet: ${mv.msg}`); + } + } + const totals = crossFileMoveTotalVerdict(CROSS_FILE_MOVES, CEILINGS); + if (!totals.ok) { + failed++; + console.error(`✗ check-skill-line-ratchet: ${totals.msg}`); + } else { + console.log(`✓ check-skill-line-ratchet: ${totals.msg}`); + } if (failed) process.exit(1); } @@ -1247,6 +1437,67 @@ function selfTest() { ['the published skills/ catalog is uncovered here too', [...MAX_TABLE_ROW_BYTES.keys()].some((k) => k.startsWith('skills/')), false], ]; })(), + // ── The cross-file move (the 2026-09-03 ruling) ───────────────────────── + // Every case is a PAIR or a near-twin of a legal declaration, because the + // failure mode of a loose implementation is a `move` that licenses an + // ordinary raise — which would run green forever and retire the ratchet. + ...(() => { + const ceil = (o) => new Map(Object.entries(o)); + const cited = `moved from a.md + b.md — ${RULING_CITATION}`; + const decl = { ruling: cited, was: 100, sources: [['a.md', 30], ['b.md', 20]] }; + // Paid: dest +5 against sources that shed 3 and 4. + const paid = ceil({ 'dest.md': 105, 'a.md': 27, 'b.md': 16 }); + // Same declaration, sources untouched — the raise nobody paid for. + const unpaid = ceil({ 'dest.md': 105, 'a.md': 30, 'b.md': 20 }); + // Paid in part only: +5 against a net decrease of 3. + const short = ceil({ 'dest.md': 105, 'a.md': 27, 'b.md': 20 }); + // The destination lowered again after the move landed. + const paidDown = ceil({ 'dest.md': 98, 'a.md': 27, 'b.md': 16 }); + // A source grown back after the move landed: the payment undone. + const regrown = ceil({ 'dest.md': 105, 'a.md': 33, 'b.md': 16 }); + const uncited = { ...decl, ruling: 'moved from a.md + b.md' }; + const mv = (map, d = decl) => crossFileMoveVerdict('dest.md', d, map); + const total = (map, moves) => crossFileMoveTotalVerdict(new Map(moves), map); + return [ + ['the citation is the ruling this file was given', RULING_CITATION, '#14685 item 5 (comment 5520452691)'], + // (1) A legal move passes — the assertion the other three are read against. + ['cross-file move — a raise covered by its sources\' net decrease PASSES', mv(paid).ok, true], + ['...and the green verdict states the arithmetic it read', mv(paid).msg.includes('+5') && mv(paid).msg.includes('decrease of 7'), true], + // (2) A raise with no decrease behind it is an ordinary raise. + ['cross-file move — a raise whose sources did NOT shrink is RED (the whole defect: a `move` that licenses an ordinary raise would run green forever)', mv(unpaid).ok, false], + ['...and the RED verdict names the raise and the net decrease it fell short of', mv(unpaid).msg.includes('+5') && mv(unpaid).msg.includes('decrease of 0'), true], + ['...and sends the author to the ordinary path rather than to a bigger declaration', mv(unpaid).msg.includes('maintainer ruling quoted in the raising PR'), true], + // (3) Partly paid is not paid — the boundary case, one line short. + ['cross-file move — a raise EXCEEDING the net decrease is RED, even by one line', mv(short).ok, false], + ['...while a raise exactly equal to it is legal', crossFileMoveVerdict('dest.md', { ...decl, was: 98 }, ceil({ 'dest.md': 105, 'a.md': 27, 'b.md': 16 })).ok, true], + // (4) The citation. A raise that names no ruling is not a move at all. + ['cross-file move — a declaration citing no ruling is RED, however sound its arithmetic', mv(paid, uncited).ok, false], + ['...and the RED verdict spells the citation it wanted', mv(paid, uncited).msg.includes(RULING_CITATION), true], + // (5) Condition (b), map-wide. Stated separately because it is the + // ruling's own wording; over disjoint participants it follows from (a). + ['cross-file move — declarations whose net movement is 0 leave the map total unchanged', total(paid, [['dest.md', decl]]).ok, true], + ['cross-file move — a declaration whose participants net POSITIVE fails the total (+2 here)', total(short, [['dest.md', decl]]).ok, false], + ['...and the RED total names the lines it grew by', total(short, [['dest.md', decl]]).msg.includes('by 2 lines'), true], + ['...and a net-negative move reports the corpus shrinking', total(paidDown, [['dest.md', decl]]).msg.includes('down 9 lines'), true], + // (6) Disjointness — the one thing that makes (a) summed equal (b). + ['cross-file move — one source may not pay for two destinations', total(ceil({ 'dest.md': 105, 'other.md': 105, 'a.md': 27, 'b.md': 16 }), [['dest.md', decl], ['other.md', { ruling: RULING_CITATION, was: 100, sources: [['a.md', 30]] }]]).ok, false], + // (7) Lifecycle, both directions — asymmetric on purpose. + ['cross-file move — a destination lowered back to or below its pre-move ceiling reads as PAID DOWN, never as red (lowering is always legitimate)', mv(paidDown).ok, true], + ['...and says so rather than reporting an arithmetic it can no longer measure', mv(paidDown).msg.includes('paid down'), true], + ['cross-file move — a SOURCE grown back past what it paid re-opens the move (the loophole: the destination keeps the lines while the payers grow back)', mv(regrown).ok, false], + // (8) Participants must be files this map covers — a typo is RED, not a skip. + ['cross-file move — an unknown destination is RED, not a skip (#4690)', crossFileMoveVerdict('nope.md', decl, paid).ok, false], + ['cross-file move — an unknown source is RED, not a skip (#4690)', mv(paid, { ...decl, sources: [['nope.md', 30]] }).ok, false], + ['cross-file move — a file may not pay its own raise', mv(paid, { ...decl, sources: [['dest.md', 130]] }).ok, false], + ['cross-file move — a declaration naming no source at all is RED', mv(paid, { ...decl, sources: [] }).ok, false], + // (9) The live map. Enforcement covers the arithmetic; what it cannot + // cover is the shape of a declaration nobody has written yet, so the + // fields every declaration must carry are pinned here. + ['every live declaration cites the ruling', [...CROSS_FILE_MOVES.values()].every((m) => String(m.ruling ?? '').includes(RULING_CITATION)), true], + ['every live declaration names its destination\'s pre-move ceiling and at least one source', [...CROSS_FILE_MOVES.values()].every((m) => Number.isInteger(m.was) && Array.isArray(m.sources) && m.sources.length > 0), true], + ['every live participant is a file this map covers', [...CROSS_FILE_MOVES].every(([d, m]) => CEILINGS.has(d) && m.sources.every(([s]) => CEILINGS.has(s))), true], + ]; + })(), ].map((c) => (Array.isArray(c[1]) || (c[1] && typeof c[1] === 'object') ? [c[0], JSON.stringify(c[1]), JSON.stringify(c[2])] : c)); let failed = 0; for (const [name, actual, expected] of cases) { From ebacad87a621a166a1fa37a7b02af86fce30dbe1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:47:16 +0000 Subject: [PATCH 2/5] tooling: record the authorised cross-file move in the remedy-authority gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #8435 convention asks who may take a registry-growing remedy on a shrink-only ratchet, and the answer has always been "a maintainer, per instance". One class of act is now authorised generally: a cross-file move in the per-file line ratchet, while the destination's raise is covered by the named sources' net decrease, the map-wide total does not increase, and the raised entry cites the ruling. The exception is written in comment space, because author-facing text describing a registry-growing act would make this gate an instance of the convention it enforces. Two self-test assertions hold it there: one that the phrase never reaches an author-facing message, one that the amended ratchet is still outside this non-recursive walk — so the exception stays honestly documentary, and a wider walk reds here instead of admitting a remedy class in silence. No new verdict class: a fifth class with a population of zero is enforcement nobody has. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- scripts/check-ratchet-remedy-authority.mjs | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/scripts/check-ratchet-remedy-authority.mjs b/scripts/check-ratchet-remedy-authority.mjs index 54f604a19a..65b74ab01a 100644 --- a/scripts/check-ratchet-remedy-authority.mjs +++ b/scripts/check-ratchet-remedy-authority.mjs @@ -16,6 +16,49 @@ // precedents are check-type-source-resolution.mjs, check-test-source-alias.mjs, // check-adr-links.mjs and check-driver-memory-census.mjs. // +// ── The ruled EXCEPTION: an authorised cross-file move (2026-09-03) ───────── +// +// The disjunction above asks WHO may take a registry-growing remedy, and on a +// shrink-only ratchet the answer was always "a maintainer, per instance". One +// class of act is now authorised generally instead of one PR at a time. +// Maintainer ruling, 2026-09-03, adopting option A of item 5 in the skills +// optimization programme's third decision batch (#14685, comment 5520452691) — +// verbatim and untranslated: 「同意」; the text it adopts: +// +// the per-file line ratchet admits a cross-file move in one PR when the +// destination's allowance rises by no more than the source's net decrease and +// total lines do not increase, with the ruling cited in the ratchet comment; +// `check:ratchet-remedy-authority`'s self-description gains this exception +// +// So: a cross-file MOVE in the per-file line ratchet is NOT a maintainer-only +// remedy while all three conditions hold — the destination's raise is covered by +// the named sources' net decrease, the map-wide total does not increase, and the +// raised entry cites that ruling. The scope is exactly the act the ruling names, +// in the gate the ruling names. A raise failing any condition is an ordinary +// raise and belongs to a maintainer as before, and no other ratchet in this farm +// gains anything: the reason a move is not a weakening is that the corpus SHRANK +// on net, which is a property of the act, not of the author. +// +// ⚠️ TODAY THE EXCEPTION IS DOCUMENTARY, and recording that is half the point of +// writing it here. The corpus walk is a NON-RECURSIVE readdir of `scripts/` (see +// corpusFiles), and the gate the ruling amends lives one directory down, in +// scripts/pm/ — it has never been in this sweep, so no verdict of this gate +// moves. A self-test assertion pins that, so a wider walk reds HERE and makes +// the class a decision someone takes rather than one that lands in silence. +// +// ⛔ NO NEW VERDICT CLASS. `marked` / `refused` / `unmarked` / `excluded` stay as +// they are. A fifth class for "authorised by standing ruling" would have a +// population of zero in this corpus, and an unreachable class is a claim of +// enforcement nobody has — the same defect this file's control corpus exists to +// prevent, one level up. If the walk ever reaches a gate offering this act, the +// honest first move is to read that gate's own verdict and record it in CONTROL, +// the way every instance in this file arrived. +// +// ⛔ THIS SECTION IS COMMENT, never a string literal — the header warning above, +// applied to itself. It describes an act that grows a registry, so in +// author-facing text it would make this gate an instance of the convention it +// enforces and flip its own verdict off `excluded`. +// // WHY A SWEEP AND NOT A SHARED MODULE. #8519 proposed a shared helper module as // the enforcement route. It is not one: a module is reachable only from gates // that choose to import it, so it standardises the gates that already agreed and @@ -1265,6 +1308,29 @@ function selfTest() { + 'nothing, and a dead declaration prints as the same silence as declaring nothing)', !ROOT_DIR_WATCH_HINTS.some((h) => h.includes('{'))); + // ── (21) The ruled cross-file-move exception (2026-09-03) ─────────────── + // + // Two assertions, and both are about THIS FILE rather than about the ratchet + // the ruling amends: where the exception is written down, and whether it is + // doing anything yet. Neither can be held by the run — a header section runs + // green whatever it says, which is exactly how a documented exception drifts + // into a claimed one. + const selfSrc = readFileSync(join(SCRIPTS_DIR, SELF_FILE), 'utf8'); + // ⛔ The phrase is ASSEMBLED, never spelled, for the reason the ADD hoist above gives in + // its own case: a literal spelling would put the phrase into THIS file's author-facing text + // and falsify the assertion below by the act of making it. + const EXCEPTION_PHRASE = ['cross-file', 'MOVE'].join(' '); + expect('the ruled exception is recorded in COMMENT space only — written into author-facing text ' + + 'it would describe a registry-growing act, and this gate would then read as an instance of ' + + 'the convention it enforces, which is what assertion (17) catches', + selfSrc.includes(EXCEPTION_PHRASE) + && !authorFacingMessages(selfSrc).some((m) => m.includes(EXCEPTION_PHRASE))); + + expect('the ruled exception is documentary today: the amended line ratchet lives one directory ' + + 'down and this walk is non-recursive, so a wider walk reds HERE rather than admitting a new ' + + 'remedy class in silence', + !corpusFiles().includes('check-skill-line-ratchet.mjs')); + if (failures.length > 0) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-ratchet-remedy-authority --self-test: ${failures.length} failure(s).\n`); From ba86fae4ceffe0fd3c8c4e3fa4db1efe1eb28ce6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:50:13 +0000 Subject: [PATCH 3/5] docs(pm-dispatch): consolidate three misplaced per-repo readings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A per-repo gate reading written into one lane charter is read by that lane and missed by the other six. Their single source is references/platform-readings.md, so the three the Phase-1 tables measured move there, under the cross-file-move rule this branch adds: lanes/cli.md -4 the objectstack required-check set, `in_progress` is not a pass, advisory red into main is shared damage lanes/services.md -2 the two aggregate-reading gate boundaries rest-channel.md -5 the merge_group count-is-not-a-mechanism tombstone, moved byte-identically platform-readings.md +10 against a net source decrease of 11, so the corpus falls by one line: the per-job-conclusion rule cli.md carried is already stated in the destination's queue section, so the moved bullet points at it rather than paying for a second copy. Ceilings move with the text (314→324, 35→31, 30→28, 93→88) and the move is declared, so the gate re-derives the arithmetic on every run instead of trusting the comment beside it. No pointer is left at any source: nothing at the three sites becomes unreadable without the moved sentence, and a pointer per move is how a corpus grows while its ledger says it shrank. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- .../pm-dispatch/references/lanes/cli.md | 4 -- .../pm-dispatch/references/lanes/services.md | 2 - .../references/platform-readings.md | 10 ++++ .../pm-dispatch/references/rest-channel.md | 5 -- scripts/pm/check-skill-line-ratchet.mjs | 53 ++++++++++++++++--- 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/lanes/cli.md b/.claude/skills/pm-dispatch/references/lanes/cli.md index d838da1041..9fc945448a 100644 --- a/.claude/skills/pm-dispatch/references/lanes/cli.md +++ b/.claude/skills/pm-dispatch/references/lanes/cli.md @@ -14,10 +14,6 @@ ## 常设承诺 -- **Required checks 六个**:`TypeScript Type Check` · `Lint & Repo Gates` · - `Test Core` · `Dogfood Regression Gate` · `Build Core` · - `Temporal Conformance (live PG + MySQL)` —— 逐 job 读各自 `conclusion`,⛔ 不认 - 聚合,`in_progress` 不是过;advisory 门禁红进 main 是共享损伤,照样止血立单。 - **PR 侧绿 ≠ 队列侧绿**;队列分支读法与边界在 platform-readings(条目名 `main/pr-{number}-{parent sha}`,parent sha 可重建队序;⚠️ ref 在出队后滞留)。 - `dispatch-gates.mjs` 只报**路径推导**的地板 —— 条款②从卡**内容**判且优先于它;已知盲 diff --git a/.claude/skills/pm-dispatch/references/lanes/services.md b/.claude/skills/pm-dispatch/references/lanes/services.md index f3fd4177b2..fb29e039e0 100644 --- a/.claude/skills/pm-dispatch/references/lanes/services.md +++ b/.claude/skills/pm-dispatch/references/lanes/services.md @@ -18,8 +18,6 @@ —— 单独的绿不是证据。 - **安全族卡的披露纪律**:复现配方不落任何公开面(卡、PR、评论);证据以抽象描述 或私有通道承载。 -- 门禁读数不轻信聚合:`check:type-check-debt` 可以在包级 typecheck 绿时红; - `check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 1 不是漂移。 ## 席内判断 diff --git a/.claude/skills/pm-dispatch/references/platform-readings.md b/.claude/skills/pm-dispatch/references/platform-readings.md index 1970159b8c..4f9608f61a 100644 --- a/.claude/skills/pm-dispatch/references/platform-readings.md +++ b/.claude/skills/pm-dispatch/references/platform-readings.md @@ -294,6 +294,16 @@ 降档(那正是降档保险丝要拒的替换),但「本车道 fable 强制」多是过宽的回忆 —— `dispatch-gates.mjs --tier PATH` 逐路径现推(实测:`SKILL.md` 与 `.claude/agents/**` 强制,`scripts/pm/**` 与 `references/**` 无 ⇒ 全阻塞的三卡 fold 拆成 1 阻 3 可跑);路径线是**下限不是放行**。 +- **required checks 的名单是每仓事实**(逐 job 读判定的规则住上面队列段):objectstack 六个 —— + `TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Dogfood Regression Gate` · + `Build Core` · `Temporal Conformance (live PG + MySQL)`;`in_progress` 不是过;advisory 门禁 + 红进 main 是共享损伤,照样止血立单。⛔ 聚合命令同样不作判定:`check:type-check-debt` 可在 + 包级 typecheck 绿时红;`check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 1 不是漂移。 +- ⚠️ **计数不是机理读数(被当天推翻的推断的墓碑)**:「`GET .../actions/runs?event=merge_group` + 计数 0 ⇒ required 集为空」提出当天即被自身推翻 —— 同一姊妹仓 2026-08-24 首现 merge_group + run(0 → 8),同日再测 224(阳性对照 `event=pull_request` 全程非零)。计数答「至今发生过没有」, + 不答「机制在不在」:零计数只作**弱先验**,判 required 集为空要读 ruleset + 的 required 集本身、或看队列合并是否真在等检查;⛔ 别处写下的计数值一律先复测再用。 ## 闭合关键词解析(PR 正文写侧) diff --git a/.claude/skills/pm-dispatch/references/rest-channel.md b/.claude/skills/pm-dispatch/references/rest-channel.md index e68aff7548..b42a9cdad2 100644 --- a/.claude/skills/pm-dispatch/references/rest-channel.md +++ b/.claude/skills/pm-dispatch/references/rest-channel.md @@ -76,11 +76,6 @@ payload → REST → MCP/GraphQL** 的策略住那里,本表是逐操作的通 `PUT .../pulls/{n}/merge`,强制队列 ruleset 下回 **405 `Changes must be made through the merge queue`**;② PR 上的 `added_to_merge_queue` timeline 事件;③ 对已入队 PR 调 update-branch 回「已入队分支不能更新, 要改先出队」。①② 拼写与边界是 `platform-readings.md` 队列段既有行,本条只归拢判据。 -- ⚠️ **计数不是机理读数(被当天推翻的推断的墓碑)**:「`GET .../actions/runs?event=merge_group` - 计数 0 ⇒ required 集为空」提出当天即被自身推翻 —— 同一姊妹仓 2026-08-24 首现 merge_group - run(0 → 8),同日再测 224(阳性对照 `event=pull_request` 全程非零)。计数答「至今发生过没有」, - 不答「机制在不在」:零计数只作**弱先验**,判 required 集为空要读 ruleset - 的 required 集本身、或看队列合并是否真在等检查;⛔ 别处写下的计数值一律先复测再用。 - **required job 名与分片矩阵的改名耦合(现行,自 2026-08-24)**: 队列 required 集按 **job / check-run 名**匹配,**workflow 名从不作为 check context 出现**(所以拿 workflow 名在选择器里搜什么也搜不到);改其中任一 job 名**或 test 分片矩阵的形状**, diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index 31c8100bfc..632ac38fe0 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -403,7 +403,20 @@ export const CEILINGS = new Map([ // operation channel mapping moved out to references/rest-channel.md below. // The two right-sized-reads rows the same ruling ordered were paid from that // saving in place, and three lines came back. Headroom 0 again. - ['.claude/skills/pm-dispatch/references/platform-readings.md', 314], + // 314 → 324 by the CROSS-FILE MOVE the 2026-09-03 ruling authorises — + // #14685 item 5 (comment 5520452691), declared in CROSS_FILE_MOVES below and + // re-derived there on every run. Three misplaced per-repo facts consolidate + // into the file that is their single source: the objectstack required-check + // set with its `in_progress` and advisory boundaries (lanes/cli.md, −4), the + // two aggregate-reading gate boundaries (lanes/services.md, −2), and the + // merge_group count-is-not-a-mechanism tombstone (rest-channel.md, −5, moved + // byte-identically). The per-job-conclusion rule the first of those carried + // was NOT copied: this file already states it in 队列成员资格 above, so the + // moved bullet points at it instead — a move that restated it would be paying + // for a second copy. +10 against a net source decrease of 11, so the map's + // total falls by one; that is the ruling's own condition and the reason this + // is a move rather than a raise. Landed count, headroom 0, same convention. + ['.claude/skills/pm-dispatch/references/platform-readings.md', 324], // Per-operation REST/GraphQL/git channel mapping — which fleet operation has // a REST twin (each row executed in a real session, provenance date carried // per row), the handful that are GraphQL-only, and the queue-routing @@ -433,7 +446,13 @@ export const CEILINGS = new Map([ // by re-wrap: the file measures zero reclaimable lines under this gate's own // wrapLine, and re-wrap funding is refused per the 2026-08-17 rule in any // case. Landed count, headroom 0, same convention. - ['.claude/skills/pm-dispatch/references/rest-channel.md', 93], + // Lowered 93 → 88 as a SOURCE of the cross-file move recorded on + // platform-readings.md above: the merge_group count-is-not-a-mechanism + // tombstone left this table for the readings file, byte-identically. This + // file's own header already routes the queue-section readings there and + // refuses to keep a second copy, so nothing is left behind but the rows that + // are channel mappings. Landed count, headroom 0, same convention. + ['.claude/skills/pm-dispatch/references/rest-channel.md', 88], ['.claude/skills/pm-dispatch/references/review-checklist.md', 84], ['.claude/skills/pm-dispatch/references/landing-operations.md', 80], // Release-aftercare duties — what a lane PM still owes AFTER a tagged release @@ -479,8 +498,14 @@ export const CEILINGS = new Map([ // lanes and now lives once in SKILL.md 执行座位职责, so every lane drops it. // cli.md pays −2 because it was carrying the map's last line of headroom. ['.claude/skills/pm-dispatch/references/lanes/engine.md', 40], - ['.claude/skills/pm-dispatch/references/lanes/services.md', 30], - ['.claude/skills/pm-dispatch/references/lanes/cli.md', 35], + // Lowered 30 → 28 and 35 → 31 as the other two SOURCES of the cross-file + // move recorded on platform-readings.md above. A per-repo gate reading in a + // lane charter is read by one lane and missed by the other six, which is the + // whole defect the move repairs: services.md gave up the two aggregate- + // reading boundaries, cli.md the required-check set with its `in_progress` + // and advisory clauses. Landed counts, headroom 0, same convention. + ['.claude/skills/pm-dispatch/references/lanes/services.md', 28], + ['.claude/skills/pm-dispatch/references/lanes/cli.md', 31], ['.claude/skills/pm-dispatch/references/lanes/devx.md', 39], ['.claude/skills/pm-dispatch/references/lanes/skills.md', 35], ['.claude/skills/pm-dispatch/references/lanes/spec.md', 46], @@ -635,7 +660,23 @@ export const RULING_CITATION = '#14685 item 5 (comment 5520452691)'; * the later raise, not to this one, and its author is holding the ruling that * has to account for it. */ -export const CROSS_FILE_MOVES = new Map([]); +export const CROSS_FILE_MOVES = new Map([ + // The readings consolidation — the first move taken under the ruling, and the + // one it was ruled for. Sources' `was` values are their ceilings on + // `origin/main` at f3ae441fa2; the reviewer holds them against that diff. + [ + '.claude/skills/pm-dispatch/references/platform-readings.md', + { + ruling: 'per-repo readings consolidation, authorised by #14685 item 5 (comment 5520452691)', + was: 314, + sources: [ + ['.claude/skills/pm-dispatch/references/lanes/cli.md', 35], + ['.claude/skills/pm-dispatch/references/lanes/services.md', 30], + ['.claude/skills/pm-dispatch/references/rest-channel.md', 93], + ], + }, + ], +]); /** * Per-file MAX TABLE ROW BYTES — the second ratchet, and the only metered thing @@ -857,7 +898,7 @@ export function crossFileMoveTotalVerdict(moves, ceilings) { if (net > 0) { return { ok: false, msg: `the declared cross-file moves raise the map's total by ${net} lines. The ruling admits a move only while total lines do not increase: pay the difference by deleting more at a source, or take the raise the ordinary way — a maintainer ruling quoted in the raising PR.` }; } - return { ok: true, msg: `declared cross-file moves: ${moves.size}, total ceilings ${net === 0 ? 'unchanged' : `down ${-net} lines`}.` }; + return { ok: true, msg: `declared cross-file moves: ${moves.size}, total ceilings ${net === 0 ? 'unchanged' : `down ${-net} line${net === -1 ? '' : 's'}`}.` }; } // ───────────────────────────────────────────────────────────────────────────── From 9266898e39b818dc1e13dd259e90b9861b3df3a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:51:27 +0000 Subject: [PATCH 4/5] tooling(pm): singularise the cross-file move verdict's line counts Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- scripts/pm/check-skill-line-ratchet.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index 632ac38fe0..747d1be5bc 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -896,7 +896,7 @@ export function crossFileMoveTotalVerdict(moves, ceilings) { } } if (net > 0) { - return { ok: false, msg: `the declared cross-file moves raise the map's total by ${net} lines. The ruling admits a move only while total lines do not increase: pay the difference by deleting more at a source, or take the raise the ordinary way — a maintainer ruling quoted in the raising PR.` }; + return { ok: false, msg: `the declared cross-file moves raise the map's total by ${net} line${net === 1 ? '' : 's'}. The ruling admits a move only while total lines do not increase: pay the difference by deleting more at a source, or take the raise the ordinary way — a maintainer ruling quoted in the raising PR.` }; } return { ok: true, msg: `declared cross-file moves: ${moves.size}, total ceilings ${net === 0 ? 'unchanged' : `down ${-net} line${net === -1 ? '' : 's'}`}.` }; } From 8b7a2ec36ca401c4514a354d0adca8323b36f543 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 15:19:53 +0000 Subject: [PATCH 5/5] docs(pm-dispatch): carry main's corrected exit code into the moved i18n reading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit references/platform-readings.md line 301 -- the bullet this branch moved out of lanes/services.md -- still said the i18n gates answer PREREQUISITE NOT MET with exit 1. main's 365141f735 (#14856) moved the last three i18n gates (scripts/check-i18n-coverage.mjs, scripts/check-i18n-bundles.mjs, packages/cli/scripts/check-app-nav-i18n.mjs) onto EXIT_PREREQUISITE_NOT_MET (3, from scripts/import-prerequisite.mjs) and corrected the source bullet to "退 3" in the same commit; the merge took our deletion of that bullet, so the corrected fact is carried into the moved text here rather than re-landing the claim main already corrected. Same line count (324, ceiling unchanged); no other edit. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 --- .claude/skills/pm-dispatch/references/platform-readings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/pm-dispatch/references/platform-readings.md b/.claude/skills/pm-dispatch/references/platform-readings.md index 4f9608f61a..53fe6e3e56 100644 --- a/.claude/skills/pm-dispatch/references/platform-readings.md +++ b/.claude/skills/pm-dispatch/references/platform-readings.md @@ -298,7 +298,7 @@ `TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Dogfood Regression Gate` · `Build Core` · `Temporal Conformance (live PG + MySQL)`;`in_progress` 不是过;advisory 门禁 红进 main 是共享损伤,照样止血立单。⛔ 聚合命令同样不作判定:`check:type-check-debt` 可在 - 包级 typecheck 绿时红;`check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 1 不是漂移。 + 包级 typecheck 绿时红;`check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 3 不是漂移。 - ⚠️ **计数不是机理读数(被当天推翻的推断的墓碑)**:「`GET .../actions/runs?event=merge_group` 计数 0 ⇒ required 集为空」提出当天即被自身推翻 —— 同一姊妹仓 2026-08-24 首现 merge_group run(0 → 8),同日再测 224(阳性对照 `event=pull_request` 全程非零)。计数答「至今发生过没有」,