Skip to content

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

Merged
brettheap merged 1 commit into
mainfrom
l3-1175-unreadable-mount-rows-tested
Oct 23, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

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 legs and members rows for a mount this user cannot reach are now each built by a test that fails without them, and the leg shape files note 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.py builds each branch rv975 found unbuilt:

  • MI (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 is spec: 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.
  • MJ, MK1, MK2, ML (members_verdict): a holder with TWO members, which no case had (the session's holder has one). with_a_member_beside_one_a_checkout_cures adds Ghost (a family.yaml row, a gitlink at the first member's commit, a .gitmodules section with a path and a url, an empty mount) and empties IRRS, so a checkout cures it.
    • test_beside_a_member_checkout_the_unreached_member_gets_the_advice, parametrized the-holders-bootstrap (MK1, MJ) and the-git-line (MK2, with holder_without(root, "bootstrap.py")). Ghost's listing is refused, and Path.is_file raises a refused stat for any name in members/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 ends rerun_sentence("member") + ". " + PERMISSION_WHY.
    • test_a_member_at_mode_000_beside_one_a_checkout_cures: the same holder with a real chmod 000 on members/Ghost (restored in mode_of's finally, skipped as root), run as a person runs the doctor. On this bench's 3.12 it is what MJ breaks.
    • ML is not dead: rerun_sentence("member") ("Run the doctor again afterwards, because ...") is in those whys only because of or 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 files note for a file this user cannot read: its WHY

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.md at mode 000 over the project fixture, on Python 3.12 (W1174-scratch's probe, doctored):

before (main 961640c6):
[leg-shape-files] status=note
  reason: spec: AGENTS.md rendered, CLAUDE.md cannot be read (Permission denied), .gitignore identical; code: AGENTS.md rendered, CLAUDE.md identical, .gitignore identical
  next:   None
  why:    None
verdict: COMPLIANT

after (this head):
[leg-shape-files] status=note
  reason: spec: AGENTS.md rendered, CLAUDE.md cannot be read (Permission denied), .gitignore identical; code: AGENTS.md rendered, CLAUDE.md identical, .gitignore identical
  next:   None
  why:    Make what this user may not open readable by this user: `chmod u+r` on a file, `chmod u+rx` on a directory (and on every directory above it inside the root), or fix who owns it. Then run this doctor again
verdict: COMPLIANT

Out of scope

Nothing in the issue is under "Needs a ruling" or "Documented limit". docs/release-notes/v1.2.0.md still 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:cacheprovider at this head (f3247e54) on Python 3.12, under a stub gh (exit 97) first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null and EMAIL set, one pytest at a time. The first result line is each file's last line.

Test file Result
tests/test_doctor_unreadable_paths.py 43 passed in 17.63s (34 at main)
tests/test_doctor_next_is_a_command.py 61 passed in 64.02s
tests/test_legs_readers_not_a_list.py 74 passed in 34.28s
tests/test_doctor_unreadable_pin.py 52 passed in 15.53s
tests/test_repo_hygiene.py 101 passed in 1.51s
tests/test_path_guard_hygiene.py 49 passed in 6.63s
tests/test_no_auto_maintenance.py 58 passed, 2 skipped in 5.43s (two skips: the session began with GIT_CONFIG_GLOBAL set)
tests/test_agents_md_codes.py 47 passed in 0.95s
tests/test_cli_reference.py 23 passed in 23.23s
tests/test_update_add_codes_documented.py 6 passed in 0.36s
tests/test_shape_doctor.py 445 passed, 3 skipped in 444.42s
tests/test_doctor_unreadable_paths.py tests/test_shape_doctor.py, in ONE pytest 488 passed, 3 skipped in 437.46s

The last row is because the new file imports the session fixtures standard, scaffolded_here and holder from tests/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 of tests/test_doctor_unreadable_paths.py. The file was restored by copy after each one and checked with cmp.

Mutant The one-site edit of shape-doctor.py Killed by (new file) Main's copy of the test file
MI check_legs: the , and <the mount's line> after a missing gitlink is dropped test_a_leg_with_no_gitlink_names_the_mount_it_cannot_reach_too (1 failed, 42 passed) passes (34 passed)
MJ members_verdict: the carried test of a member's .gitmodules no longer skips a mount this user cannot reach (if MOUNT_UNREAD not in entry dropped) 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) passes
MK1 members_verdict, the holder's own bootstrap: the advice sentences (said) are dropped after the rerun sentence the same two (2 failed, 41 passed) passes
MK2 members_verdict, the git line: the advice sentences are dropped test_beside_a_member_checkout_the_unreached_member_gets_the_advice[the-git-line] (1 failed, 42 passed) passes
ML members_verdict: or bool(refused) is dropped from rerun the three above (3 failed, 40 passed) passes
N1-note-why check_leg_shape_files: the note is built without why=advice test_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) passes
N2-missing-join check_leg_shape_files: the advice no longer follows a missing file's line test_beside_a_missing_leg_file_the_advice_follows_its_line[.gitignore] and [AGENTS.md] (2 failed, 41 passed) passes
N3a-stat-exc-kept shape_file_state: a refused stat returns None for its error test_a_leg_file_refused_for_another_reason_gets_the_other_advice[stat] (1 failed, 42 passed) passes
N3b-read-exc-kept shape_file_state: a refused read returns None for its error test_a_leg_file_at_mode_000_is_told_the_cure_and_gates_nothing, ..._gets_the_other_advice[read], and the two test_beside_a_missing_leg_file_the_advice_follows_its_line cases (4 failed, 39 passed) passes
N4-row-collects check_leg_shape_files: the row does not gather each leg's errors (refused.extend(unread_files) dropped) six of the new file's tests (6 failed, 37 passed) 1 failed, 33 passed: an existing test fails too
N5-leg-collects leg_shape_comparison: a leg does not collect a file's error (refused.append(exc) dropped) the same six (6 failed, 37 passed) 1 failed, 33 passed
N6-unread-status check_leg_shape_files: whether the row is a note is read from the words again, not from the errors four of the new file's tests (4 failed, 39 passed) 1 failed, 33 passed

All 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 cmp said it was identical; the run left no mutant in the tree (mutants-1175.log, in the lane's scratch).

Merges

git merge-tree --write-tree of this head (f3247e54) with origin/main (961640c6, main is frozen there until the v1.2.0 tag) and with every open PR whose files include shape-doctor.py or tests/test_doctor_unreadable_paths.py, re-listed with gh pr list --state open --json number,headRefOid,files at 03:23Z. All six are clean, no conflict:

Against Head Touches Result
origin/main 961640c681b5 clean
#1109 e446ff58bdfc shape-doctor.py clean
#1144 89486f699f9a shape-doctor.py clean
#1147 b75babee3ee2 shape-doctor.py, tests/test_doctor_unreadable_paths.py clean
#1159 66e68f948d86 shape-doctor.py clean
#1172 5aa0f116defc shape-doctor.py clean

#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

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

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Reviewer's Guide

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

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

Sequence diagram for unreadable mount member advice

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

Flow diagram for leg shape file note and missing-file advice

flowchart 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]
Loading

File-Level Changes

Change Details Files
Propagate read failures from individual leg shape files so the row can report cause-specific remediation without changing its note-only, non-gating behavior.
  • Return each shape-file comparison together with any refused OSError.
  • Aggregate unread-file errors across legs and derive permission or non-permission advice.
  • Append advice after missing-file guidance while preserving the missing file’s NEXT command; use unread errors rather than rendered reason text to determine note status.
shape-doctor.py
Add regression coverage for previously unbuilt unreadable-mount branches and the new leg-shape-file advice behavior.
  • Build two-member holders to exercise bootstrap and git-line NEXT branches when a neighboring mount is unreachable.
  • Cover missing gitlinks over unreadable mounts, mode-000 members, stat/read permission failures, other I/O failures, and missing-file advice ordering.
  • Add mutant-killing assertions for mount naming, advice text, refusal propagation, and note classification.
tests/test_doctor_unreadable_paths.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1175 Add tests that build and kill each of the five previously untested unreadable-mount branches in the legs and members rows (MI, MJ, MK1, MK2, and ML). ✅
#1175 Preserve the underlying read/stat error for an unreadable leg shape file and include the gate’s cause-specific advice as the leg-shape-files note’s WHY, without changing its note status or NEXT behavior. ✅

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

@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-23T08:13:14Z) Independent review (RV1193, opus, ruling 8) of #1193 at f3247e54 (main 961640c6): READY. No MUST, no SHOULD.

Checked:

NITs:

  1. Mutant N10 survives. The leg-shape-files mount early return handing back [unread[1]] passes every test, so "a mount gets no WHY" (shape-doctor.py:7866-7867) is not pinned.
  2. shape-doctor.py:7865-7866 says "beside a missing file's copy". For a rendered file the line is scaffold-project.py --help, so "line" is the right word.
  3. shape-doctor.py:7769 is 81 columns. No action.
  4. Two legs at one path: the note's why gives the advice, while the reason names only the leg kept in per_leg. At main that row was ok with no why. No action.

For #1174:

  • Stack it on this branch: the import lines and the module docstring paragraph would conflict.
  • Rule on purpose whether a refused git directory on a readable mount should skip the leg's file comparison, since leg_shape_comparison returns early on any mount_read refusal, and pin the answer.

Lane 3 has passed both points, and NIT 1, to #1174's writer.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai

sourcery-ai Bot commented Oct 23, 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 4 hours and 14 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 4c5227b into main Oct 23, 2026
5 checks passed
@brettheap
brettheap deleted the l3-1175-unreadable-mount-rows-tested branch October 23, 2026 19:33
brettheap pushed a commit that referenced this pull request Oct 23, 2026
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>
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