Skip to content

tooling(pm): derive the line ratchet's lane roster from the lanes/ directory - #16437

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-15965-ratchet-lane-roster-by-construction
Sep 7, 2026
Merged

tooling(pm): derive the line ratchet's lane roster from the lanes/ directory#16437
os-steve merged 1 commit into
mainfrom
claude/issue-15965-ratchet-lane-roster-by-construction

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #15965

The self-test's lane-roster case was an .every over eight literal lane names, so the ninth lane file (lanes/triage.md, landed by #15958) was pinned by nothing: a ceiling row put there for it kept the case green without the case ever naming it, and deleting that row again was caught only by the map-wide cases, which say nothing about a row that is simply gone.

The roster is now read from the lanes/ directory (readdirSync, every *.md, sorted) and held against CEILINGS, so the tree and the map are two independent sources checked against one another. A new lane file is pinned by construction: no list to extend, and no count in the label to keep in step with the list.

A derived roster has one failure mode of its own, and it is the same shape as the defect it repairs — every() over an empty array is true, so an unreadable directory would report perfect coverage of nothing. The verdict refuses an empty roster outright, and each red path carries a fixture case beside the live one, because a red path that stopped working runs green forever.

One file, self-test only: scripts/pm/check-skill-line-ratchet.mjs. No ceiling row changed.

Every move, named

what moved before after
self-test cases run 155 157
SELF_TEST_BATTERY_FLOOR (the roster-size pin) 155 157
header prose "ALL n rows are floored" 155 157
header prose "three independent readings agree on n" 155 157

The pinned case label and its expectation-table entry moved together in the same commit — that table is half of the duplicate-label refusal. One case became three: the live derived-roster case, plus one fixture case per red path. Nothing else in the battery-floor machinery, the H-rows or the pins was touched.

Reverse verification

Run against the committed implementation, on scratch copies placed at scripts/pm/ depth so REPO_ROOT still resolves. Each mutation was proved on disk by occurrence count before and after, not by an editor's exit code; the copies were removed afterwards, git diff HEAD is empty, and the worktree blob b7db6099e4 is identical to the HEAD blob.

(a) the defect. Pre-edit file, with lanes/triage.md's ceiling row and its table-row pin both removed (occurrences (1,1) on disk before, (0,0) after) — the realistic shape of deleting a lane's coverage:

LEG_A2_EXIT=0
  ✓ all eight lane/seat job descriptions are covered
✓ check-skill-line-ratchet self-test: 155 cases pass.
--- reds --- (NONE)

The ninth lane file leaves the map entirely, the self-test exits 0, and the roster case whose job this is stays green.

Removing only the ceiling row and leaving the pin row behind reds a different case (and the pin map names no file the ceiling map does not cover) on the asymmetry alone — the roster case stays green there too, which is why the both-rows leg above is the honest demonstration rather than the one-row leg.

(b) the fix. Edited file, same both-rows removal:

LEG_B2_EXIT=1
  ✗ every lane/seat job description in the tree carries a ceiling row
✗ check-skill-line-ratchet self-test: 1 failure(s) (cases and floor).

It is the only red, and its verdict message names the file:

.claude/skills/pm-dispatch/references/lanes/triage.md — lane job description(s) in the tree
carrying no ceiling row, so the ratchet does not read them at all. Give each one a row keyed
by that path, or take the lane file out of the tree.

(c) restored. ✓ check-skill-line-ratchet self-test: 157 cases pass. at exit 0, and the live gate run at exit 0 with zero lines — every pin holding, pm-dispatch/SKILL.md at 811 lines against its ceiling of 811, lanes/triage.md at 7 against 7.

Serial merge-tree proof

Both PRs were read from the API at proof time: #15955 and #16357 are open, not merged, at exactly the heads this proof targets. So the proof ran as ruled — twice, against HEAD 2c9c1a5282, the second pass with the custom merge driver disabled, because a driver's exit 0 only means the driver took the file.

run exit resulting tree
git merge-tree --write-tree HEAD pr15955 (04b61b5f6a) 0 ed013ccf21
git merge-tree --write-tree HEAD pr16357 (f5c08e9589) 0 e9021cb245
git -c merge.os-regen.driver=false merge-tree --write-tree HEAD pr15955 0 ed013ccf21
git -c merge.os-regen.driver=false merge-tree --write-tree HEAD pr16357 0 e9021cb245

The driver-disabled runs produce byte-identical trees, so no exit 0 above is a driver having absorbed the file. In both merged trees scripts/pm/check-skill-line-ratchet.mjs carries zero conflict markers and keeps this change intact (SELF_TEST_BATTERY_FLOOR = 157 present in each).

Gates

Derived with dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the final one-file list (the tool takes its own change set from the merge base ac76425f0, three-dot). All 31 ran, each exit captured by redirecting before any pipe:

  • 31 of 31 exit 0. Reconciled: ✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED.
  • ratchet self-test exit 0 (157 cases) · ratchet live run exit 0, zero reds
  • check-clause2-carriers --self-test exit 0 (190 cases)
  • check-governed-merges --test scripts/pm/check-skill-line-ratchet.mjs exit 0 — ✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.
  • check-nul-bytes exit 0 (8078 text files, no raw control bytes), plus a direct control-byte scan of the edited file: no hits
  • whole-repo pnpm lint (eslint . --no-inline-config) through scripts/pm/os-verify-lock.sh, slot issue-15965: VERDICT command-exit 0 · held the lock 99s · waited 0s
  • the artifact-roster gates whose roster sits under scripts/ or scripts/pm/ were run rather than read as silent: check-published-list-mirrors, check:pm-label-desc-cap, check:pm-governed-prose, check:single-claim-paths, check:skills-token-ratchet — all exit 0

