Skip to content

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

Merged
brettheap merged 9 commits into
mainfrom
l3-753-renamed-copy
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

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 commit 06eb23ef plus three commits, 23f3879c, round 2's 06e1de88 and round 3's 0a9afe3a734b553bcf975063b68f02fc04955e09 (the head), so the diff against main shows #779's change as well. The file:line references below are to 23f3879c; 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 Makefile to build.mk and theirs edited the Makefile. git carries the conflict to build.mk, a name no pin's row lists. So the conflicted shape pin's why and left_for_a_person said nothing of it, and only the merge row listed the path.

Reproduced on #736's head 3dfe2fde, with the rename on either side: UU build.mk at stages 1, 2 and 3, whose stage 1 is b9fac472, the merge base's Makefile blob. left_for_a_person was ['project.yaml']. The claim holds on main bf1f099b: the three new cases fail on #779's doctor with ['project.yaml'] == ['build.mk', 'project.yaml'].

What changed (shape-doctor.py)

  • pin_restore keeps each conflicted path's stage-1 object (:3963-3971, from the ls-files -u it already reads). It hands those of paths no pin lists to copies_renamed (:3989-3991). Their names join left_for_a_person (:4011-4014) and the why (:4046). renamed starts 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:

    • the merge base (merge-base HEAD MERGE_HEAD) holds nothing at that path;
    • its stage 1 is, object for object, the blob the base holds at a copy's path (a copy either side's pin has a row for), and exactly one copy matches (round 2: exactly one path of the whole base, and it a copy).

    More on the rule:

    • Why a missing base path matters: git gives a path the base did not hold a stage 1 only where it paired it with a path the base did hold, which is a rename.
    • A path the base held is no rename, though its bytes be a copy's. The source's own contracts/repository-naming.yaml beside a collision copy under shape/ 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_for caught it, and that test now holds the rule.
    • One read gives both facts: git --literal-pathspecs ls-tree -z <base> -- <copies and candidates>.
    • No similarity index is guessed at. Under a rename, git gives the new name the base's bytes of the old name as stage 1, whatever either side then did.
    • A blob two copies share names neither. With no 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".

    • Where one side alone holds the new name (the other side deleted the copy), it says instead "…, and one side alone holds it, so neither side is taken whole: a person keeps or deletes it by hand".
    • The path is never taken: no pin has a row to prove it by.
  • by_conflict (:4138-4152) is the content/deletion split other_sides_copies made inline, now shared by both.

  • Docstrings: a paragraph in pin_restore's unmerged entry (: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:

  • renamed on the side --ours takes, the other side editing it;
  • renamed on the other side;
  • renamed on one side and deleted on the other (stages 1 and 2 only).

In each case:

  • the NEXT is checkout --ours -- contracts/shape-pin.yaml scripts/bootstrap.py (never build.mk);
  • both rows give ['build.mk', 'project.yaml'] to a person;
  • the why says the sentence above;
  • run as printed, the NEXT leaves build.mk at the same stages with the same bytes.

Each failed on #779's doctor (stash of shape-doctor.py only).

Each run used a stub gh first on PATH that exits 97, GH_TOKEN=bogus and an empty GH_CONFIG_DIR, at the head:

/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt -o tmp_path_retention_policy=failed tests/test_doctor_pin_restore_merges.py tests/test_doctor_next_is_a_command.py tests/test_repo_hygiene.py tests/test_cli_reference.py
184 passed in 108.39s (0:01:48)

/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-sd -o tmp_path_retention_policy=failed tests/test_shape_doctor.py
436 passed, 3 skipped, 1 xfailed in 307.62s (0:05:07)

Pinned shape copies

None. shape-doctor.py is in no copy list.

Docs

None changed. No doc describes the conflicted-pin restore.

For the reviewer

  • What a person meets after the merge. I probed it on 3dfe2fde: the person keeps build.mk under its new name and finishes the merge. The doctor then says DRIFTED (1 copy-missing): the pins row's shape-copy-missing: … Makefile is pinned but absent. The why here 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 the why should add it.
  • Rename/rename is not measured. Both sides renaming the copy to different names is not tested here, and I have not checked what stages git leaves at each name.

