Repository navigation
shape-doctor.py: the leg shape files note for a leg file this user cannot read gives the gate's advice as its WHY, and each branch #975 added for a mount this user cannot reach is built by a test (#1175) - #1193
Conversation
…cannot read gives the gate's advice as its WHY, and each branch #975 added for a mount this user cannot reach is built by a test (#1175) `shape_file_state` keeps the `OSError` of a refused `stat` or read beside its words, `leg_shape_comparison` hands each one back, and `check_leg_shape_files` gives the note `cannot_read_why` of them: the permission advice or the other one, ending "Then run this doctor again". Beside a missing file's line the advice follows that line's why. The NEXT stays None and the row a note, so the verdict does not change. A whole mount gets no why here: the `legs` row names it and gives its advice. tests/test_doctor_unreadable_paths.py builds the five branches rv975's mutants MI, MJ, MK1, MK2 and ML found unbuilt: a leg with no gitlink over a mount it cannot list, and a holder with two members, one unreachable beside one a checkout cures, by the holder's own bootstrap and by the git line, in process and with a real mode 000. ML's term is not dead: the `rerun_sentence` it adds is not the advice's own "run this doctor again", and the cases tell the two apart. The note's why is asserted word for word, for a permission and for an I/O error, for a refused `stat` and a refused read, beside a missing file, and under a real mode 000. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR preserves non-gating behavior for unreadable leg shape files while carrying their underlying OSError through comparison and attaching the appropriate remediation advice, and adds focused tests that build all five previously untested unreadable-mount branches plus the new advice and ordering semantics. Sequence diagram for unreadable leg shape file advicesequenceDiagram
participant Doctor
participant Comparison as leg_shape_comparison
participant State as shape_file_state
participant Gate as cannot_read_why
Doctor->>Comparison: leg_shape_comparison(ctx, role, rel)
Comparison->>State: shape_file_state(here, there, name)
State-->>Comparison: state, OSError or None
Comparison-->>Doctor: entry, missing files, refused errors
Doctor->>Gate: cannot_read_why(refused)
Gate-->>Doctor: permission or other-cause advice
Doctor-->>Doctor: Return NOTE with advice and NEXT None
Sequence diagram for unreadable mount member advicesequenceDiagram
participant Doctor
participant Verdict as members_verdict
participant Mount
participant Advice
Doctor->>Verdict: members_verdict(...)
Verdict->>Mount: Check member listing and git metadata
Mount-->>Verdict: Permission denied
Verdict->>Advice: rerun_sentence(member)
Verdict->>Advice: Permission remediation advice
Advice-->>Verdict: Combined why
Verdict-->>Doctor: Member row names unreachable member and gives NEXT
Flow diagram for leg shape file note and missing-file adviceflowchart TD
A[check_leg_shape_files] --> B[leg_shape_comparison]
B --> C[shape_file_state]
C --> D{Unreadable file?}
D -- Yes --> E[Collect OSError]
D -- No --> F[Record file state]
E --> G[cannot_read_why]
F --> H{Missing file?}
H -- Yes --> I[missing_shape_file_fix]
H -- No --> J[Continue comparison]
I --> K[Append unreadable-file advice]
G --> K
K --> L[Return NOTE; no NEXT; verdict remains non-gating]
J --> M[Return OK or NOTE for unreadable mount]
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-23T08:13:14Z) Independent review (RV1193, opus, ruling 8) of #1193 at Checked:
NITs:
For #1174:
Lane 3 has passed both points, and NIT 1, to #1174's writer. Lane: openreposhape-3 (openRepoShape-3) |
|
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 4 hours and 14 minutes by commenting |
Ours for the parent files shape-doctor.py and tests/test_doctor_unreadable_paths.py: S carries F, which this branch already holds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>



What changed
A follow-up outside v1.2.0 (Brett Heap, 2026-10-21T10:32:04Z). It lands only after the v1.2.0 tag, on a later merge word.
Two parts, as the issue has them: the five branches #975 added to the
legsandmembersrows for a mount this user cannot reach are now each built by a test that fails without them, and theleg shape filesnote for a leg file this user cannot read now carries the gate's advice as its WHY.The five surviving mutants (tests only)
tests/test_doctor_unreadable_paths.pybuilds each branch rv975 found unbuilt:check_legs, the, and <the mount's line>after a missing gitlink):test_a_leg_with_no_gitlink_names_the_mount_it_cannot_reach_too. A leg with no gitlink recorded (make_no_gitlink) over a mount whose listing is refused (refusing). The reason isspec: no gitlink recorded, so this leg is not mounted as a submodule at all, and spec: cannot be listed (Permission denied), the NEXT is the lockstep gate, the why ends on the advice, and the leg is no drift.members_verdict): a holder with TWO members, which no case had (the session's holder has one).with_a_member_beside_one_a_checkout_curesaddsGhost(afamily.yamlrow, a gitlink at the first member's commit, a.gitmodulessection with a path and a url, an empty mount) and emptiesIRRS, so a checkout cures it.test_beside_a_member_checkout_the_unreached_member_gets_the_advice, parametrizedthe-holders-bootstrap(MK1, MJ) andthe-git-line(MK2, withholder_without(root, "bootstrap.py")).Ghost's listing is refused, andPath.is_fileraises a refusedstatfor any name inmembers/Ghost, as 3.12 and 3.13 raise one there (gitmodules_refused_in). The row names both members, the NEXT is the bootstrap or the git line, and the why endsrerun_sentence("member") + ". " + PERMISSION_WHY.test_a_member_at_mode_000_beside_one_a_checkout_cures: the same holder with a realchmod 000onmembers/Ghost(restored inmode_of'sfinally, skipped as root), run as a person runs the doctor. On this bench's 3.12 it is what MJ breaks.rerun_sentence("member")("Run the doctor again afterwards, because ...") is in those whys only because ofor bool(refused), and the advice's own "Then run this doctor again" is not that sentence, so the cases above tell them apart. The term stays.The
leg shape filesnote for a file this user cannot read: its WHYshape_file_statereturns(the words, the OSError or None): the error of a refusedstator read is kept beside the words, which carry only its reason.leg_shape_comparisonreturns a third value, theOSErrorof each leg file this user cannot read.check_leg_shape_filesgathers them, and the note'swhyiscannot_read_why(refused):PERMISSION_ADVICEfor a permission andOTHER_REASONfor any other cause, ending "Then run this doctor again". Beside a missing file's line, the advice follows that line's own why, or is the why where it has none (acp). Whether the row is a note is now read from those errors, not from the words.legsrow names it and gives the advice, and shape-doctor.py and shape_advisory.py: a path in the root this user cannot reach is named on every Python, never read as absent or stood in for by the template, a root it cannot search is refused first, and a row that could not ask is never DRIFTED #975's reason for the note ("thelegsrow asserts the mount") covers it. The docstring says so.Decided where the Direction left it open: the advice follows a missing file's line rather than replacing it, because the copy is still the NEXT for the missing file.
Measured
spec/CLAUDE.mdat mode 000 over theprojectfixture, on Python 3.12 (W1174-scratch's probe,doctored):Out of scope
Nothing in the issue is under "Needs a ruling" or "Documented limit".
docs/release-notes/v1.2.0.mdstill lists both as v1.2.0 known limits and is left alone: it records v1.2.0's state. The doctor over a leg whose git directory this user cannot read is a separate PR, stacked or on main as merge-tree shows.Tests
Every run is
/usr/bin/python3 -m pytest -q -p no:cacheproviderat this head (f3247e54) on Python 3.12, under a stubgh(exit 97) first onPATH,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/nullandEMAILset, one pytest at a time. The first result line is each file's last line.tests/test_doctor_unreadable_paths.pytests/test_doctor_next_is_a_command.pytests/test_legs_readers_not_a_list.pytests/test_doctor_unreadable_pin.pytests/test_repo_hygiene.pytests/test_path_guard_hygiene.pytests/test_no_auto_maintenance.pyGIT_CONFIG_GLOBALset)tests/test_agents_md_codes.pytests/test_cli_reference.pytests/test_update_add_codes_documented.pytests/test_shape_doctor.pytests/test_doctor_unreadable_paths.py tests/test_shape_doctor.py, in ONE pytestThe last row is because the new file imports the session fixtures
standard,scaffolded_hereandholderfromtests/test_shape_doctor.py(as main's copy already did), so the two run together in one session as CI runs them.Mutants
Each is a single-site edit of
shape-doctor.py, run against this file and against main's copy oftests/test_doctor_unreadable_paths.py. The file was restored by copy after each one and checked withcmp.shape-doctor.pycheck_legs: the, and <the mount's line>after a missing gitlink is droppedtest_a_leg_with_no_gitlink_names_the_mount_it_cannot_reach_too(1 failed, 42 passed)members_verdict: thecarriedtest of a member's.gitmodulesno longer skips a mount this user cannot reach (if MOUNT_UNREAD not in entrydropped)test_a_member_at_mode_000_beside_one_a_checkout_cures,test_beside_a_member_checkout_the_unreached_member_gets_the_advice[the-holders-bootstrap](2 failed, 41 passed)members_verdict, the holder's own bootstrap: the advice sentences (said) are dropped after the rerun sentencemembers_verdict, the git line: the advice sentences are droppedtest_beside_a_member_checkout_the_unreached_member_gets_the_advice[the-git-line](1 failed, 42 passed)members_verdict:or bool(refused)is dropped fromreruncheck_leg_shape_files: the note is built withoutwhy=advicetest_a_leg_file_at_mode_000_is_told_the_cure_and_gates_nothing,test_a_leg_file_this_user_cannot_read_is_said_not_called_absent,test_a_leg_file_refused_for_another_reason_gets_the_other_advice[stat]and[read](4 failed, 39 passed)check_leg_shape_files: the advice no longer follows a missing file's linetest_beside_a_missing_leg_file_the_advice_follows_its_line[.gitignore]and[AGENTS.md](2 failed, 41 passed)shape_file_state: a refusedstatreturnsNonefor its errortest_a_leg_file_refused_for_another_reason_gets_the_other_advice[stat](1 failed, 42 passed)shape_file_state: a refused read returnsNonefor its errortest_a_leg_file_at_mode_000_is_told_the_cure_and_gates_nothing,..._gets_the_other_advice[read], and the twotest_beside_a_missing_leg_file_the_advice_follows_its_linecases (4 failed, 39 passed)check_leg_shape_files: the row does not gather each leg's errors (refused.extend(unread_files)dropped)leg_shape_comparison: a leg does not collect a file's error (refused.append(exc)dropped)check_leg_shape_files: whether the row is a note is read from the words again, not from the errorsAll twelve are KILLED. MI, MJ, MK1, MK2 and ML are the five rv975 found unbuilt: each passes main's copy of the file, as it did before this change. The file was restored from a copy after each one and
cmpsaid it was identical; the run left no mutant in the tree (mutants-1175.log, in the lane's scratch).Merges
git merge-tree --write-treeof this head (f3247e54) withorigin/main(961640c6, main is frozen there until the v1.2.0 tag) and with every open PR whose files includeshape-doctor.pyortests/test_doctor_unreadable_paths.py, re-listed withgh pr list --state open --json number,headRefOid,filesat 03:23Z. All six are clean, no conflict:origin/main961640c681b5e446ff58bdfcshape-doctor.py89486f699f9ashape-doctor.pyb75babee3ee2shape-doctor.py,tests/test_doctor_unreadable_paths.py66e68f948d86shape-doctor.py5aa0f116defcshape-doctor.py#1147 touches the same test file and merges clean with it. The doctor over a leg whose git directory this user cannot read (#1174) is a separate PR and will say whether it stacks on this one.
Closes #1175
Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code