check:ratchet-remedy-authority is worth calling out: it classifies this very file as excluded, and its OFFER_VERB grammar would have reclassified it had the new verdict message spelled its remedy with an expanding verb next to a declared registry name. The message is worded to stay out of that grammar; the gate and its self-test both stay green.

No changeset — repo tooling only, nothing published from any package; skip-changeset applied.


Generated by Claude Code

…rectory

The self-test's lane-roster case was an `.every` over eight literal lane
names, so the ninth lane file was pinned by nothing: a ceiling row put
there for it kept the case green without the case ever naming it, and
deleting that row again was caught only by the map-wide cases, which say
nothing about a row that is simply gone.

The roster is now read from the directory (`readdirSync`, every `*.md`,
sorted) and held against the ceiling map, so the tree and the map are two
independent sources checked against one another. A new lane file is
pinned by construction: no list to extend, and no count in the label to
keep in step with the list.

A derived roster has one failure mode of its own, and it is the same
shape as the defect above -- `[].every(...)` is `true`, so an unreadable
directory would report perfect coverage of nothing. The verdict refuses
an empty roster outright, and each red path carries a fixture case beside
the live one, because a red path that stopped working runs green forever.

Moves named: the pinned case label and its expectation-table entry move
together (duplicate-label refusal); one case becomes three, so the
self-test runs 155 -> 157 cases and SELF_TEST_BATTERY_FLOOR moves
155 -> 157, with the two prose readings of that same number moved with
it. No ceiling row changed; no other file.

Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox

Co-authored-by: Claude <noreply@anthropic.com>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 7, 2026
@github-actions github-actions Bot added the size/s label Sep 7, 2026
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Verdict: ACCEPT

Implemented-by: os-dev flight T (opus), branch claude/issue-15965-ratchet-lane-roster-by-construction, head 2c9c1a5282
Reviewed-by: pm-dispatch skills seat (contract-tier read in a detached compare worktree at 2c9c1a5282, merge-base ac76425f0b; origin/main has since taken one hunk in this file from PR #16357, the retirement row pin, outside the region this PR edits)

What was checked

  • One file, scripts/pm/check-skill-line-ratchet.mjs, +75/−6, self-test only: the literal eight-name roster case is replaced by laneFilesOnDisk() (a sorted readdirSync of the lanes/ directory, [] on a read failure) and laneRosterVerdict() (every file found has a CEILINGS row keyed by its repo-relative posix path; an empty roster is red, not vacuously green). The label carries no count. The expectation table moved in step, the battery floor 155 → 157 with its two prose readings, and the two red paths each carry a fixture case, as the file's own convention requires. No ceiling row changed.
  • Gates in the compare worktree: self-test 157 cases pass; the live ratchet run exits 0 with every pin holding; check-governed-merges --test exit 0, NOT governed. CI on the head: 23 success / 11 skipped / 0 failure across the six required contexts.
  • Serial proof as ruled: four merge-tree --write-tree runs (both PR heads, with and without the custom driver) all exit 0 with byte-identical trees per head and no conflict markers. PR docs(agents,skills,pm): make the remaining one-shot-executor fences executable, and re-lock the retirement pin #16357 has since merged; the proof against its head covers the merge the queue will perform.
  • Reverse verification, three legs quoted in the PR: the pre-edit file stays green with both triage rows removed (the defect); the edited file reds on exactly the new case, naming lanes/triage.md; restored, 157 green with the worktree blob equal to HEAD's.

Rulings

  • Open question (the reverse direction: a lanes/ row in CEILINGS whose file is gone): no card. The dev measured that run() already reds on a ceiling row it cannot read, so that direction is enforcement-held today; a second self-test belt on a held surface is below the finding admission threshold. Recorded under 验收备注, noted, not filed. B is refused for the reason the dev gave: it would widen a one-file flight after its serial proof was taken.

验收备注 (noted, not filed)

  • The reverse roster direction is held by the live gate's cannot-read rule, not by a self-test case; if that rule ever moves, the pair laneFilesOnDisk / CEILINGS is the place to add the mirror case.

Landing

Not governed, ordinary queue landing: the seat flips the draft to ready and arms auto-merge (SQUASH). On merge the seat strips pm:dispatched and clears the assignee on #15965.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 7, 2026 02:18
@os-steve
os-steve enabled auto-merge September 7, 2026 02:18
@os-steve
os-steve added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 32e57e5 Sep 7, 2026
36 checks passed
@os-steve
os-steve deleted the claude/issue-15965-ratchet-lane-roster-by-construction branch September 7, 2026 03:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s 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.

[finding] the line ratchet's lane roster self-test enumerates eight lane files by name — a ninth lane file's ceiling row is pinned by nothing

2 participants