Round 2 (RV-b's review at 23f3879c: one SHOULD, N1 taken, N2 left)

New head: 06e1de88a5378c5466bcc9e8472852579cabbbcf, a fast-forward of 23f3879c.

  • S1: a file of the project's own with a copy's bytes was named a rename of that copy.
    • Before: copies_renamed listed only the copies' paths in the base. RV-b's state: the base holds tools/bootstrap.py byte for byte as the copy scripts/bootstrap.py; ours renames it to tools/boot.py and edits it, and theirs edits it. The why then said "tools/boot.py, renamed from the copy scripts/bootstrap.py" and left_for_a_person listed it.
    • Fix (copies_renamed, shape-doctor.py:4157-4200): it now reads the base's whole tree with one git 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.
    • Why this fix and not git's rename record (diff -M from 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.
    • What it costs: a copy renamed beside a file holding its exact bytes is not named, as before shape-doctor.py pin_restore: a shape copy renamed on one side of a merge conflicts under its new name, which no pin's row lists, so the why never names it and only the merge row does #753; the merge row still lists the path. A test pins that case too.
  • 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" (renamed_copies, :4203-4239; kept passed at :3979 and :4048). It reads true in every case the tests build: each rename test now finishes the merge with build.mk kept and reads DRIFTED (1 copy-missing). Where the pin taken here has no row for the old name, nothing is said, since nothing is missing.
  • N2 (a rename on both sides) is left for lane 3's follow-up.
  • Tests (tests/test_doctor_pin_restore_merges.py):
    • The rename setup is shared now: merged_after (:1062) with the acts renamed, deleted and edited (: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).
    • New: 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 on 23f3879c and are not named now. Dropping the uniqueness guard now fails both, so RV-b's surviving mutant is killed.
  • Run on 06e1de88, with a stub gh (exits 97), GH_TOKEN=bogus and an empty GH_CONFIG_DIR:
/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt -o tmp_path_retention_policy=failed tests/test_doctor_pin_restore_merges.py tests/test_doctor_next_is_a_command.py tests/test_repo_hygiene.py tests/test_cli_reference.py
186 passed in 116.92s (0:01:56)

/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-sd3 -o tmp_path_retention_policy=failed tests/test_shape_doctor.py -k conflicted
14 passed, 426 deselected in 35.36s

Round 3 (RV-b's re-verification at 06e1de88: one SHOULD, tests only)

New head: 0a9afe3a734b553bcf975063b68f02fc04955e09, a fast-forward of 06e1de88. No behaviour changes; one docstring sentence changes.

  • The "is a copy" guard (shape-doctor.py:4205, held[blob][0] in copies):
    • New case: 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 own docs/notes.md, bytes no other path holds, is renamed to docs/n.md on ours and edited on theirs.
    • Expected: nothing is called a rename, and docs/n.md is not in left_for_a_person or the why.
    • Table: round 2's twin table and test are folded into it as NOT_A_COPYS_RENAME (:1195), three cases.
  • The "kept pin lists the old name" guard (:4236):
    • New test: 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.
    • Expected: build.mk is still named as renamed from the copy, and no "still lists" sentence is said. Finished with build.mk kept, the verdict starts COMPLIANT and is not copy-missing.
  • Shared helpers:
    • 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 and test_a_copy_renamed_on_one_side_is_named_for_a_person (:1119). That test still checks DRIFTED (1 copy-missing).
  • Mutants, each applied to a backed-up working copy, run, then restored byte for byte (sha256 checked):
    • Dropping 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.
    • Always adding the sentence (listed = sorted(set(renamed.values()))): 1 failed, 6 passed (the new kept-pin test: 'still lists' not in ...). Killed.
  • N1 (optional), taken: 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.
  • Run on 0a9afe3a, with a stub gh (exits 97), GH_TOKEN=bogus and an empty GH_CONFIG_DIR:
/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt -o tmp_path_retention_policy=failed tests/test_doctor_pin_restore_merges.py tests/test_repo_hygiene.py tests/test_cli_reference.py
131 passed in 75.79s (0:01:15)

/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-sd4 -o tmp_path_retention_policy=failed tests/test_shape_doctor.py -k conflicted
14 passed, 426 deselected in 34.87s

🤖 Generated with Claude Code

Summary by Sourcery

Handle conflicted shape-copy renames accurately during pin restoration and guide manual merge resolution.

Bug Fixes:

  • Identify conflicted files renamed from pinned shape copies, include them in the explanation and hand them to a person for manual resolution instead of silently omitting them.
  • Prevent project-owned files that merely share copy contents from being misidentified as copy renames.
  • Report when a retained pin still lists the original copy name and will become copy-missing after the merge.

Enhancements:

  • Refactor merge-conflict grouping into shared logic for content and deletion conflicts.

Tests:

  • Add coverage for copy renames with edits or deletions on the opposite side, shared-content false positives, unique project files, and pins without the original copy row.

Brett Heap and others added 2 commits October 17, 2026 01:27
…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>
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

The 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 rename

sequenceDiagram
    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
Loading

File-Level Changes

Change Details Files
Detect conflicted copy renames by correlating stage-1 merge-base blobs with copy paths, then describe the renamed path as a person-resolvable conflict without selecting it.
  • Capture stage-1 objects from unmerged index entries.
  • Use the merge base and literal ls-tree results to distinguish true renames from paths already present in the base and require exactly one matching copy.
  • Add renamed_copies messaging and include renamed paths in left_for_a_person.
  • Cover content and delete/modify conflicts through shared by_conflict classification.
shape-doctor.py
Make merge pin restoration choose only a side whose pin is readable and avoid issuing a repeat remediation when neither side is readable.
  • Centralize side selection and explanatory text in side_to_take.
  • Propagate no-command merge explanations into unreadable-pin rows.
  • Update restoration documentation and helper behavior for the revised ownership rules.
shape-doctor.py
Add regression coverage for renamed copies and unreadable merge-pin scenarios.
  • Test renames on either side and rename-versus-delete conflicts, including unchanged conflict stages after the generated NEXT.
  • Test readable-side selection and no-NEXT behavior when neither pin reads.
  • Update existing expectations for shared conflict grouping and person-owned resolution.
tests/test_doctor_pin_restore_merges.py

Assessment against linked issues

Issue Objective Addressed Explanation
#753 Identify conflicted paths whose stage-1 blob came from a rename of a copy listed by either side’s pin, name each path as renamed from that copy in the conflicted pin’s why, and include it in left_for_a_person without taking it whole. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T04:11:47Z) Independent review of #781 by RV-b (opus), on head 23f3879c: READY, with one SHOULD to take before it lands. Two NITs. It lands only after the v1.1.0 tag, after #779.

Measured (main → #781):

state main #781
ours renames Makefile → build.mk, theirs edits it build.mk not named "build.mk, renamed from the copy Makefile, is conflicted too … a person merges it by hand"; left_for_a_person [build.mk, project.yaml]; the NEXT leaves build.mk alone
renamed and also edited on the renaming side not named named (stage 1 is still the base's blob)
cherry-pick or rebase (no MERGE_HEAD) not named not named, as documented
rename detection off (-s recursive -X no-renames) DU Makefile, build.mk clean identical to main
both sides rename not named not named (N2)

On git 2.43, ort still detects the rename under -c merge.renames=false and -X no-renames. Only -s recursive -X no-renames turns detection off.

S1 (take before landing): a project's own file whose bytes equal a copy's is named as a renamed copy.

  • Where: shape-doctor.py:4155-4191, copies_renamed. It lists only the copies' paths in the base, so its "exactly one copy matches" check never sees a non-copy file with the same blob.
  • Measured:
  • Why it matters: this breaks "a file of the project's own is never named" (:3862-3868), and the docstring's "a blob two copies share names neither" (:4170) does not cover a copy and an own file sharing one.
  • Not covered by any test: dropping the uniqueness guard (held.get(blob) for len(...) == 1) still passes the file's 28 tests and the test_shape_doctor.py case.
  • Fix, either:
    • require the blob to be unique across the whole base tree (ls-tree -r -z <base>, every path that holds it);
    • or read git's own rename record (git diff -z --name-status -M <base> HEAD and <base> MERGE_HEAD), and accept a pairing only where the old path is a copy.
  • Tests to add: this state (not named), and two base paths sharing the blob (neither named).

NITs:

Mutants:

mutant result
the renamed path left out of left_for_a_person 3 failed
the renamed path taken in the NEXT 3 failed
drop "the base held nothing at that path" killed only by test_shape_doctor.py::test_a_conflicted_shape_pin_takes_only_the_copies_its_side_has_rows_for
drop the blob-uniqueness guard survives (S1)

Tests:

  • four targeted files: 184 passed;
  • both PRs' tests over main's doctor: 7 failed, 21 passed (exactly the new cases).

Merges and CI: clean with bf1f099b and every open lane-3 v1.1.1 PR. CI is green.

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>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T04:28:54Z) RV-b's re-verification of #781 round 2 (06e1de88): READY. S1 and N1 are fixed, and the head says only true things in every state built. One SHOULD remains, tests only, to take before landing.

S1 is fixed.

  • RV-b's own state: tools/bootstrap.py, byte for byte scripts/bootstrap.py, renamed to tools/boot.py. It is now not named, as on main.
  • The twin state (the copy renamed beside an own file with the same bytes): not named either, as documented. The merge row still lists it.
  • Is uniqueness across the whole base too strict? No. A fresh root's 11 pinned copies have distinct bytes. Only a project file kept byte for byte equal to a copy falls back to the old silence.

N1 is true in every state:

state the sentence after finishing the merge, keeping the new name
renamed on ours said DRIFTED (1 copy-missing)
renamed on theirs said DRIFTED (1 copy-missing)
renamed on ours, deleted on theirs said DRIFTED (1 copy-missing)
renamed, kept pin without the Makefile row not said COMPLIANT, SHAPE BEHIND (… 1 upstream-added)

SHOULD (take before landing): two of the rule's guards are untested, and breaking either makes the WHY say something false.

  • The "is a copy" guard (:4200):
    • Dropping it passes every test.
    • It then names a project's own docs/notes.md (unique bytes), renamed to docs/n.md on ours and edited on theirs, as "renamed from the copy docs/notes.md". This is the commonest rename beside the pin.
    • The head is correct here; only the test is missing.
  • The "kept pin lists the old name" guard (:4231):
    • Always adding the sentence passes every test.
    • It then tells a merge that finishes COMPLIANT that the Makefile will be reported missing.
  • Fix: add two cases:
    • an own file with unique bytes, renamed on one side and edited on the other, is not named;
    • a rename where the kept pin has no row for the old name gets no "still lists" sentence.

N1 (NIT): "the base held nothing at the new name" (:4199) is now redundant.

  • With the whole base read, a path the base held carries its own blob as stage 1, which is excluded anyway. The mutant that drops it now survives (round 1's test_shape_doctor.py case killed it).
  • It still acts only on a rename onto a path the base held (rename/add).
  • Optionally call it belt and braces in the docstring.

Mutants:

mutant result
drop the "exactly one path" count killed (both TWINS cases)
drop "base held nothing" survives (N1, equivalent)
drop "is a copy" survives (SHOULD)
always add the N1 sentence survives (SHOULD)

Tests:

Merges and CI:

  • clean with bf1f099b and each open lane-3 PR;
  • Sonar, package and tests-git-bash pass;
  • Linux tests was pending at 04:28Z.

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>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T04:38:15Z) RV-b's re-verification of #781 round 3 (0a9afe3a): READY. No MUST or SHOULD. Round 3 touches only the test file and one docstring.

Both surviving mutants are now killed:

mutant result
unmutated head 32 passed
drop and held[blob][0] in copies 1 failed: test_a_rename_of_no_copy_names_nothing[the-projects-own-unique-file-renamed] ('docs/n.md' not in [...])
always add the "still lists" sentence 1 failed: test_a_rename_the_kept_pin_has_no_row_for_says_nothing_is_missing

Each fails only its new test, on the assertion meant to catch it.

The tests check what the guards protect.

  • The unique-file case: the base holds nothing at docs/n.md, and docs/n.md's stage 1 is main:docs/notes.md, held by one path only. So only the in copies guard can refuse it.
  • The kept-pin case still exercises the rename, since build.mk is named. It asserts no "still lists" sentence, and, once the merge finishes keeping build.mk, a COMPLIANT verdict with no copy-missing.
  • The opposite case keeps its coverage: the full sentence and DRIFTED (1 copy-missing).

Merge with main bf1f099b: clean (52a5a3cc).

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)

brettheap pushed a commit that referenced this pull request Oct 17, 2026
…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>
brettheap and others added 5 commits October 17, 2026 18:37
Restack of #781 after its parent #779 (#752) squash-landed on main as c100843.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Restack of #781 after its parent #779 (#752) squash-landed on main. Conflicts in shape-doctor.py and tests/test_doctor_pin_restore_merges.py taken ours; S's tree equals F's tree, so the merge changes no file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 02:14
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

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 @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit ed052f2 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the l3-753-renamed-copy branch October 18, 2026 02:15
brettheap pushed a commit that referenced this pull request Oct 18, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
#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>
brettheap added a commit that referenced this pull request Oct 18, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant