Skip to content

siblings.py and shape-doctor.py: a link to nothing at ../<Project> is NOT A CLONE, reported and left as it is, so the dry run no longer says would clone where the real run ends CLONE FAILED, and the doctor's members row no longer calls the slot absent (#784) - #788

Merged
brettheap merged 7 commits into
mainfrom
l3-784-dangling-slot
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

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

Closes #784.

Stacked on #772; lands after it. Lands after the v1.1.0 tag.

What changed, and why

A dangling symbolic link at ../<Project> read as an empty slot, because exists follows a link. Measured with #772's head 1d149933 and a link to nothing at ../IRRS in the two-member holder. Every claim in the issue held:

templates/family-root/scripts/siblings.py:

  • not_a_clone (:471) now answers "is a link to nothing", with link as the kind of thing (:496). The docstring is at :487.
  • place therefore reports the slot as NOT A CLONE, in the finding the other NOT A CLONE slots get: "<who>: <at> is a link to nothing, so this command will not write into it. Move it aside or name the link something else; nothing here overwrites a link somebody made."
  • The link is left as it is, and the dry run says NOT A CLONE, not would clone.
  • dispatch (--make) is unchanged: holds_git (:460) answers "no" for a link to nothing (:776), so make park and make resume keep SKIPPED no clone.

shape-doctor.py:

  • unclonable (:7974) answers dangling for a link to nothing (:7996), so sibling_slot (:8004) says not-a-clone, and the doctor and make siblings agree again.
  • The row's sentence comes from UNREADABLE_BESIDE (:8174, "dangling": "is a link to nothing" at :8176). It says what is there and nothing of what make siblings does with it. That holds for every holder, because one whose pinned siblings.py predates this change still ends CLONE FAILED on the link.
  • The comments on sibling_slot, UNREADABLE_BESIDE and beside_sentence (:8179) say so.

What is tested

Commands, with a stub gh (exit 97) first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR, at e966a684, on #772's head 1d149933 (main e3137010):

/usr/bin/python3 -m pytest -q -rsx -p no:cacheprovider --basetemp=<scratch>/bt-a -o tmp_path_retention_policy=failed tests/test_family.py tests/test_family_siblings_placement.py tests/test_siblings_family_name.py tests/test_family_makefile_siblings.py tests/test_park_resume_targets.py tests/test_windows_paths.py tests/test_family_printed_quoting.py tests/test_update_shape_add.py tests/test_update_shape_kept_helper.py tests/test_repo_hygiene.py tests/test_cli_reference.py
723 passed in 168.79s (0:02:48)

/usr/bin/python3 -m pytest -q -rsx -p no:cacheprovider --basetemp=<scratch>/bt-b -o tmp_path_retention_policy=failed tests/test_shape_doctor.py tests/test_doctor_members_spelled.py tests/test_doctor_gitmodules_not_utf8.py
473 passed, 3 skipped in 361.88s (0:06:01)   (skips: tests/test_shape_doctor.py:10436, the PowerShell line, Windows host only)

/usr/bin/python3 -m pytest -q -rsx -p no:cacheprovider --basetemp=<scratch>/bt-c -o tmp_path_retention_policy=failed tests/test_shape_doctor.py -k "sibling or not_a_clone or slot"
42 passed, 402 deselected in 25.00s

Pinned shape copies

templates/family-root/scripts/siblings.py is a family-root copy carried by digest. A holder pinned before this change shows an upstream-changed row for it until update-shape.py brings it forward. No other pinned copy is touched.

Notes

  • The Windows skip is LINKS_HOLD, not conftest.WINDOWS_SKIP. It skips on Windows alike, but its reason is the true one, "a symbolic link needs a privilege on Windows". WINDOWS_SKIP's reason is a POSIX shell, which these tests do not need.
  • No docs change: no doc describes these slots or quotes the NOT A CLONE finding, and siblings.py's --help is unchanged.

🤖 Generated with Claude Code

Summary by Sourcery

Handle dangling sibling links consistently as non-cloneable slots across sibling placement and shape diagnostics.

New Features:

  • Treat dangling symbolic links as non-cloneable sibling slots and report them without modifying the link.

Bug Fixes:

  • Keep dry-run sibling placement results consistent with real runs by preventing clone attempts into dangling links.
  • Classify dangling links consistently in the shape doctor's member reports instead of marking them absent.

Tests:

  • Add coverage for preserving dangling links, reporting them as NOT A CLONE, and skipping them for make operations.

Brett Heap and others added 2 commits October 17, 2026 00:53
…, at `../<Project>` is NOT A CLONE, reported and left as it is, so the run no longer ends in a traceback at `iterdir` before the members after it are placed, and `make park` no longer ends in one over a folder it cannot enter (#735)

`place` asked only whether a folder had something in it: `iterdir` raised
`NotADirectoryError` for a file and `PermissionError` for a folder with
mode 0311 or 000, a traceback, exit 1. `not_a_clone` now gives the
finding's words for each slot nothing is cloned into: the folder of files
keeps its finding byte for byte, a file "is a file, not a folder", and a
folder it cannot list "is a folder that cannot be listed", the words the
doctor's `members` row says of the same slots. `holds_git` asks for a
`.git` and reads an `OSError` as none, for `place` and for `dispatch`
(`--make`), which Python 3.12 answered with `PermissionError` in a folder
that cannot be entered.

The doctor's agreement test (#705) now passes for a file and for both
locked folders, so its strict xfail is gone; the doctor's output is
unchanged, and its comments now say a holder pinned before this change
still ends in the traceback.

`siblings.py` is a family-root copy carried by digest: a holder pinned
before this change shows an `upstream-changed` row for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is NOT A CLONE, reported and left as it is, so the dry run no longer says `would clone` where the real run ends CLONE FAILED, and the doctor's `members` row no longer calls the slot `absent` and says `make siblings` places the member there (#784)

`exists` follows a link, so a dangling one read as nothing at all. In
`siblings.py`, `not_a_clone` now answers "is a link to nothing" for it, in
the finding the other NOT A CLONE slots get. In the doctor, `unclonable`
answers `dangling`, so `sibling_slot` says `not-a-clone`, and the row says
"is a link to nothing" and nothing of what `make siblings` does there: a
holder pinned before this change still ends CLONE FAILED on it. `make
park` and `make resume` keep `SKIPPED no clone`.

`siblings.py` is a family-root copy carried by digest: a holder pinned
before this change shows an `upstream-changed` row for it.

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 closes the gap where Path.exists() treated dangling ../<Project> links as empty slots: sibling placement and shape-doctor inspection now classify them consistently as NOT A CLONE/not-a-clone, report the link without overwriting it, and retain existing no-clone behavior for make verbs. Expanded tests verify exact diagnostics, link preservation, cross-tool agreement, and Windows-specific symlink handling.

Sequence diagram for dangling-link classification

sequenceDiagram
    participant User
    participant Siblings as siblings.py
    participant Doctor as shape-doctor.py
    participant Slot as ProjectSlot

    User->>Siblings: not_a_clone(ProjectSlot)
    Siblings->>Slot: exists()
    Slot-->>Siblings: false
    Siblings->>Slot: is_symlink()
    Slot-->>Siblings: true
    Siblings-->>User: NOT A CLONE
    Siblings-->>User: leave link unchanged

    User->>Doctor: sibling_slot(ProjectSlot)
    Doctor->>Slot: exists()
    Slot-->>Doctor: false
    Doctor->>Slot: is_symlink()
    Slot-->>Doctor: true
    Doctor->>Doctor: unclonable(ProjectSlot)
    Doctor->>Doctor: beside_sentence(ProjectSlot)
    Doctor-->>User: not-a-clone, is a link to nothing
Loading

File-Level Changes

Change Details Files
Treat dangling symbolic links as non-cloneable slots instead of absent paths.
  • Detect links whose targets do not exist in sibling placement and doctor inspection.
  • Report NOT A CLONE and leave the link untouched during dry runs and real runs.
  • Keep --make operations skipping the slot as having no clone.
  • Use consistent doctor wording and not-a-clone state for dangling links.
templates/family-root/scripts/siblings.py
shape-doctor.py
Add coverage for dangling-link behavior and preserve existing unreadable-slot coverage.
  • Parametrize family placement tests to create files, locked folders, and dangling links.
  • Assert failure status, exact diagnostic output, subsequent member placement, and unchanged slot contents.
  • Verify park/resume skip dangling links and doctor output agrees with siblings dry-run output.
  • Skip symlink cases on Windows with a symlink-specific reason.
tests/test_family.py
tests/test_shape_doctor.py

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-17T05:16:27Z) Independent review of #788 by RV-d (opus), on head e966a684 (stacked on #772): READY. No MUST and no SHOULD; two optional NITs. It lands only after the v1.1.0 tag, after #772.

Each link at ../IRRS, on a real two-member holder (--dry-run, make siblings, make park, make resume, then the doctor):

link #772's head: dry / real / doctor #788: dry / real / doctor
absolute, to nothing would clone / CLONE FAILED / absent NOT A CLONE / NOT A CLONE / not-a-clone, "is a link to nothing"
relative, to nothing same same
a link to a link to nothing same same
to a missing folder inside an existing one same same
a link loop same same
a relative link to a folder with files (control) NOT A CLONE unchanged
an absolute link to an empty folder (control) cloned through the link unchanged
into a folder this user cannot enter "a folder that cannot be listed" unchanged (N1)
  • make park and make resume say SKIPPED no clone for every dangling variant.
  • IRSS is always placed after it.
  • The link chain is untouched.
  • The doctor agrees with make siblings in every row.

Answers:

NITs (optional):

Mutants: all 5 are killed by the four a-link-to-nothing tests:

  • not_a_clone ignores is_symlink;
  • unclonable treats a dangling link as empty;
  • the dry run says "would clone";
  • the doctor loses its "dangling" wording;
  • a link gets the file wording.

Tests:

  • five files: 271 passed;
  • tests/test_shape_doctor.py -k (siblings rows): 43 passed;
  • the link probe: 8 passed on both heads.

Merges: clean with bf1f099b, every other open lane-3 PR, and lane 4's #690 tip.

Lane: openreposhape-3 (openRepoShape-3)

This was referenced Oct 17, 2026
Base automatically changed from l3-735-sibling-slot to main October 17, 2026 23:42
Brett Heap and others added 3 commits October 17, 2026 23:45
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… four files #772 and #788 both changed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T00:09:07Z) #788 is restacked after #772's squash (112eeed), as bec71bb5, by lane 3's three-merge recipe (writer R788, sonnet-xhigh):

  1. f204abe1 merges siblings.py: a file, or a folder make siblings cannot list or enter, at ../<Project> is NOT A CLONE, reported and left as it is, so the run no longer ends in a traceback at iterdir before the members after it are placed, and make park no longer ends in one over a folder it cannot enter (#735) #772's final head bcb8e54a. It is clean.
  2. 5f7ea9b3 merges the squash 112eeed2, taking ours only for siblings.py: a file, or a folder make siblings cannot list or enter, at ../<Project> is NOT A CLONE, reported and left as it is, so the run no longer ends in a traceback at iterdir before the members after it are placed, and make park no longer ends in one over a folder it cannot enter (#735) #772's four files, which conflicted as expected. git diff bcb8e54a 112eeed2 is empty on them, and this merge changed none of them.
  3. bec71bb5 merges main e62b5e17. It is clean.

Lane: openreposhape-3 (openRepoShape-3)

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 03:44
@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 11 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 0905eef into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the l3-784-dangling-slot branch October 18, 2026 03:44
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…ep 1 of 3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…k step 2 of 3)

Conflicts in shape-doctor.py and templates/family-root/scripts/siblings.py
were taken ours: #788's final head 5854aeb and its squash 0905eef have the
same tree, so the branch already holds the squash's version of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
… to nothing at `../<Project>` (`exists()` says no and `os.path.isjunction` says yes) is NOT A CLONE, as a link to nothing is, so the dry run no longer says `would clone` and the doctor's `members` row no longer calls the slot `absent` (#793) (#794)

Closes #793.

#788 tells a link to nothing from an empty slot with `Path.is_symlink()`,
which says no to a directory junction, the link Windows makes without
privilege. A junction to nothing at `../<Project>` therefore read as an empty
slot: with the junction simulated, the dry run said `would clone`, the real
run tried to clone into it, and the doctor's `members` row called the slot
`absent`.

Now `not_a_clone` in `templates/family-root/scripts/siblings.py` and
`unclonable` in `shape-doctor.py` also ask the junction check, spelled as
`family_link` spells it, `getattr(os.path, "isjunction", lambda _: False)`,
after `exists()`. Where it says yes for a slot that does not exist, the slot
gets the answer a link to nothing gets: `NOT A CLONE` in the dry run and
`not-a-clone` in the doctor, and the junction is left as it is.
`os.path.isjunction` is Python 3.12's. Before 3.12 there is none, so there a
junction to nothing still reads as an empty slot. Windows itself is not
measured: `tests/test_family_siblings_placement.py` and
`tests/test_shape_doctor.py` simulate the junction by making
`os.path.isjunction` say yes for the slot, for one to nothing and for one to a
live folder, so the check stays after `exists()`.

Lane 3's independent reviewer RV-d (opus) found it READY (#794
issuecomment-6232574289). Lane 3 checked the test-only NIT round after it, and
RV-d's READY stood (#794 issuecomment-6232681374). After #788 landed, it was
restacked onto #788's squash by three merges, and its changed lines equal the
reviewed delta (#794 issuecomment-6245446016). 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