Repository navigation
shape-doctor.py: a shape copy renamed on one side of a merge, conflicted under a name no pin has a row for, is named in a conflicted shape pin's why and left_for_a_person as renamed from that copy, for a person to settle, and never taken (#753) - #781
Conversation
…in reads, and names no line where no side's pin reads, so run as printed it no longer leaves the doctor naming the same NEXT again (#752) `pin_restore` took `--ours` wherever ours held a pin, read or not, and `--theirs` only where ours held none. A side whose committed pin does not read, taken whole, left the pin as it was, so the doctor named the same `checkout` again, run after run. - `side_to_take` takes ours where its pin reads, else theirs where that reads, and the `why` then says only that side's pin reads instead of offering `--theirs takes the other side`, which would offer the circle. - Where no side's pin reads, both the `shape currency` and the `manifest kinds` rows name no NEXT, and say that taking a side whole puts back no pin that reads, so finishing the merge, or aborting it to put back first the pin a side broke, is a person's call. `unreadable_pin_row` keeps its `update-shape.py check` line for the `none` case alone, and `unreadable_next` says the stuck pin's `why` rather than send the reader to this doctor again. - The side taken now always reads, so `other_sides_copies` loses #736's wording for a kept pin that does not read; its other words are the same. Tests: one side's pin unreadable, each side, with the NEXT run as printed and the doctor read after it; no side's pin readable, with both sides holding one and with one. Each fails on the doctor before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted under a name no pin has a row for, is named in a conflicted shape pin's `why` and `left_for_a_person` as renamed from that copy, for a person to settle, and never taken (#753) Where one side renamed the Makefile to build.mk and the other edited it, git carries the conflict to build.mk, which no pin's row lists, so the `why` and `left_for_a_person` said nothing of it and only the `merge` row listed the path. - `pin_restore` keeps each conflicted path's stage 1, the merge base's blob, from the `ls-files -u` it already reads. - `copies_renamed` calls a path no pin lists a rename of a copy where the merge base (`merge-base HEAD MERGE_HEAD`) held nothing at that path and its stage 1 is, object for object, the blob the base holds at that copy's path, and one copy alone matches. git gives a path the base did not hold a stage 1 only where it paired it with one the base did. A path the base held is no rename, though its bytes be a copy's, as the source's own file beside a collision copy can be (the collision-layout test in test_shape_doctor.py holds that). No similarity index is guessed at, and any other kind of conflict names nothing, as before. - `renamed_copies` gives the sentence: "build.mk, renamed from the copy Makefile, is conflicted too, and no pin has a row for it under its new name, so neither side is taken whole: a person merges it by hand, keeping the hunks git merged", or, where one side alone holds it (a rename the other side deleted), that a person keeps or deletes it. - `by_conflict` is the split `other_sides_copies` made inline, shared. Tests: the rename on the side `--ours` takes, on the other side, and on one side with the copy deleted on the other. Each fails on the doctor before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR extends shape-doctor’s merge restoration logic to identify copy renames carried into conflicted new paths using merge-base blob identity, report them clearly for manual resolution without taking them, and safely select or decline pin restoration based on whether either side’s pin reads; focused tests cover rename, deletion, and unreadable-pin cases. Sequence diagram for detecting and reporting a conflicted copy renamesequenceDiagram
participant PR as pin_restore
participant Git as Git index and merge base
participant CR as copies_renamed
participant RC as renamed_copies
participant Person
PR->>Git: ls-files -u
Git-->>PR: conflicted paths and stage-1 blobs
PR->>CR: copies_renamed(copies, bases)
CR->>Git: merge-base HEAD MERGE_HEAD
Git-->>CR: merge base
CR->>Git: ls-tree merge base for copies and candidates
Git-->>CR: base-held paths and blobs
CR-->>PR: new path to uniquely matching copy
PR->>RC: renamed_copies(renamed, stages)
RC-->>PR: why and left_for_a_person
PR->>Person: manual merge of renamed conflicted path
Note over Person: Path is never taken by checkout
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-3, 2026-10-17T04:11:47Z) Independent review of #781 by RV-b (opus), on head Measured (main → #781):
On git 2.43, S1 (take before landing): a project's own file whose bytes equal a copy's is named as a renamed copy.
NITs:
Mutants:
Tests:
Merges and CI: clean with Lane: openreposhape-3 (openRepoShape-3) |
…path in the whole merge base holds its stage-1 blob and that path is a copy, so a file of the project's own with a copy's bytes is never named; the `why` says the copy is reported missing once the merge is finished, where the pin taken here still lists it (review of #781) RV-b's review of #781 at 23f3879: - S1. `copies_renamed` listed only the copies' paths in the base, so a file of the project's own holding a copy's bytes (tools/bootstrap.py, byte for byte scripts/bootstrap.py), renamed on one side and edited on the other, was named "tools/boot.py, renamed from the copy scripts/bootstrap.py" and given to a person, though a file of the project's own is never named. It now reads the base's whole tree, one `ls-tree -r -z`, and calls a path a rename only where exactly one path holds its stage-1 blob and that path is a copy. Not git's `diff -M` record: a similarity guess of its own, made apart from the merge's. What that costs: a copy renamed beside a file with its exact bytes is not named, as before #753. - N1. Where the pin taken here still lists the old name, the `why` adds "The pin taken here still lists Makefile, so this doctor reports it missing once the merge is finished, unless it is renamed back". Tests: the own file renamed, and the copy renamed beside its twin, name nothing (both were named before; dropping the uniqueness guard now fails them); each rename case finishes the merge with build.mk kept and reads `DRIFTED (1 copy-missing)`. The rename setup is shared (`merged_after`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-17T04:28:54Z) RV-b's re-verification of #781 round 2 ( S1 is fixed.
N1 is true in every state:
SHOULD (take before landing): two of the rule's guards are untested, and breaking either makes the WHY say something false.
N1 (NIT): "the base held nothing at the new name" (
Mutants:
Tests:
Merges and CI:
Lane: openreposhape-3 (openRepoShape-3) |
…tes no copy holds, renamed, is named no copy's rename, and a rename the kept pin has no row for says nothing is missing, so both guards of #753's rename rule are held (review of #781, round 3) RV-b's re-verification of #781 at 06e1de8 found two guards of `copies_renamed` and `renamed_copies` that no test held: - "the one path holding the blob is a copy": dropped, a project's own docs/notes.md, renamed to docs/n.md on one side and edited on the other, was named "renamed from the copy docs/notes.md". Now `test_a_rename_of_no_copy_names_nothing[the-projects-own-unique-file- renamed]` holds it, beside round 2's two twin cases (the table and the test renamed to cover all three). - "the pin taken here lists the old name": dropped, a merge that ends COMPLIANT was told the Makefile would be reported missing. Now `test_a_rename_the_kept_pin_has_no_row_for_says_nothing_is_missing` restages the kept pin without the Makefile's row, finds no "still lists" sentence, and finishes the merge to a verdict that is COMPLIANT and not copy-missing. `merged_after` takes an own file's bytes as well as a copy's, and `finished_keeping` finishes a rename's merge for both tests. In shape-doctor.py, `copies_renamed`'s docstring calls "the base held nothing at the new name" belt and braces since round 2: alone it acts only on a rename onto a path the base held (rename/add). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-17T04:38:15Z) RV-b's re-verification of #781 round 3 ( Both surviving mutants are now killed:
Each fails only its new test, on the assertion meant to catch it. The tests check what the guards protect.
Merge with main NIT (carried over): the "base held nothing" condition is documented as belt and braces, and is untested on its own. That matches round 2's measurement. Lane: openreposhape-3 (openRepoShape-3) |
…ew name only where that side's own commit holds the proving bytes at that one path, so a project's own twin kept in step with the copy and renamed too is never named the copy's new name (review of #789, round 3) RV-b's S4: the base holds the Makefile and the project's own twin of it, `tools/Makefile.orig`. Ours keeps the twin in step with a.mk, and each side renames the twin too. Ours' commit then holds the bytes its pin row records for the Makefile at a.mk and at tools/x.mk, which git paired with the twin, and the `why` said "renamed to a.mk and tools/x.mk on the side taken here": a person following it could delete their own file. - `side_moved_it` proves a name only where the side's own commit holds its bytes at that path alone (`tree_holders`, `ls-tree -r -z` of HEAD or MERGE_HEAD): #781's one-path rule, applied to the side's tree. It closes the same gap for the base's blob written at two paths. - `base_holders` reads the base through `tree_holders`. Tests: two `TWICE` cases, the twin kept in step with the standard moving (pin-row proof) and without (base-blob proof). Each names only b.mk and leaves `[Makefile, b.mk, project.yaml]`; both fail without the one-path check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 2 days and 17 hours by commenting |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#781's final head a61f4e1 and its squash ed052f2 hold the same tree. The squash conflicted only in shape-doctor.py and tests/test_doctor_pin_restore_merges.py, the two files #781 and this branch both change; both are taken ours, which changes neither file from the merge of a61f4e1 above. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…at its old name, is named gone on both sides with each new name at which a side's own commit holds, at that path alone, either the base's blob of the copy or the bytes that commit's own pin row records for the copy, never as a merge of hunks git did not merge (#783) (#789) Closes #783. Ours renamed the copy `Makefile` to `a.mk` and theirs renamed it to `b.mk`. The index holds `DD Makefile`, `AU a.mk` and `UA b.mk`, and the `why` called the Makefile "a copy conflicted too ... a person merges it by hand, keeping the hunks git merged", though git merged no hunks, and it named neither new name. Now `pin_restore` in `shape-doctor.py` never takes a copy at stage 1 alone (`DD`) and never gives it the hunks sentence. `copies_renamed_twice` names a new name only on exact evidence: a path one side alone added, that no pin lists, is that side's name for the copy where the side's own commit holds, at that path and at no other, either the merge base's blob of the copy, which no other path of the base holds, or the bytes whose sha256 that commit's own pin row records for the copy, which no path of the base holds. The second proof is for a standard that moved the copy between the two pins: each side's `apply` rewrote it, and neither commit holds the base's blob. The `why` says where a side's name is not proved, and neither name is ever taken. With no merge base (a cherry-pick, a rebase, `merge --squash`) nothing is proved, and a project's own twin kept in step with a copy proves nothing. `tests/test_doctor_pin_restore_merges.py` builds a real merge for each case. Its `no_merge_head` helper deletes MERGE_HEAD's file, because git 2.46 and later refuse `git update-ref -d` on a pseudoref. Lane 3's independent reviewer RV-b (opus) found it READY in its third round (#789 issuecomment-6232729550), and RV789d (opus) found the fix round after it READY (#789 issuecomment-6240455257). After #781 landed, it was restacked onto #781's squash by three merges, and its changed lines equal the reviewed delta (#789 issuecomment-6244751174). It lands on Brett Heap's word "merge lane 3's v1.1.1 drafts after the v1.1.0 tag" (2026-10-17T13:00:57Z). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-3 (openRepoShape-3)
Claim: #753 (comment)
Closes #753.
Stacked on #779; lands after it. Both change the same lines of
pin_restore. This branch is #779's commit06eb23efplus three commits,23f3879c, round 2's06e1de88and round 3's0a9afe3a734b553bcf975063b68f02fc04955e09(the head), so the diff against main shows #779's change as well. The file:line references below are to23f3879c; round 2's are in its section, which supersedes the uniqueness rule below.Lands after the v1.1.0 tag. The issue's "Lands after: #736" is met: #736 landed as
bf1f099b.What happened
Ours renamed the
Makefiletobuild.mkand theirs edited theMakefile. git carries the conflict tobuild.mk, a name no pin's row lists. So the conflicted shape pin'swhyandleft_for_a_personsaid nothing of it, and only themergerow listed the path.Reproduced on #736's head
3dfe2fde, with the rename on either side:UU build.mkat stages 1, 2 and 3, whose stage 1 isb9fac472, the merge base'sMakefileblob.left_for_a_personwas['project.yaml']. The claim holds on mainbf1f099b: the three new cases fail on #779's doctor with['project.yaml'] == ['build.mk', 'project.yaml'].What changed (
shape-doctor.py)pin_restorekeeps each conflicted path's stage-1 object (:3963-3971, from thels-files -uit already reads). It hands those of paths no pin lists tocopies_renamed(:3989-3991). Their names joinleft_for_a_person(:4011-4014) and thewhy(:4046).renamedstarts empty for any file but the shape pin (:3977).copies_renamed(:4155-4191) matches by the merge base's own blob, at a name the base did not hold. A path no pin lists is a copy's rename where two things hold:merge-base HEAD MERGE_HEAD) holds nothing at that path;More on the rule:
contracts/repository-naming.yamlbeside a collision copy undershape/is such a path. My first cut missed this condition and called that file a rename.tests/test_shape_doctor.py::test_a_conflicted_shape_pin_takes_only_the_copies_its_side_has_rows_forcaught it, and that test now holds the rule.git --literal-pathspecs ls-tree -z <base> -- <copies and candidates>.MERGE_HEAD(a rebase, a cherry-pick), or no base git can name, nothing is called a rename, as before.renamed_copies(:4194-4215) writes the sentence: "build.mk, renamed from the copy Makefile, is conflicted too, and no pin has a row for it under its new name, so neither side is taken whole: a person merges it by hand, keeping the hunks git merged".by_conflict(:4138-4152) is the content/deletion splitother_sides_copiesmade inline, now shared by both.Docstrings: a paragraph in
pin_restore'sunmergedentry (:3847-3854). The "never named" rule (:3862-3868) now covers a file under the name the merge base held it at.Tests (
tests/test_doctor_pin_restore_merges.py)test_a_copy_renamed_on_one_side_is_named_for_a_person[...](:1052) has three cases:--ourstakes, the other side editing it;In each case:
checkout --ours -- contracts/shape-pin.yaml scripts/bootstrap.py(neverbuild.mk);['build.mk', 'project.yaml']to a person;whysays the sentence above;build.mkat the same stages with the same bytes.Each failed on #779's doctor (stash of
shape-doctor.pyonly).Each run used a stub
ghfirst on PATH that exits 97,GH_TOKEN=bogusand an emptyGH_CONFIG_DIR, at the head:Pinned shape copies
None.
shape-doctor.pyis in no copy list.Docs
None changed. No doc describes the conflicted-pin restore.
For the reviewer
3dfe2fde: the person keepsbuild.mkunder its new name and finishes the merge. The doctor then saysDRIFTED (1 copy-missing): thepinsrow'sshape-copy-missing: … Makefile is pinned but absent. Thewhyhere does not predict that. It holds only where the pin kept has a row for the old name, so the sentence stays at naming the copy's rename and giving it to a person. Say if thewhyshould add it.Round 2 (RV-b's review at
23f3879c: one SHOULD, N1 taken, N2 left)New head:
06e1de88a5378c5466bcc9e8472852579cabbbcf, a fast-forward of23f3879c.copies_renamedlisted only the copies' paths in the base. RV-b's state: the base holdstools/bootstrap.pybyte for byte as the copyscripts/bootstrap.py; ours renames it totools/boot.pyand edits it, and theirs edits it. Thewhythen said "tools/boot.py, renamed from the copy scripts/bootstrap.py" andleft_for_a_personlisted it.copies_renamed,shape-doctor.py:4157-4200): it now reads the base's whole tree with onegit ls-tree -r -z <base>. A path is a copy's rename only where exactly one path of the whole base holds its stage-1 blob, that path is a copy, and the base held nothing at the new name.diff -Mfrom the base to each side): that record is a similarity guess of its own, made apart from the merge's, and the doctor guesses nothing here. Blob identity is exact.pin_restore: a shape copy renamed on one side of a merge conflicts under its new name, which no pin's row lists, so thewhynever names it and only themergerow does #753; themergerow still lists the path. A test pins that case too.whyadds "The pin taken here still lists Makefile, so this doctor reports it missing once the merge is finished, unless it is renamed back" (renamed_copies,:4203-4239;keptpassed at:3979and:4048). It reads true in every case the tests build: each rename test now finishes the merge withbuild.mkkept and readsDRIFTED (1 copy-missing). Where the pin taken here has no row for the old name, nothing is said, since nothing is missing.tests/test_doctor_pin_restore_merges.py):merged_after(:1062) with the actsrenamed,deletedandedited(:1033-1059).test_a_copy_renamed_on_one_side_is_named_for_a_person(:1117) also asserts the N1 sentence and, after the merge,DRIFTED (1 copy-missing).test_a_blob_two_paths_of_the_base_hold_names_no_rename[the-projects-own-file-renamed|the-copy-renamed-beside-its-twin](:1171). Both cases were named on23f3879cand are not named now. Dropping the uniqueness guard now fails both, so RV-b's surviving mutant is killed.06e1de88, with a stubgh(exits 97),GH_TOKEN=bogusand an emptyGH_CONFIG_DIR:Round 3 (RV-b's re-verification at
06e1de88: one SHOULD, tests only)New head:
0a9afe3a734b553bcf975063b68f02fc04955e09, a fast-forward of06e1de88. No behaviour changes; one docstring sentence changes.shape-doctor.py:4205,held[blob][0] in copies):test_a_rename_of_no_copy_names_nothing[the-projects-own-unique-file-renamed](tests/test_doctor_pin_restore_merges.py:1212). A project's owndocs/notes.md, bytes no other path holds, is renamed todocs/n.mdon ours and edited on theirs.docs/n.mdis not inleft_for_a_personor thewhy.NOT_A_COPYS_RENAME(:1195), three cases.:4236):test_a_rename_the_kept_pin_has_no_row_for_says_nothing_is_missing(:1165). The Makefile is renamed on ours, and the kept pin is restaged without the Makefile's row.build.mkis still named as renamed from the copy, and no "still lists" sentence is said. Finished withbuild.mkkept, the verdict startsCOMPLIANTand is notcopy-missing.merged_after(:1062) takes an own file's bytes as well as a copy's.finished_keeping(:1153) finishes a rename's merge for both this test andtest_a_copy_renamed_on_one_side_is_named_for_a_person(:1119). That test still checksDRIFTED (1 copy-missing).and held[blob][0] in copies:1 failed, 6 passed(the new unique-file case:'docs/n.md' not in ['docs/n.md', 'project.yaml']). Killed.listed = sorted(set(renamed.values()))):1 failed, 6 passed(the new kept-pin test:'still lists' not in ...). Killed.copies_renamed's docstring (:4170-4176) calls "the base held nothing at the new name" belt and braces since round 2. Alone it acts only on a rename onto a path the base held (rename/add), which stays unnamed.0a9afe3a, with a stubgh(exits 97),GH_TOKEN=bogusand an emptyGH_CONFIG_DIR:🤖 Generated with Claude Code
Summary by Sourcery
Handle conflicted shape-copy renames accurately during pin restoration and guide manual merge resolution.
Bug Fixes:
Enhancements:
Tests: