Skip to content

shape-doctor.py: a conflicted shape pin's NEXT takes the side whose pin 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) - #779

Merged
brettheap merged 2 commits into
mainfrom
l3-752-unreadable-side
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-3 (openRepoShape-3)
Claim: #752 (comment)

Closes #752.

Lands after the v1.1.0 tag. (The issue's "Lands after: #736" is met: #736 landed as bf1f099b, this branch's base.)

What happened

A conflicted shape pin's NEXT took --ours wherever ours held a pin, whether it 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 on every run.

Reproduced on #736's head 3dfe2fde and on main bf1f099b's tree (f596d9cd, the predicted squash, equal to the landed one). In both_pure's merge:

  • Stage 2 restaged as files: [ (RV-b's state): the NEXT was checkout --ours -- contracts/shape-pin.yaml. Run as printed, the doctor printed the same NEXT again.
  • Both stages restaged that way: the same --ours NEXT, again after it ran.
  • Ours removed and theirs not reading: the NEXT was checkout --theirs, a pin that does not read.

The issue's claim holds on current main.

What changed (shape-doctor.py)

  • side_to_take (:4125-4174), called by pin_restore (:3957-3959), picks the side. It takes ours where ours' pin reads, else theirs where that reads.
    • Where both sides read, the why starts as before: "--theirs takes the other side. Take one side of the pin whole".
    • Where only one side holds a pin, it says, as before: "only that side of the merge holds a pin. Take it whole".
    • Where both sides hold one and only one reads, it says "only that side's pin reads as one, and the other side taken whole would leave a pin that does not read. Take it whole". It never offers the other side, which would offer the circle.
  • No line where no side's pin reads. pin_restore returns ("unmerged", None, why). The why says that no side taken whole puts back a pin that reads, and that this doctor would name the same line again. It ends: "A pin is never mended by hand, so no line is named here: whether to finish the merge, or to abort it and first put back the pin a side broke, is a person's call". When only one side holds a pin, it says "the one side of it that holds a pin holds one that does not read".
  • The callers keep that why and name no NEXT:
    • unreadable_pin_row (:4638) keeps its update-shape.py check line for the none restore alone.
    • unreadable_next (:4561-4590) keeps the why of a pin left stuck that way, instead of sending the reader to this doctor again with UNREADABLE_BY_HAND. That text would say no tool-written file has a pin that reads in its history. The manifest kinds row therefore prints what the shape currency row prints. Its return type now admits a None command.
  • other_sides_copies (:4085-4121) loses shape-doctor.py: a conflicted shape pin's NEXT reads both sides' pins, so a copy conflicted beside it that only the side not taken has a files: row for is named in its why and left_for_a_person for a person to merge, and never taken (#687) #736's kept-pin-does-not-read wording. The pin taken here now always reads. Its words are otherwise unchanged; I checked the old kept_reads=True concatenation against the new one.
  • Docstrings: pin_restore's unmerged entry (:3773-3782), EVERY PIN … IS READ FIRST (:3884-3886), and the two callers'.

Tests (tests/test_doctor_pin_restore_merges.py)

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
181 passed in 108.85s (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 346.17s (0:05:46)

Pinned shape copies

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

Docs

None changed. No doc describes the conflicted-pin restore. README.md, AGENTS.md and docs/cli.md are untouched, and so is docs/release-notes/.

For the reviewer

🤖 Generated with Claude Code

Summary by Sourcery

Fix conflicted shape-pin remediation so generated NEXT commands leave a readable pin and unresolved cases are deferred to the user.

Bug Fixes:

  • Ensure conflicted shape pins are restored from a side whose pin is readable, preventing the doctor from repeatedly recommending the same command.
  • Avoid suggesting a remediation command when neither side of a conflicted pin contains a readable pin.

Enhancements:

  • Propagate merge-resolution explanations consistently across shape-currency and manifest-kinds results.
  • Simplify conflicted-copy messaging now that the selected pin is always readable.

Tests:

  • Add coverage for selecting the readable side of a conflicted pin and for leaving the NEXT unset when no side is readable.

…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>
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates conflicted shape-pin restoration so the doctor only checks out a side whose pin reads, and names no NEXT when neither side is readable; output propagation, copy messaging, documentation, and regression tests are adjusted accordingly.

Sequence diagram for readable conflicted-pin restoration

sequenceDiagram
    participant Doctor
    participant GitIndex
    participant PinReader
    Doctor->>GitIndex: ls-files -u
    GitIndex-->>Doctor: Pin stages
    Doctor->>PinReader: pin_reads_at(:2, pin)
    PinReader-->>Doctor: Readable or unreadable
    Doctor->>PinReader: pin_reads_at(:3, pin)
    PinReader-->>Doctor: Readable or unreadable
    alt Ours reads or both read
        Doctor-->>Doctor: Return --ours and stage 2
    else Only theirs reads
        Doctor-->>Doctor: Return --theirs and stage 3
    end
    Doctor-->>Doctor: Include explanatory why
Loading

Sequence diagram for unreadable conflicted-pin reporting

sequenceDiagram
    participant Doctor
    participant PinRestore
    participant ShapeCurrency
    participant ManifestKinds
    Doctor->>PinRestore: pin_restore(detail)
    PinRestore-->>Doctor: unmerged, no command, why
    Doctor->>ShapeCurrency: Keep no NEXT and explain person's call
    Doctor->>ManifestKinds: Propagate the same why
    ManifestKinds-->>Doctor: No fallback shape-doctor command
Loading

Flow diagram for conflicted shape-pin restoration

flowchart TD
    A[Conflicted shape pin] --> B{Which side's pin reads?}
    B -->|Ours reads| C[Choose --ours]
    B -->|Only theirs reads| D[Choose --theirs]
    B -->|Both read| E[Choose a side and explain alternative]
    B -->|Neither reads| F[No NEXT command]
    C --> G[Return unmerged restore with checkout command]
    D --> G
    E --> G
    F --> H[Return unmerged restore with person's-call explanation]
Loading

File-Level Changes

Change Details Files
Selects a merge side only when its conflicted pin is readable, and suppresses the NEXT command when neither side provides a readable pin.
  • Adds side selection based on pin_reads_at, preferring ours when readable and otherwise using readable theirs.
  • Preserves the existing both-readable behavior and adds explanations for the single-readable case.
  • Returns unmerged with a null command and a human-resolution explanation when no side's pin reads.
shape-doctor.py
Propagates no-command merge outcomes consistently through both doctor output rows.
  • Allows unreadable_next to return a nullable command and reuses the stuck pin's explanation instead of recursively directing users back to the doctor.
  • Keeps the update-shape check command only for the separate none restore case.
  • Updates docstrings and remediation text to document readable-pin requirements and human resolution.
shape-doctor.py
Simplifies conflicted-copy messaging now that any selected pin is guaranteed readable.
  • Removes the obsolete kept-pin readability parameter and unreadable-pin wording.
  • Retains copy ownership and manual merge guidance for files conflicted beside the pin.
shape-doctor.py
Adds regression coverage for readable-side selection and unresolved unreadable-pin conflicts.
  • Verifies checkout chooses the opposite side when ours or theirs is unreadable and that the resulting pin reads after execution.
  • Verifies both output rows use next: null, share the same explanation, and report unmerged when no side reads.
  • Updates existing copy-conflict expectations to remove the obsolete unreadable case.
tests/test_doctor_pin_restore_merges.py

Assessment against linked issues

Issue Objective Addressed Explanation
#752 When a conflicted shape pin has at least one side whose pin reads, make the printed NEXT take a readable side rather than a side whose pin does not read. ✅
#752 When no side's conflicted pin reads, name no NEXT and explain in the why that neither side can restore a readable pin and the decision is for a person. ✅
#752 Add tests for both cases that run the printed NEXT as printed where applicable and verify the doctor's subsequent output does not repeat the same NEXT. ✅

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:08Z) Independent review of #779 by RV-b (opus), on head 06eb23ef: READY. No MUST, SHOULD or NIT. It lands only after the v1.1.0 tag.

The issue's loop holds on main bf1f099b:

  • with ours' pin as files: [, the NEXT is checkout --ours, and after it runs the doctor names the same NEXT again;
  • likewise with both sides unreadable, with ours a YAML list, files: 5, or a mapping that is not a pin, and with ours deleted while theirs does not read.

On the head every state ends somewhere new. "After finish" means every conflicted path taken from the NEXT's side, then the merge committed.

state NEXT after NEXT after finish
ours unparsable checkout --theirs ("only that side's pin reads as one…") the pin reads; NEXT update-shape.py check COMPLIANT, SHAPE BEHIND
theirs unparsable checkout --ours the pin reads COMPLIANT
ours a list / files: 5 / not a pin --theirs the pin reads COMPLIANT, SHAPE BEHIND
both unparsable none in either row, the same WHY
ours deleted, theirs reads --theirs (unchanged from main) no repeat COMPLIANT, SHAPE BEHIND
ours deleted, theirs unparsable none

What RV-b checked in the code:

Mutants:

mutant result
take a side whether it reads or not 4 failed
the manifest kinds row ignores the stuck WHY 2 failed
shape currency names check again 2 failed

Tests: the four targeted files gave 181 passed. The four new cases fail on main's doctor.

Merges and CI:

  • clean with bf1f099b and with every open lane-3 v1.1.1 PR, pairwise;
  • on all of them chained, nine doctor and pin suites gave 432 passed, and tests/test_shape_doctor.py gave 439 passed, 3 skipped;
  • CI is green.

Lane: openreposhape-3 (openRepoShape-3)

This was referenced Oct 17, 2026
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 17, 2026 23:03
@sourcery-ai

sourcery-ai Bot commented Oct 17, 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 1 hour and 39 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit c100843 into main Oct 17, 2026
5 checks passed
@brettheap
brettheap deleted the l3-752-unreadable-side branch October 17, 2026 23:03
brettheap pushed a commit that referenced this pull request Oct 17, 2026
Restack of #781 after its parent #779 (#752) squash-landed on main as c100843.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
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>
brettheap added a commit that referenced this pull request Oct 18, 2026
…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) (#781)

Closes #753.

Ours renamed the `Makefile` copy to `build.mk` and theirs edited the
`Makefile`. git carries the conflict to `build.mk`, a name no pin 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.

Now `pin_restore` in `shape-doctor.py` keeps each conflicted path's stage-1
object, and `copies_renamed` calls a path no pin lists a copy's rename only on
exact evidence: the merge base holds nothing at that path, and its stage-1
blob is the blob of exactly one path of the whole base, a copy. No similarity
is guessed at. The new name joins `left_for_a_person` and the `why`
("build.mk, renamed from the copy Makefile, is conflicted too ... a person
merges it by hand"), and is never taken. Where the pin taken still lists the
old name, the `why` adds that the doctor will report it missing once the merge
is finished, unless it is renamed back.
`tests/test_doctor_pin_restore_merges.py` builds the real merges, and pins the
cases that name no rename.

Lane 3's independent reviewer RV-b (opus) found it READY in three rounds (#781
issuecomment-6229486930, -6229640251 and -6229719143). After #779 landed,
it was restacked onto #779's squash by three merges, and its changed lines
equal the reviewed delta. 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>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
Brings in #827 (#712, 9f235da), whose gate words the doctor copies, and
#779 (#752, c100843), lane 3's doctor PR.

One path conflicted, shape-doctor.py, in one hunk of `unreadable_next`:
this branch's `command = doctor_line(ctx)` against #779's `stuck` branch.
Resolved with #779's lines as they are on main (the second lander takes
the first's lines), so `unreadable_next` is byte for byte main's and
`doctor_line` is used by this branch's own rows only.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…trap_after_bump.py` imports both `appended_to_command_scope` and `needs_a_byte_name` (#715)

Main moved from `0dee22b3` to `ca47b71d` while round 3 was paused (#846, #779, #772, #738, #842, #814, #790, #820, #774, #830 and #841). `git merge-tree` of `8b4dbcf6` with it stopped in one path:

- `tests/test_bootstrap_after_bump.py`, its imports. Main's `aec3b38f` (tests append what they tell git to the command scope) added `appended_to_command_scope` to the `from conftest import (...)` list. This branch added `from test_bootstrap_spelling import needs_a_byte_name` below it (#715, round 1). Both are kept.

`scaffold-project.py`, `scripts/bump-leg.py` and `scripts/shape_materialize.py` merged without a conflict.

Lane: openreposhape-4 (openRepoShape-4)
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