Skip to content

tooling(pm): admit a same-PR cross-file line-budget move, and take the first one - #14860

Merged
hotlong merged 7 commits into
mainfrom
claude/issue-14685-ratchet-cross-file-move
Sep 3, 2026
Merged

tooling(pm): admit a same-PR cross-file line-budget move, and take the first one#14860
hotlong merged 7 commits into
mainfrom
claude/issue-14685-ratchet-cross-file-move

Conversation

@os-litant

@os-litant os-litant commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Part of #14685
Part of #13597

Item 5 of decision batch 3, ruled A. Ruling comment 5520452691 on the card, verbatim:

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. platform-readings.md absorbs the three misplaced per-repo facts under it.

Two halves land together because neither is useful alone: the rule, and the first move taken under it.

1. The rule — scripts/pm/check-skill-line-ratchet.mjs

The defect is structural, not 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; 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.

A declared cross-file move is now the one raise an author may take alone. Three conditions, all re-derived from the tree on every run:

  • (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.

The mechanical form, and why this one

The flight brief offered two shapes. I took (i), in-tree declaration data, not (ii) a --baseline origin/main diff, and the map entry carries the human comment the ruling asks for plus a machine-readable CROSS_FILE_MOVES declaration rather than a comment the script has to regex out of its own source.

Why:

  • A baseline ref answers nothing once the move has landed. --baseline origin/main only sees a raise while the PR is open; after merge, HEAD and origin/main agree and the check goes silent — so the rule would be enforced exactly once, by a run nobody re-reads.
  • origin/main is not a stable input. It is a local snapshot a sibling worktree's fetch moves under the run, and a shallow CI checkout need not carry it at all. A gate whose verdict depends on it must either red on a missing ref (breaking shallow checkouts) or skip — and a gate that cannot find its input must fail, not skip.
  • Parsing your own source for a trailing comment breaks silently. The declaration is data, so the fields a move needs (ruling, was, sources) are read, not scraped; the prose comment beside the ceiling stays what it has always been, prose for a reviewer.

What the gate can see, and what stays a reviewer's job

Stated in the header rather than implied. The script reads the working tree and its own map; it never sees the PR diff, so it cannot know a raise happened at all, still less that a decrease landed in the same PR. What it does is hold a declared move to its own arithmetic: each 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, available in a shallow checkout.

What stays with the reviewer is completeness: that each recorded was is the value on origin/main, and that no other entry moved. Both are one git diff away in this PR and neither is inferable from a tree. That is the bar this map has always used — every ceiling in it is an author's number, read at review against the diff — so the declaration weakens nothing; it is the first raise path in the file a machine can check at all.

Condition (b) is belt and braces, and the header says so: over declarations whose participants are disjoint (which the total check 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.

Lifecycle, deliberately asymmetric

A declaration keeps working after its PR lands.

  • 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. A move declaration must never red a shrink.
  • Raising a source later is the loophole a move mechanism opens — 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, and its author is holding the ruling that has to account for it.

Self-test

--self-test goes 111 to 136 cases (+25). The four the brief names, plus the boundary and lifecycle cases: a legal move passes; a raise whose sources did not shrink is RED; a raise exceeding the net decrease is RED even by one line, while a raise exactly equal to it is legal; a declaration citing no ruling is RED however sound its arithmetic; a declaration netting positive fails the map-wide total; one source may not pay for two destinations; a destination lowered back reads as paid down; a source grown back re-opens the move; an unknown participant is RED, not a skip; a file may not pay its own raise.

Verdict lines and exit codes are unchanged. Two lines are added to the run's output: the per-move verdict, and the map-wide total (printed even at zero declarations, so "the move audit ran" is visible rather than assumed).

2. The exception — scripts/check-ratchet-remedy-authority.mjs

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". The ruling authorises one class of act generally instead, and the gate's self-description now records it.

Three calls worth reading rather than counting:

  • It is written in comment space, never a string literal. That gate reads author-facing text, so an exception describing a registry-growing act, written into a message, would make the gate an instance of the convention it enforces and flip its own verdict off excluded — the shape its assertion (17) exists to catch. A new self-test assertion pins the exception phrase as present in source and absent from every author-facing message. This reverse-verified itself on the first attempt: the assertion reddened because the phrase was spelled as a plain literal in the test, which put it into author-facing text. It is assembled from parts now, the way the file's existing ADD hoist is, and for the same reason.
  • Today the exception is documentary, and the gate says so. The corpus walk is a non-recursive readdir of scripts/, and the ratchet the ruling amends lives one directory down in scripts/pm/. It has never been in this sweep, so no verdict of this gate moves — before and after are the same OK line, byte for byte. A second new assertion pins that, so widening the 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 that gate's control corpus exists to prevent, one level up.

3. The first move — three per-repo readings consolidate

Site by site. Every source span was measured at this branch's base f3ae441fa2, and the destination was grepped for the fact first, exactly as the flight brief asks.

Fact Source span (at base) Already at the destination? Disposition
The objectstack required-check set (six names), plus in_progress is not a pass and advisory red into main is shared damage references/lanes/cli.md:17-20 (4 lines) The per-job-conclusion / do-not-trust-the-aggregate rule: YES, stated in platform-readings.md 队列成员资格 (门禁放行判据 = 承载门禁族 job 的 conclusion …). The six names, in_progress and the advisory clause: no Rule NOT copied — the moved bullet points at the existing statement. Names and boundaries added; source deleted
Gate readings do not trust aggregates: check:type-check-debt can red while a package typecheck is green; check:i18n exiting 1 on PREREQUISITE NOT MET is not drift references/lanes/services.md:21-22 (2 lines) No Added to 读数陷阱, folded into the same bullet as the row above (it is the same rule with two named instruments); source deleted
The merge_group count-is-not-a-mechanism tombstone references/rest-channel.md:79-83 (5 lines) No Added to 读数陷阱 byte-identically (verified by comparing the deleted span against the added span); source deleted

No pointer sentence is left at any source. Nothing at the three sites becomes unreadable without the moved sentence: cli.md already routes queue readings to platform-readings.md in the bullet above the deleted one; services.md keeps two independent standing commitments; rest-channel.md keeps four queue-routing bullets and its own header already refuses to hold a second copy of what platform-readings.md owns. A pointer per move is how a corpus grows while its ledger says it shrank.

Per-file arithmetic

File Lines before/after Ceiling before/after Bytes before/after Tokens before/after
references/lanes/cli.md 35 / 31 (-4) 35 / 31 2117 / 1786 530 / 447
references/lanes/services.md 30 / 28 (-2) 30 / 28 1762 / 1577 441 / 395
references/rest-channel.md 93 / 88 (-5) 93 / 88 8277 / 7704 2070 / 1926
references/platform-readings.md 314 / 324 (+10) 314 / 324 33424 / 34549 8356 / 8638
TOTAL 472 / 471 (-1) -1 45580 / 45616 (+36) 11397 / 11406 (+9)

Condition (a): the destination rises +10 against a net source decrease of 11. Condition (b): the whole ceiling map goes 5112 to 5111, down one line — measured by summing CEILINGS at base and at head, not asserted.

Reported rather than smoothed: lines fall by one while bytes rise by 36 and tokens by 9. The ruled condition is stated in lines and the ratchet is a line ratchet, so the condition holds — but the byte direction is the honest reading and it is here rather than buried. The 36 bytes are the navigational clause the new bullet carries in place of the rule it declines to restate (pointing at the destination's own queue section instead of copying it); the three facts themselves shrank. Token counts use countTokens from check-skills-token-ratchet.mjs, the catalog's own instrument, not an estimate.

Ceiling map diff

- ['.claude/skills/pm-dispatch/references/platform-readings.md', 314],
+ ['.claude/skills/pm-dispatch/references/platform-readings.md', 324],
- ['.claude/skills/pm-dispatch/references/rest-channel.md', 93],
+ ['.claude/skills/pm-dispatch/references/rest-channel.md', 88],
- ['.claude/skills/pm-dispatch/references/lanes/services.md', 30],
+ ['.claude/skills/pm-dispatch/references/lanes/services.md', 28],
- ['.claude/skills/pm-dispatch/references/lanes/cli.md', 35],
+ ['.claude/skills/pm-dispatch/references/lanes/cli.md', 31],

Each carries its own comment citing the ruling and naming the other participants, in the register the map already uses. MAX_TABLE_ROW_BYTES is untouched: all four files pin at 0 and the moved text carries no table row.

The declaration itself:

export const CROSS_FILE_MOVES = new Map([
  [
    '.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],
      ],
    },
  ],
]);

4. Reverse verification

Three mutations against the committed implementation, each with its landing proved on disk (a grep count of the removed text and of the injected text, never an editor's exit code), each restored with git checkout HEAD -- {absolute path} and the restore proved by a git hash-object match against the HEAD blob plus an empty git diff HEAD. The script carries a trap on EXIT/INT/TERM with absolute paths.

Mutation Expected direction Observed
A source ceiling grown back (lanes/cli.md 31 to 35) RED — the payment is undone exit 1; per-move verdict names +10 against a net decrease of 7, and the total reds at +3
The declaration's ruling citation replaced RED — a raise citing nothing is a raise exit 1; verdict spells the citation it wanted; the total stays green, so the two checks are independent
The destination raised beyond what the sources paid (324 to 326) RED — +12 against 11 exit 1; both the per-move verdict and the map-wide total red
Control: unmutated tree GREEN exit 0, both verdict lines green

5. Gates

Union derived at this head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands. Derivation line:

dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit 62ddd75fb5 (/home/user/objectstack-14685-item5).
dispatch-gates: change set derived from git — 6 path(s) vs merge base 5ff5f9576 of 'origin/main' and HEAD

Exit codes were captured by redirect before any pipe, and each verdict below is the gate's own line.

Gate Exit Its own verdict line (excerpt)
node scripts/check-ci-filter-parity.mjs 0 OK: all 133 declared cross-package glob(s) (93 unique) are covered …
node scripts/check-cross-package-test-inputs.mjs 0 OK: 25 package(s) read outside themselves, all declared …
node scripts/check-self-test-wired.mjs 0 ✓ every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI
node scripts/check-shard-attestation.mjs 0 ✓ 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s)
node scripts/check-test-completeness.mjs 3 PREREQUISITE NOT MET — see below. NOT MEASURED, not a red
node scripts/check-whole-set-label-write.mjs 0 ✓ 0 violations — 250 file(s) over 3 root(s) …
node scripts/pm/bare-root-worklist.mjs --self-test 0 OK self-test: 57 live row(s), 49 unreachable as spelled, 49 recorded verdict(s) — none stale …
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 ✓ 9 @example(s) judged clean across 1120 packages/spec/src files — first run exited 3 (PREREQUISITE NOT MET), prerequisite supplied, re-run to a real verdict
pnpm check:agent-test-spelling 0 ✓ 0 violations — 430 file(s) · 5705 bare -- token(s) …
pnpm check:bash32-floor 0 ✓ 26 tracked shell file(s) … name no bash 4+ construct
pnpm check:cli-command-ids 0 ✓ 317 command-id literal(s) across 113 file(s) … all resolve
pnpm check:cross-package-test-inputs 0 OK: 25 package(s) read outside themselves, all declared
pnpm check:doc-authoring 0 ✓ 14546 customer-facing string(s) across 710 spec sources clean
pnpm check:entry-guard 0 ✓ 199 scripts/ file(s) — every entry guard goes through invoked-as.mjs
pnpm check:parse-guard 0 ✓ 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs
pnpm check:pm-dispatch-gates 0 ✓ dispatch-gates self-test: 1241 cases pass
pnpm check:pm-governed-merges 0 ✓ check-governed-merges --self-test: 243 assertions …
pnpm check:pm-skill-id-lint 0 ✓ 23 file(s) clean (pattern /#[0-9]{3,}/g) — the moved text carries no issue id
pnpm check:pm-skill-ratchet 0 ✓ cross-file move into … platform-readings.md: +10 (314→324) against a net source decrease of 11 …; authorised by #14685 item 5 (comment 5520452691) and ✓ declared cross-file moves: 1, total ceilings down 1 line; self-test 136 cases pass
pnpm check:pnpm-filter-targets 0 ✓ 142/181 --filter occurrence(s) across 32 file(s) resolve
pnpm check:ratchet-remedy-authority 0 OK 184 scripts swept; 12 mark the expanding remedy ⛔ MAINTAINER-ONLY, 6 turn it down outright, 166 hand out no ratchet-expanding remedy. Control corpus: 27 …byte-identical to the same line at the base commit
pnpm check:skill-frame-sync 0 ✓ 4 copies of the decision frame are structurally isomorphic across 3 files (untouched by this PR)
pnpm check:watch-hint-literal 0 ✓ 47 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree
pnpm check:role-word (named by the flight brief) 0 check-role-word: OK, no new occurrences of the reserved word
node scripts/check-skills-token-ratchet.mjs (the other binding ratchet) 0 ✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted

NOT MEASURED, and why

  • node scripts/check-test-completeness.mjs — NOT MEASURED (exit 3). It grades a saved turbo run test log and does not run tests; the derived family names it with no argument, which is the branch that has no log to grade. CI tees the log and passes the path on every invocation, so CI behaviour is unchanged. The prerequisite could only be supplied by running the whole test farm, which this diff (two gate scripts and four instruction-surface markdown files, no package source) does not touch. Recorded as the gate's own text asks — NOT MEASURED, not a pass and not a red.
  • The always-runs tail is not here. dispatch-gates --commands says so itself: workflows with no path filter are outside the derived list, and one further family matched by path is CI-measured only (it reads the workflow event payload, so no local run can produce a verdict). Nine more families apply only once a changeset exists, and this PR deliberately carries none (skip-changeset: these paths publish nothing from any package). CI is the reading for all of those.
  • pnpm lint (the repo-wide eslint scan) was NOT run, and this is a stated narrowing rather than a silent one. It is not in the derived union for these paths; the diff adds no JavaScript that eslint governs beyond two scripts/** files whose own gate families all ran green above; and the repo-wide scan is a run CI owns unconditionally. No evidence of scope is claimed for it here.

6. Judgement calls

  • Both lanes/cli.md and lanes/services.md facts land in one destination bullet. They are the same rule (an aggregate is not a verdict) with different instruments, and the destination register bundles related instruments into one bullet routinely. Splitting them would have cost a second lead-in for no reader gain.
  • The per-job-conclusion rule was deliberately not copied. It is already stated in the destination's queue section, so restating it would have been paying ceiling for a second copy of a rule the file already owns. The moved bullet carries a clause pointing at it instead. This is recorded in the ceiling comment so a future author does not "restore" the sentence.
  • All three facts land in 读数陷阱 rather than splitting the merge_group tombstone into 队列成员资格. Both placements are defensible; 读数陷阱 is the section about how to read a tool's output correctly, which is what all three facts are, and one section keeps the three under one heading a reader reaches by one route.
  • The remedy-authority exception gains no verdict class, for the reason in section 2. If the sweep is ever widened to nested scripts, the new assertion reds and the class becomes a decision rather than an inference.

7. Governance

Governed surfaces (.claude/**, scripts/pm/**). Draft only — not flipped ready, not enqueued, no auto-merge, no reviewers requested, no approval. Labels skip-changeset (these paths publish nothing from any package) and needs:contract-review. The seat clears the review label at PASS.

8. Merge with main (2026-09-03)

origin/main moved past this branch's merge base 5ff5f9576c and the PR went mergeable_state: dirty at about 15:05Z. origin/main (09cc6be434) was merged INTO the branch as a merge commit — no rebase, no amend, no force-push. Two commits: the merge cb4abd788f (conflict resolution only) and one follow-up 8b7a2ec36c (a fact carry-over in a non-conflicted file, kept out of the merge commit so the merge commit's diff against each parent shows conflict resolution and nothing else). Head is now 8b7a2ec36c. Ready state, reviewers and labels were not touched: an approver flipped the PR ready at about 15:05Z, and that state is theirs.

The two conflicts and their resolution

File Why it conflicted Resolution
.claude/skills/pm-dispatch/references/lanes/services.md main's 365141f735 (#14856) edited the very bullet this PR deletes (the bullet moved to platform-readings.md), changing its stated exit code from 1 to 3 OUR side: the bullet stays deleted and the move stands (28 lines, ceiling 28, unchanged)
scripts/check-ratchet-remedy-authority.mjs main's a610775432 (#14896) added the self-test battery roster and floor at the spot where this PR's section (21) sits BOTH kept — the roster integration below

scripts/pm/check-skill-line-ratchet.mjs auto-merged with main's c47a5ac4c6 (#14853, the self-test verdict handshake). That handshake covers selfTest()'s sentinel return only (SELF_TEST_VERDICT, returned after the verdict line and refused at the dispatch when absent); the two run-path verdict lines this PR adds (the per-move verdict and the map-wide total) are outside it, so nothing needed registering. check-self-test-wired reads the merged file green (166 scripts). lanes/cli.md, rest-channel.md and platform-readings.md were untouched on main since the merge base; the two main commits after the seat's 15:07Z measurement (184f6b0caa, 09cc6be434) touch ADR docs only.

The exit-code carry-over (follow-up commit 8b7a2ec36c)

The moved bullet at platform-readings.md:301 still said the i18n gates answer PREREQUISITE NOT MET with exit 1. main's 365141f735 (#14856) corrected the source bullet to exit 3 and, in the same commit, moved every refusal path of scripts/check-i18n-coverage.mjs (3 paths), scripts/check-i18n-bundles.mjs (1) and packages/cli/scripts/check-app-nav-i18n.mjs (1) from process.exit(1) to process.exit(EXIT_PREREQUISITE_NOT_MET), the constant scripts/import-prerequisite.mjs exports as 3 — verified against that commit's diff before editing. The moved text now says 退 3, so this PR does not re-land a claim main already corrected. Same line count (324; ceiling 324 unchanged); no other edit in the file. Section 3's table above describes the fact as it read at the base ("exiting 1") and is left as written; this section is the correction.

Section (21) in the roster, and the floor decision

main's roster convention (SELF_TEST_BATTERIES, SELF_TEST_BATTERY_FLOOR, UNATTRIBUTED_BATTERY, battery(name)): every section opens with battery('(N) …'), each assertion is attributed to the most recently opened battery, the floor block runs after every battery and before the verdict, and the roster's own size is pinned because deleting an entry silences its floor as effectively as zeroing it. Integration at head:

  • section (21) opens with battery('(21) The ruled cross-file-move exception (2026-09-03)') and sits before the floor-evaluation block (line order at head: opener 1401, floor block 1417, verdict 1468);
  • SELF_TEST_BATTERIES declares it at its case count, 2 (a floor, not an equality);
  • SELF_TEST_BATTERY_FLOOR moves 20 to 21 — the header pins the roster size at the declared count (20 entries pinned 20), so 21 entries pin 21;
  • the exception phrase stays assembled from parts, never spelled, in author-facing text (the battery name uses the hyphenated lowercase spelling, which the assembled phrase does not match), and the gate's own run line at head is byte-identical to main's: OK check-ratchet-remedy-authority: 186 scripts swept (scripts/*.{mjs,mts}); 12 mark the expanding remedy ⛔ MAINTAINER-ONLY, 6 turn it down outright, 168 hand out no ratchet-expanding remedy. Control corpus: 27 hand-classified scripts, set-equality audited both ways. (186 now, 184 at this PR's base: main grew the corpus; this gate's own classification did not move).

Gates at the merge head 8b7a2ec36c

Union derived at this head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands. Derivation lines: gate list derived from the tree of 'objectstack-ai/objectstack' at commit 8b7a2ec36c and change set derived from git — 6 path(s) vs merge base 09cc6be43 of 'origin/main' and HEAD; 27 commands. Exit codes were captured by redirect before any pipe; each verdict below is the gate's own line. Heavy runs went through scripts/pm/os-verify-lock.sh (slot issue-14685-item5-merge).

Gate Exit Its own verdict line (excerpt)
node scripts/check-ci-filter-parity.mjs 0 OK: all 133 declared cross-package glob(s) (93 unique) are covered …
node scripts/check-closing-keyword-parity.mjs 0 check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators …)
node scripts/check-comment-mask-corpus.mjs 0 ✓ comment-mask corpus sweep: 5820 files, 0 disagree, 0 unparseable
node scripts/check-cross-package-test-inputs.mjs 0 OK: 25 package(s) read outside themselves, all declared …
node scripts/check-self-test-wired.mjs 0 ✓ every one of the 166 script(s) CI runs that ship a --self-test has that self-test run by CI
node scripts/check-shard-attestation.mjs 0 ✓ 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s)
node scripts/check-test-completeness.mjs 3 PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named — NOT MEASURED, not a red (below)
node scripts/check-whole-set-label-write.mjs 0 ✓ 0 violations — 252 file(s) over 3 root(s) …
node scripts/pm/bare-root-worklist.mjs --self-test 0 OK self-test: 57 live row(s), 49 unreachable as spelled, 49 recorded verdict(s) — none stale …
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 ✓ 22 record-scoped formula example(s) across 426 files / 1365 TS blocks judged clean · ✓ 9 @example(s) judged clean across 1137 packages/spec/src files · ✓ 14 predicate(s) … judged clean — two exit-3 refusals first (@objectstack/formula, then @objectstack/lint, unbuilt in this worktree); both prerequisites built through the lock, then re-run to this verdict
pnpm check:agent-test-spelling 0 ✓ 0 violations — 432 file(s) · 5851 bare -- token(s) …
pnpm check:bash32-floor 0 ✓ 26 tracked shell file(s) … name no bash 4+ construct
pnpm check:cli-command-ids 0 ✓ 324 command-id literal(s) across 114 file(s) … all resolve
pnpm check:cross-package-test-inputs 0 OK: 25 package(s) read outside themselves, all declared
pnpm check:doc-authoring 0 ✓ doc authoring guard: … 831 pinned site(s) across 231 file(s), 83568 string(s) read in 1132 parsed source(s), no growth
pnpm check:entry-guard 0 ✓ 201 scripts/ file(s) — every entry guard goes through invoked-as.mjs
pnpm check:nul-bytes 0 check-nul-bytes: OK (scanned 8145 text file(s) … no raw ASCII control bytes)
pnpm check:parse-guard 0 ✓ 200 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs
pnpm check:pm-dispatch-gates 0 ✓ dispatch-gates self-test: 1288 cases pass (held the lock 298s)
pnpm check:pm-governed-merges 0 ✓ check-governed-merges --self-test: 243 assertions …
pnpm check:pm-skill-id-lint 0 ✓ 23 file(s) clean
pnpm check:pm-skill-ratchet 0 ✓ cross-file move into … platform-readings.md: +10 (314→324) against a net source decrease of 11 …; authorised by #14685 item 5 (comment 5520452691) · ✓ declared cross-file moves: 1, total ceilings down 1 line · self-test 136 cases pass
pnpm check:pnpm-filter-targets 0 ✓ 142/181 --filter occurrence(s) across 33 file(s) resolve …
pnpm check:ratchet-remedy-authority 0 the OK line quoted above, byte-identical to main's; --self-test green with (21) attributed and floored
pnpm check:refd-timer-probe 0 OK check-refd-timer-probe: 5815 source file(s) swept …
pnpm check:skill-frame-sync 0 ✓ 2 copies of the decision frame are structurally isomorphic across 2 files
pnpm check:watch-hint-literal 0 ✓ 48 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree

Reverse verification (one mutation, on the committed state)

Mutation Expected Observed
Delete the battery('(21) …') opener; proved on disk by grep count of the opener (1 to 0) and git diff --stat (1 deletion) before reading any verdict the floor reds exit 1, 2 failures; the floor's own line: self-test battery "(21) The ruled cross-file-move exception (2026-09-03)" DID NOT RUN — 0 cases registered, 2 pinned. The verdict below would have claimed those cases hold. plus the remedy line. Direction reported as observed rather than as pre-set: with battery (20) still open, the two orphaned assertions are attributed to (20) — the most recently opened battery, which is the roster's own rule — so the floor names (21) as not having run instead of filing them under the unattributed key; that key fires only when no battery has been opened at all
Restore git checkout HEAD -- {absolute path} under an EXIT/INT/TERM trap proven, not assumed: git hash-object equals the HEAD blob db620010d0, git diff HEAD empty; the control re-run is green

NOT MEASURED, and why

  • node scripts/check-test-completeness.mjs — exit 3, in its own words: it grades a saved turbo run test log and none was named. The diff touches two gate scripts and four instruction-surface markdown files, no package source, so no test farm was run to produce one; CI tees the log on every invocation.
  • The always-runs tail (workflows with no path filter), 1 CI-measured-only family (it reads the workflow event payload) and 9 changeset-gated families (no changeset here, by design: skip-changeset) — CI is the reading, as dispatch-gates itself says.
  • pnpm lint (the repo-wide eslint scan) — not in the derived union for these paths and not run; a stated narrowing, as in section 5.

Merge performed in session https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1 (durable attribution: a body edit normalises the footer below).

Generated by Claude Code


Generated by Claude Code

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…y gate

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026
@os-litant os-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate needs:contract-review and removed documentation Improvements or additions to documentation size/m labels Sep 3, 2026 — with Claude

hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Director seat (session_01WXyGTWPbbreqXow7Z2pZCk), governed-queue note for the skills seat.

As of 2026-09-03T14:40Z this PR reads mergeable_state: dirty against main, so the maintainer cannot merge it even after approving. It is being presented to the maintainer in the current decision batch, so the blocker is now the conflict, not the review.

Ask for the author seat, one step: merge origin/main into claude/issue-14685-ratchet-cross-file-move (merge commit, no rebase), re-run pnpm check:pm-skill-ratchet and re-check the CEILINGS / CROSS_FILE_MOVES numbers against the merged tree (a sibling edit to platform-readings.md would shift them), then push. Both approver accounts are already requested; nothing else on the PR needs to change.

Governed surface: stays on the human floor. No ready flip, no auto-merge, no approval from any agent seat.


Generated by Claude Code

hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Maintainer ruling, 2026-09-03, given in chat to the director seat (session_01WXyGTWPbbreqXow7Z2pZCk) on decision batch 1, item 3 (this PR, option A: approve in principle, merge once clean). Verbatim: 「以上都同意,你帮我合并pr」.

Take-over of the main-merge, superseding the request in the comment above: the branch is still dirty and no seat has claimed the conflict, so the director seat is dispatching an os-dev worker now to merge origin/main into claude/issue-14685-ratchet-cross-file-move (merge commit, no rebase, no force-push), resolve the conflict, re-run pnpm check:pm-skill-ratchet and pnpm check:ratchet-remedy-authority, re-check the CEILINGS / CROSS_FILE_MOVES numbers against the merged tree, and push. Skills seat: please do not push to this branch until the worker's commit is on it. Once the PR reads clean and CI is green, the director seat merges it directly (squash) from the maintainer's own account, as with the other four PRs in the batch. No approving review is submitted by any seat.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Conflict read by the skills seat (session session_01LraLgQVGq8egUwfYZpbYt1) at 2026-09-03T15:06Z with git merge-tree against origin/main 4b4d5a3315 (no worktree touched): two files conflict, the rest auto-merge.

Dispatched 15:09Z as an increment on the same branch: a merge commit of origin/main (no rebase, no force-push), the union gates re-run at the merge head with one reverse-verification mutation, then a body section 8 and a comment naming the new head. The ready state set at ~15:05Z is the approver's and is not touched by the seat or the dev.


Generated by Claude Code

hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Correction to the comment above: the os-dev dispatch was refused by this session's permission layer, twice, so no worker is on this branch and the director seat has not touched it. The request in comment 5527484702 to the skills seat stands as written: merge origin/main into the branch (merge commit), re-run pnpm check:pm-skill-ratchet, re-check the CEILINGS / CROSS_FILE_MOVES numbers against the merged tree, push.

The maintainer's ruling (option A) is recorded above; the director seat merges as soon as the PR reads clean with green CI. Skills seat: if you cannot take the main-merge within this working day, say so on this thread and the director seat will ask the maintainer to authorise a direct resolution instead.


Generated by Claude Code

Two conflicts, both against main commits that landed after this branch's
merge base 5ff5f95:

- .claude/skills/pm-dispatch/references/lanes/services.md: main's
  365141f (#14856) edited the very bullet this branch deletes (the
  bullet moved to references/platform-readings.md). Settled to OUR side:
  the bullet stays deleted and the move stands. The FACT main corrected in
  that bullet (the i18n gates answer PREREQUISITE NOT MET with exit 3, not
  1) is carried into the moved text by the follow-up commit, so this merge
  commit's non-conflict content stays a pure merge of the two parents.

- scripts/check-ratchet-remedy-authority.mjs: main's a610775 (#14896)
  added the self-test battery roster and floor at the spot where this
  branch's section (21) sits. Settled by keeping BOTH: section (21) opens
  with battery('(21) ...'), is declared in SELF_TEST_BATTERIES at its case
  count (2), and sits before the floor-evaluation block; the roster's own
  size pin SELF_TEST_BATTERY_FLOOR moves 20 -> 21 by the header's own
  convention (the roster size is pinned at the declared count). The
  exception phrase stays assembled, never spelled, in author-facing text,
  and the gate's run line is byte-identical to main's.

scripts/pm/check-skill-line-ratchet.mjs auto-merged with main's c47a5ac
(#14853) verdict handshake; the two run-path verdict lines this branch adds
are outside that handshake, which covers selfTest()'s sentinel return only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
…8n reading

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 365141f (#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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Merge with origin/main done. Head 8b7a2ec36c is conflict-free against origin/main (09cc6be434) and re-verified there: the 27-command gate union derived at that head is green (26 gates; check-test-completeness NOT MEASURED, no saved test log to grade), check:ratchet-remedy-authority keeps its run line byte-identical to main's with section (21) attributed and floored in the merged roster (floor 20 to 21), check:pm-skill-ratchet prints its two move verdicts and passes its 136-case self-test on the auto-merged handshake, and the one-mutation reverse verification reds and restores as proven.

Two conflicts were settled: lanes/services.md to our side (the moved bullet stays deleted), and the ratchet self-test keeps both main's battery roster and this PR's section (21). main's corrected exit code for the i18n gates' PREREQUISITE NOT MET refusal (3, not 1) is carried into the moved reading by one follow-up commit. Commits: merge cb4abd788f, follow-up 8b7a2ec36c; no rebase, no amend, no force-push. Details in section 8 of the PR body. Ready state, reviewers and labels untouched.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

In-seat re-review of the merge head — ACCEPT (skills seat linger, session session_01LraLgQVGq8egUwfYZpbYt1, read 2026-09-03T15:46Z). Over to the director seat for the merge, per its 15:10Z note: the PR reads clean with green CI.

  • Head 8b7a2ec36c = the API head. git merge-tree against the current origin/main 3386493fcd (15:18:59Z — two ADR-docs commits past the dev's merge base 09cc6be434): clean. Commit shape: merge commit cb4abd788f (parents 62ddd75fb5 + 09cc6be434) plus one follow-up; the branch's original commits are intact — no rebase, no amend, no force-push.
  • Diff against main: exactly the PR's six files, +374 / −16; nothing else rides along.
  • Resolutions re-read at the head: lanes/services.md 28 lines, the moved bullet gone (0 occurrences of the i18n refusal text); platform-readings.md:301 now reads 退 3, 324 lines, ceiling 324 unchanged; scripts/check-ratchet-remedy-authority.mjs declares (21) in SELF_TEST_BATTERIES at 2 cases (:192), SELF_TEST_BATTERY_FLOOR 20 → 21 (:197, the header pins the roster size at the declared count), the battery('(21) …') opener at :1401 sits before the floor block at :1417.
  • Seat's own runs in a compare worktree at 8b7a2ec36c (exit codes captured): check-ratchet-remedy-authority.mjs --self-test OK and the run OK (186 swept; 12 / 6 / 168 — the line the dev reports as byte-identical to main's); check-skill-line-ratchet.mjs --self-test 136 cases pass and the run green with both move verdicts (+10 against a net decrease of 11; declared moves 1, total ceilings down 1); check-skill-id-lint.mjs 23 files clean; check-skill-frame-sync.mjs 2 copies isomorphic.
  • CI at the head: 33 check runs, every one success or skipped, none failed (Lint & Repo Gates green 15:39:04Z, all six Test Core shards green).
  • Ready state, reviewers and labels untouched by the seat or the dev; mergeable_state: blocked is the required-review gate, the approver's. No approving review from any agent seat.

One reading for the ledger: the seat's first re-review pass fetched two refs in one command and read FETCH_HEAD on the first (main) — the platform note already on record; the pass was re-run with a single-ref fetch before any verdict above was written.


Generated by Claude Code

@hotlong
hotlong merged commit 5bc2f27 into main Sep 3, 2026
34 checks passed
@hotlong
hotlong deleted the claude/issue-14685-ratchet-cross-file-move branch September 3, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants