tri skill renumber: move appended sections to the numbers the base left free - #3052
Conversation
…ft free Closes #3051 Six section-number collisions in one week on one file. Two of them hit the same branch four hours apart: renumbered to 468/469, master then took 468, 469 and 470, and it moved again to 471/472. The repair never varied -- rebuild from origin/master, re-append with the next free numbers, assert the master prefix is byte-identical, re-run `tri skill check` -- and nothing in it needed judgement. Only the assertion needed care, because the file is 12,000 lines and a lost section looks exactly like a file that never had it. The command leans on the invariant the workflow already has: a section is APPENDED, so the branch's file is the merge base's file plus a tail, and the tail is what moves. When that is false it refuses and says so; it cannot tell which lines are yours once an existing section has been edited, and a quietly guessed split point would be worse than the manual repair. Three mutants, three kills, and the third one is the point. Dropping the word boundary in the reference rewriter (`§110` becoming `§4710`) and accepting a non-append silently were both caught by the first six tests. Taking the first number from the TAIL instead of the base -- the exact line behind the second collision -- SURVIVED all six, because that decision lived in the integration path. It moved into `plan()`, and the new test fails when two tails carrying different existing numbers plan differently. Demonstrated on itself: this change's own section was written as `## 999.` and the command placed it at 471.
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
…d never glue a heading Refs #3051 Two defects found by the command's FIRST real use, on the conflict it was written for. One: the byte-prefix test refused, because master had REWORDED an existing section while the branch was open. The branch's file is then not any commit's file plus an append -- it is an older base plus a tail with the base moved underneath it in the middle. Falls back to identifying the tail by shared section TITLES, and prints which method it used. Two, and this one it shipped: a tail found by title starts AT its first `## ` heading with no leading newline, so concatenating it onto a trimmed base glued the heading to the base's last line and it stopped being a heading. 436 sections became 435. The count caught it; reading the file did not. Fixed with an explicit joiner and a pre-write guard: the rebuilt file must have exactly base + tail sections or nothing is written. Mutating the joiner back kills the new test. Also: when the numbers already follow the base but the base region is stale, the file is now rebuilt rather than left alone -- that is the other half of the same repair, and it prints what it discards (any edit to a section the base already had) before doing it. 544 -> 547 tests.
First real use, two defects, one of them shippedI pointed this command at the conflict it was written for — PR #3044, the seventh collision on this file — and it found two things. It refused, correctly. Master had reworded an existing section while that branch was open, so the branch's file was not any commit's file plus an append: it was an older base plus a tail, with the base moved underneath it in the middle. The byte-prefix test cannot see a tail there, and refusing was the right answer. Added a fallback that identifies the tail by shared section titles — a section whose title is in the base is the base's, however its body was reworded — and the output now says which method it used. Then it shipped a defect, and the count caught it. A tail found by title starts at its first It stopped being a heading. 436 sections became 435. I noticed because I asserted Fixed two ways, because one of them is a guard and the other is the bug:
Also added: when the numbers already follow the base but the base region is stale, the file is now rebuilt instead of left alone — that is the other half of the same repair — and it prints what it discards (any edit you made to a section the base already had) before doing it. PR #3044 is repaired and pushed: 436 sections, master prefix asserted byte-identical, my two sections at 471 and 472. Correction to the last commit message: it says "544 -> 547 tests". The real figure is 544 -> 545. |
PR DashboardGenerated at: 2026-09-03 20:00:38 UTC
Summary
Seal Status
|
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
…r than master Refs #3051 Eighth collision, and the first one BETWEEN two of my own open branches rather than with master: #3053 and this one both claimed 471. Resolved with the command's own --base flag pointed at the sibling branch instead of master, which is what that flag is for and had not been used in anger before. Note for the tool: it can only see one base at a time, so N open branches still need N-1 sequential renumbers in a chosen order. It does not discover siblings.
Refs #3051 Correcting the previous commit on this branch, which claimed to renumber to 475 and did not. The command REFUSED -- "no merge base with origin/w48b-tri-fmt", because `git fetch origin <branch>` updates FETCH_HEAD and not refs/remotes/origin/<branch> unless the remote's refspec covers it -- and I piped its output through `sed -n '2,14p'`, which swallowed the error line. My own check then printed "prefix identical: True" against an EMPTY base string, which is vacuous and read as confirmation. The section stayed at 471, colliding with master, and was pushed that way. Two guards would each have caught it: read the command's exit code rather than its first fourteen lines, and assert the base is non-empty before comparing against it. Both are in the check I now run after every renumber. Then the substantive finding. With the ref fetched properly, `--base origin/w48b-tri-fmt` numbered correctly around the sibling -- and rebuilt my file ON the sibling, so this branch carried the sibling's two sections and would have merged them under this PR. `--base` is for a different base BRANCH, not for a peer. Number every branch against the shared base; whoever merges second conflicts and renumbers, which is now one command. Recorded in the module doc and in the flag's own help text, where the next person meets it. Section is 473 against origin/master. 553 tests.
PR DashboardGenerated at: 2026-09-03 20:20:15 UTC
Summary
Seal Status
|
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
1 similar comment
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
PR DashboardGenerated at: 2026-09-03 20:24:50 UTC
Summary
Seal Status
|
Refs #3051 The `--base origin/<sibling>` trap was left as a paragraph in the doc, and a paragraph is what this command's own section says is not enough. `--first N` is the thing the paragraph was standing in for: both branches number against the SHARED base, and the second one starts higher. A number, not a different base -- pointing --base at a peer rebuilds your file on it and carries its sections into your PR. A start at or below the base's highest is refused. Starting there collides with the very file being numbered against, which is the one thing this command exists to prevent, and the refusal names the floor so the caller can pick. Two mutants, two kills, each by its own test: accepting any --first kills the refusal test alone; ignoring --first and always deriving kills both. 555 tests.
Added
|
| mutant | result |
|---|---|
accept any --first (drop the collision guard) |
a_requested_start_that_would_collide_is_refused FAILED (1 passed / 1 failed) |
ignore --first and always derive |
both tests FAILED (0 passed / 2 failed) |
| restored | 2 passed |
cargo test -p tri --bins → 555 passed, 0 failed, 0 filtered.
Also: auto-merge is now on for this PR. Five of my branches were chasing a master that moves every few minutes, and the sync-on-BEHIND loop was restarting ~35 checks each time — a treadmill, not progress. --auto lets GitHub merge each when its own checks settle.
PR DashboardGenerated at: 2026-09-03 20:34:55 UTC
Summary
Seal Status
|
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Refs #3051 Ninth collision, and the first use of --first in anger. #3053 is open with two sections that will land at 476/477, so this one starts at 478 -- numbered against the SHARED base, which is the whole point of the flag: --base pointed at that sibling would have rebuilt this file on it and carried its sections into this PR.
Refs #3051 Tenth collision. Also a correction to the previous commit's reasoning: I said a multi-ref `git fetch origin master w48b-tri-fmt` had left origin/master stale. It had not -- the parent of that merge is 6c54ab3, which was the tip at the time, and #3056 landed afterwards. Master moved between the merge and the check. The w48b-tri-fmt ref failure earlier in the session WAS real; conflating the two was my error.
PR DashboardGenerated at: 2026-09-03 21:48:22 UTC
Summary
Seal Status
|
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Refs #3051 The previous commit's own check printed `OK=False` -- 442 sections where 440 were expected -- and I pushed anyway. The guard fired and I did not read it, which is the whole failure; the corruption is downstream of that. What it did: after resolving the conflict with `git checkout HEAD -- <file>`, the tail no longer stood in the append relation the command needs, and the renumber moved a slice that included master's own 475 and 478. The file ended with three duplicated titles. Rebuilt as origin/master plus my one section, asserted: 440 sections, master prefix byte-identical, zero duplicate titles. The lesson is not about the tool. A count that disagrees with its expectation is a stop, not a line of output to scroll past -- which is what every section in this file about population counts says, in the file this commit edits.
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
PR DashboardGenerated at: 2026-09-03 21:49:30 UTC
Summary
Seal Status
|
Closes #3051
Measured
Six section-number collisions in one week on
.claude/skills/ci-gates/SKILL.md. Two of them hit the same branch four hours apart: renumbered to 468/469, master then took 468, 469 and 470, and it moved again to 471/472.The repair never varied — rebuild from
origin/master, re-append with the next free numbers, assert the master prefix is byte-identical, re-runtri skill check. Nothing in it needed judgement. Only the assertion needed care, because the file is ~12,000 lines and a lost section looks exactly like a file that never had it.What it does
Leans on the invariant the workflow already has: a section is appended, so the branch's file is the merge base's file plus a tail, and the tail is what moves.
--checkreports the moves and writes nothing.--baseand--filedefault toorigin/masterand the ci-gates skill.It refuses rather than guesses. If the file is not the merge base plus an append — an existing section was edited, or a previous conflict was hand-resolved — it stops and says so. The whole method rests on that invariant, and a command that quietly picked a split point would be worse than the manual repair it replaces.
Evidence
cargo test -p tri --binsrenumfilter#[test]count in the module§469→§472; foreign§447untouched;§4470untouchedtri skill checkafterMutation — and the third one is the point:
§110becomes§4710a_prefix_of_a_longer_number...FAILEDan_append_is_recognised_and_an_edit_is_notFAILEDThat last mutant is the exact line behind the second collision, and no test could see it because the decision lived in the integration path. It moved into
plan(), and the new test fails when two tails carrying different existing numbers plan differently. With that test in place the mutant is killed, 6 passed / 1 failed.Demonstrated on itself
This change's own section was written as
## 999.and the command placed it at 471. The section also confesses a defect it created: it first said "by the rule two sections up", and a positional cross-reference is broken by the very renumbering this command performs — the section it pointed at is on an unmerged branch and will land somewhere else. Name the rule, not its address.What it will not do
It cannot help when master wrote the same lesson, not just the same number. Numbering is the mechanical half; two people writing one lesson twice is a different problem and this tool has no opinion about it.