Skip to content

family.py and the family root's siblings.py: a holder's .gitmodules this user cannot read, as through a link into a folder it cannot search, is gitmodules-unreadable, never no section for remove's rollback or no url for make siblings (#1110) - #1164

Draft
brettheap wants to merge 7 commits into
mainfrom
fix/1110-family-unsearchable-gitmodules

Conversation

@brettheap

@brettheap brettheap commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1110. This is its slice B: scripts/family.py's _gitmodules_names, _sections_giving and _stages_as, and the family root's scripts/siblings.py submodule_urls. The issue stays open for its other sites.

It is outside v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z ("leave the follow-ups out of v1.2.0"), and lands after the tag, on a later word.

What

Each of the three readers asked Path.is_file() of the holder's .gitmodules. Python 3.9 to 3.13 raise a refused stat (PermissionError); from 3.14 is_file() answers False to any OSError.

Measured with a family holder whose .gitmodules is a link into a folder at mode 000: at base 038dafba9 on Python 3.12.3 and on a 3.14-like shim (a sitecustomize that gives pathlib 3.14's answers), and at head on both. The control, the same link into a folder at 755, reads the file through the link at base and at head, on both.

Caller base, 3.12.3 base, 3.14-like shim head, 3.12.3 and the shim
family.py remove (as a person runs it), dry run and real, link into 000 or 755 gitmodules-unstaged, exit 2: git lists the link as modified, so this refusal comes before either reader same same (the order is kept)
family.py remove (as a person runs it), dry run and real, .gitmodules a file at mode 000, git's stat cache stale (git ls-files -m lists it) "a git or gh command failed", quoting git hash-object same gitmodules-unreadable, exit 2, nothing written
the same, git's stat cache current (git ls-files -m does not list it) "a git or gh command failed", quoting git config --file .gitmodules --list same gitmodules-unreadable, exit 2, nothing written
_gitmodules_names / _sections_giving / _store_git_uses / _unrestorable / _checked_one_section / _checked_mount_readable (in process) PermissionError (None, []) / [] / None / "no section of .gitmodules gives git members/IRRS" / None / True each: gitmodules-unreadable, .gitmodules: cannot be read (Permission denied)
remove's rollback (a pre-commit hook links .gitmodules into 000 and refuses the commit) traceback, exit 1, members/IRRS left gone, nothing typed exit 2, "The checkout of members/IRRS comes back only from its remote"; typed: the .gitmodules index entry, then only mkdir members/IRRS exit 2, the step named with its cure; typed: the .gitmodules index entry, git checkout -- .gitmodules, git -c protocol.allow=never submodule update --init --no-fetch -- members/IRRS, git -C members/IRRS checkout -q main --. Typed as printed once the mode is given back, they put the member back on main and leave git status clean
remove's rollback (a pre-commit hook that only runs chmod 000 .gitmodules) exit 2, "comes back only from its remote"; typed: the index entry, then mkdir members/IRRS the same: exit 2, "comes back only from its remote"; typed: the index entry, then mkdir members/IRRS as the row above
bump's and remove's rollback (a pre-commit hook that only runs chmod 000 family.yaml and refuses the commit): PinRollback._file_by_hand serves family.yaml too exit 2, "NOT family.yaml restored byte for byte"; typed: only the family.yaml index entry. With the mode given back, family.yaml stays the copy the verb had rewritten, and git status lists M family.yaml the same exit 2, the same sentence; typed: the index entry, then git checkout -- family.yaml. With the mode given back, family.yaml holds its recorded bytes and git status --porcelain is empty
siblings.py --dry-run / family.py siblings --dry-run traceback, exit 1 exit 0, "would clone https://github.com/InkRouter/IRRS.git (family.yaml repository:)" gitmodules-unreadable, exit 2
siblings.py (real run) traceback, exit 1 tried to clone from github.com, CLONE FAILED, exit 1 gitmodules-unreadable, exit 2, nothing cloned beside the holder
siblings.py --make help --dry-run, no clones traceback, exit 1 as the control: exit 1, make help ran in 0 of 2 members gitmodules-unreadable, exit 2

PinRollback._file_by_hand (scripts/family.py:8236-8256) serves every file the rollback records: bump's family.yaml, and remove's .gitmodules and family.yaml. Taking bytes it cannot read for not the recorded ones, which the two .gitmodules rollback rows above need, therefore also changes bump and remove for an unreadable family.yaml: git checkout -- family.yaml is typed after the index entry, and with the mode given back family.yaml comes back as recorded, where at base it stayed the copy the verb had rewritten (the last row above). It is the same fix at a second file, and one row holds it under each verb (r1164b's SHOULD-1).

In process, with the stat of a .gitmodules git still reads as the index's refused: at base remove --dry-run said no section is removed, exit 0, and the real run was refused only by its rollback's record, as member-unreadable. At head both are gitmodules-unreadable, exit 2, before any git command that writes, and the holder is left as it was.

The refusal names the file and the cause. A permission gets the chmod cure; any other cause gets none:

REFUSED gitmodules-unreadable: .gitmodules: cannot be read (Permission denied)
Remediation: siblings.py reads the holder's .gitmodules for each member's url, the one `make bootstrap` uses, and nothing was cloned, fetched or run without it. A path this command cannot read is a question it cannot answer, never a pass. 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), or fix who owns it. Then run the same command again.

Why

  • One file, one code. gitmodules-unreadable is the code add's _submodule_paths and the holder's own validate-family.py already give this file, so the cure follows the cause at each site this slice changes (not in add's read or the holder's validator's, which keep their own remediation: see Known limits). In family.py the constant is GITMODULES_UNREAD_CODE, so it is not read for GITMODULES_UNREADABLE, which is the remediation _submodule_paths gives a file git cannot parse.
  • Whatever git's stat cache says. _stages_as, which remove and add ask before either reader, now asks _gitmodules_on_disk once lstat says the file is regular, before git hash-object. Without it, a file at mode 000 was refused two ways: by git hash-object's failure where git had to read the file to compare it, and as gitmodules-unreadable where git's cache spared the read. A link is still gitmodules-unstaged's first. The remedy says what the verb reads the file for (GITMODULES_READ_FOR), so add is not told what remove does.
  • Asked as _is_a asks. Nothing at the name is still no file, as it always was, and so is a directory. A refused stat is refused by name.
  • os.access too, after _checked_mount_readable's HEAD precedent. Without it, a .gitmodules at mode 000 whose stat succeeds is still read as giving no url in siblings.py (git's read fails, and that failure reads as no url) and, in the rollback, as no store.
  • No "inside the holder" in the cure (where=""): a link can lead out of the holder.
  • The rollback takes it as it takes a store it cannot read (READABLE_ONCE_CURED = (STORE_UNREADABLE, GITMODULES_UNREAD_CODE)), in PinRollback._undo_work_tree and _work_tree_by_hand. The mount is made again, empty, and the update is typed after the cure; it is never "comes back only from its remote". Met inside the rollback, the refusal's detail carries its cure, as a store's does, since the sentence prints no remediation.
  • Bytes it cannot read are not the recorded ones. _file_by_hand typed only the index entry where it could not read the current .gitmodules. The hook's copy is the one git rm wrote, without the member's section, so the update typed after the cure said No url found for submodule path 'members/IRRS'. Now git checkout -- .gitmodules is typed where the index held the recorded bytes; where they were there already, it writes the same bytes. Once cured, the checkout comes back from the store git rm keeps, with no network. _file_by_hand serves every recorded file, so the same change reaches family.yaml under bump and remove: git checkout -- family.yaml follows the index entry, and the file comes back as recorded (see What).
  • The CLI order is kept. gitmodules-unstaged still refuses a person's linked .gitmodules first.
  • siblings.py: path_unreadable(code, ...) is new and member_unreadable delegates to it. _run_placement and dispatch_all catch the refusal and return 2 before anything is cloned, fetched or run. The docstring's exit-2 paragraph names the case.

Tests

  • New tests/test_family_gitmodules_unreadable.py, 42 rows. Each site has a control (nothing there, a directory there, or a link this user can read); in-process rows for a refused stat, an I/O error and a refused os.access, which run on every Python and platform and as root; real links into a folder at 000, or a file at 000, skipped on Windows and as root; and, for family.yaml, a pre-commit hook that only takes its mode off, one row run under bump and under remove, skipped the same way.

    • head 74096eb47: 42 passed on 3.12.3, and 42 passed on the 3.14-like shim (it loaded in 14 family.py and 6 siblings.py subprocesses);
    • base 038dafba9: 31 failed, 11 passed, on 3.12.3 and on the shim alike, the same 31: the 29 of the previous head and the new row under each verb. Both new rows fail on behaviour, at assert manifest.read_bytes() == recorded after the typed commands ran. On the shim no row reached the network: the one row that tries a clone, test_make_siblings_refuses_a_gitmodules_linked_into_a_locked_folder[real], stopped three times at transport 'https' not allowed.
    • The 11 that pass on base are the controls (3 of them the new directory rows) and the order guard test_a_persons_remove_over_a_linked_gitmodules_is_refused_first.
    • The round after review (r1164): at the previous head 4352ac2fd, 7 of the 40 fail. Five are this round's behaviour: the two rollback rows typed by hand (the typed git submodule update exits 128), the stale-cache remove row, the in-process add row and the rollback sentence's cure. Two fail only on the renamed constant.
    • The round after review r1164b (SHOULD-1, NIT-1, NIT-2): the new row, with a docstring paragraph and two imports, is the only change to the file, and git diff --stat 3162b523b 74096eb47 names that test file alone, so scripts/family.py and siblings.py are as they were.
  • tests/test_path_guard_hygiene.py. The three KNOWN_OPEN entries for these sites are taken out. Before that, its stale-entry check failed on exactly those three ("0 found, 1 listed"). At 4352ac2fd: 49 passed (hygiene); at 3162b523b and at 74096eb47: 107 passed, 2 skipped with tests/test_no_auto_maintenance.py (call G2 below).

  • Mutants, each run against the new file in a scratch clone at head, each file restored after: review r1164's 22, and 7 more for this round. Run at 3162b523b: 29 of 29 killed, no survivors, none "not applied"; the clone was restored after each (git status of the clone shows only the untracked probe file). The two that survived at 4352ac2fd, M10 and M21, are killed now by the directory rows (r1164 SHOULD-1).

    One more for r1164b's SHOULD-1, run at 74096eb47: A21, _file_by_hand keeping the base behaviour for family.yaml only (same = name != GITMODULES), against the new file with tests/test_family_py_unreadable.py and tests/test_family_bump_remove_rollback.py (148 rows). It is killed, on 3.12.3 and on the shim: 2 rows failed, exactly the new row under bump and under remove, and the other 146 pass with it, so no earlier row held that case. The same 148 rows with no mutant: 148 passed on both. The clone's scripts/family.py blob equals HEAD's after it. The code M1-M29 mutate is unchanged at 74096eb47, so that is 30 of 30 killed, no survivors.

    Mutant Rows failed (at 3162b523b; A21 at 74096eb47)
    M1 _gitmodules_on_disk: _is_a back to .is_file() 5
    M2 _gitmodules_on_disk: os.access branch removed 9
    M3 _gitmodules_on_disk: OSError taken as absent 19
    M4 where="" dropped 9
    M5 _undo_work_tree back to == STORE_UNREADABLE 1
    M6 _work_tree_by_hand back to != STORE_UNREADABLE 4
    M7 READABLE_ONCE_CURED without the .gitmodules code 4
    M8 the code set to member-unreadable 15
    M9 except OSError narrowed to PermissionError 2
    M10 S_ISREG changed to any mode (survived at 4352ac2fd) 2
    M11 _gitmodules_names back to .is_file() 9
    M12 _sections_giving back to .is_file() 6
    M13 siblings.py submodule_urls: _is_a back to .is_file() 3
    M14 submodule_urls: os.access branch removed 1
    M15 submodule_urls: OSError taken as {} 10
    M16 submodule_urls: except OSError narrowed to PermissionError 1
    M17 path_unreadable: advice always OTHER_REASON 5
    M18 path_unreadable: the code ignored 10
    M19 _run_placement: refusal exit 2 changed to 1 4
    M20 dispatch_all: refusal exit 2 changed to 1 2
    M21 submodule_urls: S_ISREG changed to any kind (survived at 4352ac2fd) 1
    M22 _undo_work_tree: the mount not made again for any refusal 1
    M23 _file_by_hand: bytes it cannot read back to return typed 2 (4 at 74096eb47, with the family.yaml rows)
    M24 _stages_as: _gitmodules_on_disk not asked 2
    M25 _stages_as: the verb not passed on 1
    M26 _gitmodules_on_disk: the remedy always remove's 1
    M27 _undo_work_tree: no cure appended to the detail (NIT-1) 1
    M28 _undo_work_tree: the permission cure changed to OTHER_CURE (NIT-1) 1
    M29 _file_by_hand: bytes it cannot read taken as the same 2 (4 at 74096eb47, with the family.yaml rows)
    A21 (r1164b's) _file_by_hand: base behaviour kept for family.yaml only 2 (the new row, [bump] and [remove]), on 3.12.3 and on the shim
  • The pin. A family holder made at base 038dafba9 passes its own scripts/validate-family.py (exit 0, 10 copied shape files match). update-shape.py check against head names exactly scripts/siblings.py as upstream-changed, with the other 9 unchanged (measured at 4352ac2fd; this round does not touch siblings.py). Every name head's siblings.py imports from repo_shape (10) and bootstrap (6) is in a base holder's copies, so tests/test_update_shape_kept_helper.py needs no change.

  • The gate on Python 3.12.3 (no full suite):

    Call at 74096eb47 at 3162b523b at 4352ac2fd
    tests/test_family_gitmodules_unreadable.py (the new file) 42 passed on 3.12.3 and 42 on the shim 40 passed on both 33 passed (33 rows, as the file stood then)
    tests/test_family_py_unreadable.py and tests/test_family_bump_remove_rollback.py 106 passed 106 passed 106 passed (review r1164, with this round's _file_by_hand change)
    tests/test_path_guard_hygiene.py and tests/test_no_auto_maintenance.py (a new row sets core.trustctime locally) 107 passed, 2 skipped (call G2; the 2 skipped rows of test_no_auto_maintenance.py need a session that began without GIT_CONFIG_GLOBAL, and the probe environment sets it to /dev/null) 107 passed, 2 skipped (call G2) 49 passed (hygiene)
    the doc trio (test_agents_md_codes, test_cli_reference, test_repo_hygiene) 170 passed (run as a check: no doc, code or --help changed) 170 passed (call G3) 170 passed
    the files that name a changed function or refusal (test_family_member_branch_not_utf8, test_family_add_move_back_sections, test_family_refuse_up_front, test_family_remove_gitmodules, test_family_add_spelling_leftovers, test_family_add_spellings_remedies, test_refusal_default_remediation) not re-run: scripts/family.py is unchanged since 3162b523b 124 passed (call G4: the first three files) and 169 passed (call G5: the last four) in the 132-file run below
    tests/test_scaffold_leg_path_collisions.py not re-run: only the new test file differs 104 passed (lane 1, after this round) 104 passed
    tests/test_shape_doctor.py, 4 shards of 112 ids not re-run: only the new test file differs 112, 112, 112 passed; 109 passed, 3 skipped (lane 1) 112, 112, 112 passed; 109 passed, 3 skipped
    every other test file that names family.py, siblings.py or family-root (132 files, 8274 ids, 28 shards of 300) not re-run: only the new test file differs, and the new file's own rows are the first row above 8241 passed, 33 skipped (lane 1; every shard's count equals its count at the earlier head) 8241 passed, 33 skipped

Known limits

Not in this PR

Lane: openreposhape-1 (openRepoShape-1)

🤖 Generated with Claude Code

… and the family root's siblings.py submodule_urls, with its new test file

Checkpoint at the coordinator's word (usage window). Work in progress.

Done in this commit:
- scripts/family.py: `_gitmodules_on_disk` asks the work tree's
  `.gitmodules` with `_is_a` and `os.access`; `_gitmodules_names` and
  `_sections_giving` use it, so a file this user may not read is
  `gitmodules-unreadable` (`.gitmodules: cannot be read (<reason>)`, exit
  2), never "no section". `PinRollback` treats that code as it treats a
  store it cannot read (`READABLE_ONCE_CURED`): mount made again, update
  typed after the cure, never "only from its remote".
- templates/family-root/scripts/siblings.py: `submodule_urls` asks the
  same way and raises `gitmodules-unreadable` (`path_unreadable`); the
  placement and `--make` callers refuse with exit 2 before anything is
  cloned, fetched or run.
- tests/test_path_guard_hygiene.py: the three KNOWN_OPEN entries out.
- tests/test_family_gitmodules_unreadable.py: 33 rows. Head: 33 passed.
  Base 038dafb (3.12.3 and a 3.14-like shim): 25 failed, 8 passed (the
  8 are controls and the remove-order guard).

Not yet done: mutants, the gate, the pin measurement on the base-made
holder, pr-body.md. No test is known to fail at this commit.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR hardens all three targeted .gitmodules readers against stat or read permission failures, consistently reports gitmodules-unreadable, prevents unsafe sibling operations, and teaches removal rollback to recover from the existing local store after remediation, with broad regression coverage.

Sequence diagram for unreadable .gitmodules refusal in sibling operations

sequenceDiagram
    participant User
    participant Siblings as siblings.py
    participant Gitmodules as .gitmodules
    participant Refusal
    participant Members as Member operations

    User->>Siblings: dispatch_all() or _run_placement()
    Siblings->>Gitmodules: submodule_urls()
    Gitmodules-->>Siblings: stat/access failure
    Siblings->>Refusal: path_unreadable(gitmodules-unreadable)
    Refusal-->>Siblings: exit 2 with remediation
    Siblings-->>User: refuse operation
    Note over Members: No clone, fetch, or member command runs
Loading

File-Level Changes

Change Details Files
Replace .is_file() checks for the holder’s .gitmodules with explicit regular-file and readability validation that preserves “missing” semantics while refusing unreadable paths with a dedicated error code.
  • Add _gitmodules_on_disk using _is_a, os.access, and normalized refusal details/remediation.
  • Route _gitmodules_names and _sections_giving through the shared guard.
  • Introduce GITMODULES_UNREAD and include it in rollback recovery paths.
scripts/family.py
Prevent sibling placement and dispatch from proceeding when the holder’s .gitmodules cannot be read.
  • Add reusable path_unreadable refusal construction while retaining member_unreadable compatibility.
  • Validate .gitmodules before reading pinned submodule URLs.
  • Catch refusals in placement and dispatch entry points and return exit code 2 before cloning, fetching, or running members.
  • Document the new refusal behavior and remediation.
templates/family-root/scripts/siblings.py
Add comprehensive regression coverage for unreadable .gitmodules behavior across readers, removal rollback, sibling placement, and make dispatch.
  • Cover missing/readable controls, refused stat, I/O errors, refused os.access, locked symlink targets, and root/Windows handling.
  • Verify stable error codes, diagnostics, remediation, exit status, operation ordering, and offline rollback commands.
  • Remove the three resolved entries from path-guard hygiene tracking.
tests/test_family_gitmodules_unreadable.py
tests/test_path_guard_hygiene.py

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-1, 2026-10-22T20:24:56Z) Independent review r1164 of #1164 at 4352ac2f, partial, stopped at the shared window's LAST STEP: NOT READY. There is one MUST. After the cure, the commands the rollback prints for the PR's own hook case fail as typed: No url found for submodule path. It fails safely, reaching no network and keeping the object store. r1164 measured a one-line candidate fix, $S/candidate-fix.diff. There are also two SHOULDs and four NITs, and 20 of 22 mutants are killed. Lane 1 runs the fix round in a later window. The parts r1164 did not cover are listed at the end.

r1164: partial review of PR #1164 (head 4352ac2fd, base 038dafba9)

I stopped at LAST STEP (20:21Z). The mutant clone is back at 4352ac2fd with nothing changed (git status --short is empty), the review checkout is clean, and no run is still going. I did not write r1164/review-r1-partial.md: my operating rules forbid report files. This message is the whole partial review, so lane 1 can save it under that name.

Verdict: NOT READY. There is one MUST. The rest of the PR stands up to measurement.

What I confirmed

  • Base: git merge-base HEAD 038dafba9 gives 038dafba9, which is the stated base.
  • New test file, head: 33 passed on 3.12.3, and 33 passed on my 3.14-like sitecustomize shim. The shim is $S/shim314/sitecustomize.py; its log shows it loaded in 7 family.py and 6 siblings.py subprocesses.
  • New test file, base: 25 failed and 8 passed, on 3.12 and on the shim alike.
    • The 8 that pass are the controls and the order guard, as the body says.
    • The failures are about behaviour: DID NOT RAISE Refusal, the exit codes 1 and 0 where 2 is expected, and PermissionError tracebacks from if not path.is_file().
    • On the shim, the real siblings row at base ended in fatal: transport 'https' not allowed, so no row reached the network.
  • Body tables: every row I measured matches the body.
  • test_path_guard_hygiene.py: 49 passed at head and 49 at base. Base's list run against head's code fails on exactly the three removed entries, each 0 found, 1 listed. No KNOWN_OPEN entry is left for family.py or siblings.py.
  • Doc trio: 170 passed at head.
  • Imports: the diff adds no import line, so the test_update_shape_kept_helper.py claim holds as far as reading the diff shows.
  • Title and closing line: the title is 249 characters. The body says Part of #1110 and has no closing keyword.
  • Other test files: every fixture the new file uses gives each test its own copy (holder, estate, placed), so no test can change another's holder.

MUST-1: after the cure, the rollback's typed commands fail in the PR's own hook scenario

Where:

  • scripts/family.py:8288-8294: with the new storeless = exc.code not in READABLE_ONCE_CURED, the rollback types git submodule update here.
  • scripts/family.py:8233-8238: that update depends on _file_by_hand. When the current .gitmodules cannot be read, it types only the index entry. The work tree keeps the hook's copy, and git rm has already taken the member's section out of that copy.

Evidence, at head:

  • My probe $S/probe/tests/test_r1164_probe.py:
    • P1 is the PR's own hook: a link into a folder at mode 000.
    • P4 is a hook that runs only chmod 000 .gitmodules.
  • Each probe gives the mode back, unsets core.hooksPath, then types each command as printed.
  • P1, on 3.12 and on the shim:
    SECTION-IN-LINKED-FILE False
    TYPED 'git update-index --add --cacheinfo 100644,…,.gitmodules' -> exit 0
    TYPED 'git -c protocol.allow=never submodule update --init --no-fetch -- members/IRRS' -> exit 128: fatal: No url found for submodule path 'members/IRRS' in .gitmodules
    TYPED 'git -C members/IRRS checkout -q main --' -> exit 128: fatal: cannot change to 'members/IRRS': No such file or directory
    
  • P4 on the shim fails the same way and leaves STATUS M .gitmodules with member-dir False.
  • Base, P4 on 3.12: the sentence said "comes back only from its remote" and typed mkdir members/IRRS.

So base made a false claim, and head types a recovery that fails as printed. It fails safely: no network is reached and the object store stays. But the Why section's "Once cured, the checkout comes back from the store git rm keeps" is not true for the case that really happens.

Fix: I measured this candidate ($S/candidate-fix.diff).

         except OSError:
-            # Named by the step that met it (#430); the index entry is still
-            # this command's to type, and the bytes are `git status`'s.
-            return typed
+            same = False
  • Result: the commands now typed are git update-index …, then git checkout -- .gitmodules, then the update, then the branch.
    • Typed after the cure, they bring the member back on main at the recorded commit, and git status is clean, in P1 and P4.
    • The 33 rows still pass.
    • test_family_py_unreadable.py and test_family_bump_remove_rollback.py give 106 passed.
  • Tests to add:
    • In test_a_rollback_meeting_a_gitmodules_linked_into_a_locked_folder, give the mode back, unset core.hooksPath, then run typed_by_hand and assert_back_on_main.
    • A row for a hook that only runs chmod 000 .gitmodules.
  • Alternative: take the claim out of the body and name it under Known limits with a follow-up issue.

SHOULD-1: two mutants survive, because no row says a .gitmodules that is not a regular file counts as none

  • Where: scripts/family.py at _gitmodules_on_disk's _is_a(path, stat.S_ISREG), and templates/family-root/scripts/siblings.py:460.
  • Evidence: mutant M10 (S_ISREG changed to _any_mode) and mutant M21 (S_ISREG changed to _anything) each ended "33 passed".
  • Fix: add a control row with .gitmodules as a directory. It should give (None, []), [] and {} at base and head alike, as is_file() always answered.

SHOULD-2: remove names a regular .gitmodules at mode 000 one of two ways, depending on git's stat cache

NITs

Mutants: 20 of 22 killed

All 22 ran on 3.12 in $S/mut, each file put back in a finally. Rows failed per mutant:

  • M1: 5
  • M2: 5
  • M3: 15
  • M4: 6
  • M5: 1
  • M6: 3
  • M7: 3
  • M8: 12
  • M9: 2
  • M10: survived
  • M11: 8
  • M12: 5
  • M13: 3
  • M14: 1
  • M15: 10
  • M16: 1
  • M17: 5
  • M18: 10
  • M19: 4
  • M20: 2
  • M21: survived
  • M22: 1

Where my mutants are the same as the writer's, the counts match the body's table. The driver and its full list are in $S/mutants.py.

Parts I did not cover

  • The writer's wider gate: the 132-file shard run, the four test_shape_doctor.py shards and test_scaffold_leg_path_collisions.py.
  • The pin claims: a base-made holder passing its own validate-family.py, and update-shape.py check naming only siblings.py.
  • Mutants on the shim. They ran on 3.12 only; the in-process rows imitate 3.14.
  • P1 at base. Only P4 was measured at base.
  • The real, non-dry run of make park.
  • The skipped rows for Windows and for root.
  • P5's ls-files -m line was lost in my grep. P5's refusal shows git did not list the file.

Files

$S is /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1164.

  • $S/probe/tests/test_r1164_probe.py: probes P1 to P5.
  • Outputs:
    • $S/probe-head-312.txt
    • $S/probe-head-shim.txt
    • $S/probe-base-p4.txt
    • $S/probe-fix-p1.txt
    • $S/probe-fix-p4.txt
    • $S/probe-fix-blast.txt
    • $S/probe-head-p35.txt
    • $S/probe-base-p3.txt
  • Runs of the new file:
    • $S/head-312.txt
    • $S/head-shim.txt
    • $S/base-312.txt
    • $S/base-shim.txt
  • Mutants and the fix:
    • $S/mutants.py
    • $S/mutants-312.txt
    • $S/candidate-fix.diff
    • $S/family-candidate.py
  • $S/shim314/sitecustomize.py: the 3.14-like shim.
  • $S/penv: the probe environment wrapper.

The scratch clones $S/base, $S/mut and $S/probe hold untracked probe files only. My review checkout /workspace/projects/openRepoShape-worktrees/l1-r1164 is unchanged.

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 5 commits October 22, 2026 21:07
…it could not read before the update, so the commands typed after the cure bring the member back (r1164 MUST-1, #1110)

`_file_by_hand` returned only the index entry where the current bytes
could not be read. After a pre-commit hook made `.gitmodules` a link into
a folder at mode 000 (or the file at 000), the work tree kept the copy
`git rm` wrote, without the member's section, so the typed `git submodule
update` said "No url found". Unknown bytes are now not taken for the
recorded ones: `git checkout -- .gitmodules` is typed where the index held
them.

Tests: the link row now gives the mode back, unsets core.hooksPath, types
the commands and checks the member is back on main; a new row covers a
hook that only runs `chmod 000 .gitmodules`. Both fail at 4352ac2.

Part of #1110.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o readers and siblings.py's submodule_urls, even where its read is refused (r1164 SHOULD-1, #1110)

Control rows: a directory at `.gitmodules` gives `(None, [])`, `[]` and
`{}`, as `is_file()` always answered, and with `os.access` refusing its
read it still does. They pass at 038dafb and at head, and kill r1164's
mutants M10 and M21 (`S_ISREG` changed to any mode), which survived.

Part of #1110.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bject`, so `remove` names a .gitmodules at mode 000 `gitmodules-unreadable` whatever git's stat cache says (r1164 SHOULD-2, #1110)

Where `git ls-files -m` had to read the file, could not and listed it as
modified, `_stages_as` quoted the failed `git hash-object` ("a git or gh
command failed"); where git's cache spared the read, `_gitmodules_names`
gave `gitmodules-unreadable`. Both now give `gitmodules-unreadable`. A
link is still `gitmodules-unstaged`'s first. `_stages_as` takes the verb,
and the remedy says what `add` or `remove` reads the file for
(`GITMODULES_READ_FOR`).

Tests: both stat-cache states made on purpose (`core.trustctime` off, the
mtime set long ago and the index refreshed; stale: the mtime moved again),
each asserted through `git ls-files -m`, dry run and real; and an in-process
`add` row over an assume-unchanged copy it may not open.

Part of #1110.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d with its cure in the sentence, as a store it cannot read is (r1164 NIT-1, #1110)

`_undo_work_tree` appends to the refusal's detail, which the sentence
prints with no remediation: for a permission, "`chmod u+r` on it, and
`chmod u+rx` on every directory above it, lets git read it" (no "inside
the holder": a link can lead out of it); for another cause, OTHER_CURE.

Part of #1110.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…NREAD_CODE`, so it is not read for `GITMODULES_UNREADABLE`, the remediation `_submodule_paths` gives (r1164 NIT-4, #1110)

A rename only: the value, every refusal and every sentence are as they
were. The constant's comment says which of the two is which.

Part of #1110.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T03:07:15Z) Independent review r1164b of #1164 at 3162b523: NOT READY, for one SHOULD; no MUST. r1164's MUST and its other findings are fixed. The review stopped at LAST STEP, and the parts it did not cover are named at the end.

r1164b: independent review of PR #1164 at head 3162b523b (base 038dafba9), stopped at LAST STEP

Verdict: NOT READY, on one SHOULD. No MUST.

r1164's MUST and its other six findings are fixed, and I measured each one. Every claim in the body that I checked holds, apart from one gap: the rollback change also changes what bump and remove print for an unreadable family.yaml. The body does not say so and no row holds it (SHOULD-1). Once that is named and pinned, I see no reason to hold the PR on what I covered.

r1164's findings, re-checked at head

  • MUST-1 (typed commands failed after the cure): fixed. My probes P4 (the hook links .gitmodules into a folder at 000) and P5 (the hook runs chmod 000 .gitmodules) type git update-index …, git checkout -- .gitmodules, the update and git -C members/IRRS checkout -q main --. Each command exits 0, and the result is member-dir True at-recorded True branch 'main' status ''. That holds on 3.12 and on my shim.
  • SHOULD-1 (no directory row): fixed. The 3 directory rows pass on base and on head. My mutants A18 (S_ISREG changed to _any_mode) and B4 (S_ISREG changed to _anything) are now killed.
  • SHOULD-2 (remove named a file at 000 two ways): fixed. In P3 at head, both listed True (stale cache) and listed False (current cache) give REFUSED gitmodules-unreadable: .gitmodules: cannot be read (Permission denied), dry run and real, with the index bytes unchanged. At base the same two cases quote git hash-object --path=.gitmodules -- .gitmodules and git config -z --file .gitmodules --list.
  • NIT-1 (no cure in the rollback sentence): fixed. Row …cannot_open_gitmodules_makes_the_mount_again passes, and mutants A10 and A11 are killed.
  • NIT-2 (bootstrap.py, and the family root's validate-family.py, bootstrap.py and siblings.py: a root whose own .git this user cannot read is a PermissionError traceback on 3.12, or not-a-git-repo where exists() answers False, not refused by name #1148's head): the body now names 3a389565a. I did not re-run the merge-tree.
  • NIT-3: stays under Known limits, as before.
  • NIT-4 (constant name): fixed. The constant is now GITMODULES_UNREAD_CODE.

The body's claims I measured

  • New file at head: 40 passed on 3.12 and 40 on the shim. The shim log shows it loaded in exactly 12 family.py and 6 siblings.py subprocesses.
  • New file at base: 29 failed and 11 passed, the same 29 on 3.12 and on the shim. All 29 fail on behaviour: DID NOT RAISE Refusal, exit 0 or 1 where 2 is expected, and PermissionError or CommandFailed tracebacks. The 11 that pass are the 9 controls and the 2 order-guard rows.
  • At 4352ac2fd: 7 of 40 fail, exactly the seven the body names.
  • Hygiene: base's test_path_guard_hygiene.py run against head's code fails on exactly the three removed entries, each 0 found, 1 listed.
  • What table, every row:
    • P1: on base 3.12 each of the 6 functions raises PermissionError. On base under the shim they answer (None, []), [], None, 'no section of .gitmodules gives git members/IRRS', None and True. At head each gives gitmodules-unreadable.
    • P2: a link into a folder at 755 gives gitmodules-unstaged, exit 2, at base and at head.
    • P4: on base 3.12, exit 1 with a traceback and the member's directory gone. On base under the shim, "comes back only from its remote", and the typed commands are the index entry, then mkdir.
    • P6: on base 3.12 the siblings dry run is a traceback. On base under the shim it exits 0 and says would clone https://github.com/InkRouter/IRRS.git (family.yaml repository:); the real run gives CLONE FAILED with transport 'https' not allowed.
    • P8, no clones: on base under the shim, exit 1 with both members SKIPPED no clone, the same as the control. Head gives exit 2.
  • Rollback row the body marks "not measured": P5 on the base shim gives the same as 3.12. The body can fill that cell in.
  • Known limit about add (P11): both cache states give _submodule_paths' gitmodules-unreadable with "Mend the file by hand". This is confirmed.
  • Gate at head, re-run:
    • G1: 106 passed.
    • G2: 107 passed, 2 skipped.
    • G3: 170 passed.
    • G4: 124 passed.
    • G5: 169 passed.
    • My own extra siblings.py set (placement, inputs, bootstrap-unreadable, doctor-gitmodules): 171 passed.
  • Base: the merge-base is 038dafba9. main is one commit past it, touching only docs/release-notes/v1.2.0.md.
  • Title and closing form: the title is 249 characters. The body says "Part of family.py, the family root's siblings.py, shape-doctor.py and shape_advisory.py: a name in the root that is a link into a folder this user cannot search is still read as absent at twelve sites (follow-up of #430 slice Z) #1110", and GitHub lists no closing issue.

SHOULD-1: the rollback change also changes bump and remove for an unreadable family.yaml, unnamed and unpinned

  • Where: scripts/family.py:8256 (same = False in PinRollback._file_by_hand, line 8236). This method serves every recorded file: bump's (MANIFEST,) and remove's (GITMODULES, MANIFEST).
  • Evidence: my probes P9 (bump, with a hook that runs chmod 000 family.yaml; exit 1) and P10 (the same hook under remove). In each, the mode is given back and the typed commands are run.
    • Base: only git update-index --add --cacheinfo 100644,…,family.yaml is typed. After it runs, P9 reads family.yaml as before False; status ' M family.yaml\n' and P10 reads the same.
    • Head: git checkout -- family.yaml is typed as well, and after both run P9 reads family.yaml as before True; status ''.
    • The change is correct and an improvement. But the body's What and Why talk only of .gitmodules.
    • grep -n 'family.yaml\|MANIFEST\|bump' tests/test_family_gitmodules_unreadable.py matches only the two import lines (61 and 68), so no row holds the family.yaml case.
    • I wrote mutant A21 (base behaviour kept for family.yaml only) but did not run it before LAST STEP.
  • Fix:
    • Name the bump/remove family.yaml effect in What and Why.
    • Add a row: a hook runs chmod 000 family.yaml under bump; give the mode back, unset core.hooksPath, run typed_by_hand, and assert family.yaml holds its recorded bytes and git status --porcelain is empty.
    • Run A21 against the new file and G1 to show the row kills it.

NITs

  • NIT-1: the body's table says "not measured" for P5 on the base shim. It is measured now: exit 2, "comes back only from its remote", typed index entry then mkdir members/IRRS, the same as 3.12.
  • NIT-2: the Why bullet says the cure "follows the cause wherever the file is read". That overstates it for the holder's validator.
    • Where: templates/family-root/scripts/validate-family.py:1895-1905, _gitmodules_listed.
    • Evidence, from reading the code (not run): a permission failure there is still gitmodules-unreadable with "Mend the file by hand", as P11 measured for add.
    • Fix: name validate-family.py beside add in Known limits.

Mutants: 19 distinct, all killed (23 runs, 0 survivors)

Each mutant ran in a scratch clone, with the file restored in a finally and its blob checked equal to HEAD's after each one. Both clones' git status --short is empty.

Mutant What it changes 3.12 rows failed Shim rows failed
A1 _gitmodules_on_disk: _is_a back to is_file() 5 10
A2 _gitmodules_on_disk: no os.access 9 –
A3 _gitmodules_on_disk: except PermissionError only 2 2
A4 where="" dropped 9 –
A5 remedy always remove's 1 –
A6 _stages_as does not ask _gitmodules_on_disk 2 –
A7 _stages_as does not pass the verb on 1 –
A8 _gitmodules_names back to is_file() 9 9
A9 _sections_giving back to is_file() 6 6
A10 no cure appended in _undo_work_tree 1 –
A11 cure asks exc, not its cause 1 –
A12 mount made again for a store only 1 –
A18 S_ISREG changed to _any_mode – 2
A20 _gitmodules_on_disk asked before lstat (the order guard) – 2
A22 OSError taken as absent – 19
B1 submodule_urls: _is_a back to is_file() – 9
B3 submodule_urls: except PermissionError only – 1
B4 S_ISREG changed to _anything – 1
B13 OSError taken as no urls – 10

A dash means that mutant did not run on that Python before LAST STEP.

Not covered (LAST STEP)

State

The review checkout /workspace/projects/openRepoShape-worktrees/l1-r1164 is detached at 3162b523b with an empty git status --short. No run of mine is live. The only mutants.py process running belongs to r1189.

Everything is in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1164b:

  • probe file: test_r1164b_probe.py
  • shim: shim314/sitecustomize.py
  • probe environment: penv
  • mutant driver: mutants.py
  • logs: logs/ (probe-*.txt, mut-p1.txt, mut-s1.txt, g1.txt to g6.txt, base-*.rows, head-*.rows, prev-312.txt)

The scratch clones base, probe and prev hold untracked probe files only.

Lane: openreposhape-1 (openRepoShape-1)

…dex's copy after the entry, under `bump` and under `remove`, so `_file_by_hand`'s change holds for the manifest too (r1164b SHOULD-1, #1110)

`PinRollback._file_by_hand` serves every recorded file, so taking bytes it
cannot read for not the recorded ones also changed what `bump` and `remove`
type for an unreadable `family.yaml`: `git checkout -- family.yaml` follows
the index entry. No row held it. A `pre-commit` hook runs `chmod 000
family.yaml` and refuses; with the mode given back and the typed commands
run, `family.yaml` holds its recorded bytes and `git status --porcelain` is
empty.

Base 038dafb: both rows fail at the bytes assertion (31 failed, 11
passed in the file) on Python 3.12.3 and on the 3.14-like shim. Head: 42
passed on both.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T12:09:42Z) Independent re-check r1164c of #1164 at 74096eb4: provisionally READY, with two NITs. It stopped at lane 1's STOP. 12 of r1164b's 16 mutants, and the new file on the merged trees, were not run; they are named at the end.

Lane 1 applies both NITs to the body.

r1164c: independent review of PR #1164 at 74096eb47 (base 038dafba9). Stopped on lane 1's STOP; partial.

Verdict: READY on what I covered (provisional). No MUST, no SHOULD; two NITs.

r1164b's SHOULD and both NITs are fixed, and I measured each. The verdict is provisional because 12 of the 16 mutants were not run, and the new file was not run on either merged tree.

Part 1: the fix round

  • The SHOULD is fixed. The new row test_a_rollback_meeting_a_family_yaml_at_000_types_the_index_copy[bump|remove] is the only change: git diff --stat 3162b523 74096eb4 names the test file alone (48+/1-). What and Why now name the family.yaml effect.
  • New file at head: 42 passed on 3.12 and 42 passed on my shim. The shim log shows it loaded in 14 family.py and 6 siblings.py subprocesses, as the body says.
  • New file at base, head's file copied in: 31 failed and 11 passed, with the same rows on 3.12 and on the shim.
    • comm against r1164b's base-312.rows shows the 31 are r1164b's 29 plus the two new rows.
    • Both new rows fail at assert manifest.read_bytes() == recorded on both Pythons.
    • On the base shim, transport 'https' not allowed appears 3 times, so no row reached the network.
  • NIT-1 is fixed: the P5 base-shim cell is filled in.
  • NIT-2 is fixed: Known limits now names validate-family.py's _gitmodules_listed beside add, and the Why bullet is scoped. I measured it on a holder made at head:
    • .gitmodules at mode 000: rc=2, REFUSED gitmodules-unreadable … Mend the file by hand.
    • A link into a folder at 000: rc=2, the same.

Part 2

  1. Mutants: 4 distinct run, 7 runs, all killed, 0 survivors. Each file was restored and its blob checked equal to HEAD's.
    • A13, A14, A15 and A16 on 3.12: 4 rows failed each.
    • A13, A14 and A15 on the shim: 4 rows failed each.
    • Not run (stopped): A17, A19, A21, A23, B2 and B6 to B12, and the 3.12 runs of B1, B3, B4 and B13.
  2. The overlaps:
  3. The pin claims both hold.
    • A holder made by base's family.py init passes its own validate-family.py: rc=0, 10 copied shape file(s) match their digests.
    • Head's update-shape.py check --upstream <head clone> --at 74096eb4 gives upstream-changed scripts/siblings.py and unchanged for the other 9 files (upstream-changed 1 unchanged 9).
    • Extra check: after apply --yes --branch, the holder's validate-family.py gives rc=0, and its siblings.py is byte-equal to head's.
    • By AST, every name head's siblings.py imports is defined in the base holder's copies (none missing): 10 from repo_shape, 6 from bootstrap. The import set is unchanged from base to head.

NITs

Not covered (STOP)

  • The 12 unrun mutants, and B1, B3, B4 and B13 on 3.12.
  • The new file on the m1147 and m1172 merged trees.
  • G2 and the doc trio re-run at head (the new row runs git config --local --unset).

State

  • My runs are stopped. The drivers ended rc=143, and ps shows nothing of r1164c's.
  • My TERM command also hit its own shell (exit 144); no harm done.
  • The scratch clones mut and mut2 have an empty git status.
  • The review checkout /workspace/projects/openRepoShape-worktrees/l1-r1164 is at 74096eb47 with no changes.
  • Scratch folder: /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1164c.
    • Logs: logs/.
    • Driver: tools/mutants.py, r1164b's with only its scratch path changed.
    • Resolution script: tools/resolve1172.py.
    • Shim: shim314/sitecustomize.py.
    • Merge clones: m1147 (scratch merge dad0a11b) and m1172 (scratch merge 4dad5e36c). Neither was pushed.

Lane: openreposhape-1 (openRepoShape-1)

@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-23T17:59:46Z) #1164 is READY at 74096eb4: r1164c's unrun parts are now run, with its own drivers, and every one passed.

r1164c (issuecomment-6364558859) was a provisional READY. It stopped at lane 1's STOP with 12 of r1164b's 16 mutants unrun, the 3.12 runs of B1, B3, B4 and B13 unrun, and the new file run on neither merged tree. Lane 1 ran all of that from the main loop, with r1164c's tools/mutants.py and tools/run.sh, in its scratch clones at 74096eb4, under its probe environment:

r1164c found no MUST and no SHOULD, and both of its NITs are already applied to the body. So #1164 counts READY at 74096eb4. Its slot in the v1.2.1 order stays where lane 3 put it: after #1144, #1172 and the family.py PRs.

Lane: openreposhape-1 (openRepoShape-1)

brettheap added a commit that referenced this pull request Nov 8, 2026
…py: an `id:`, a date or a `repository:` ending in a line feed is refused by name, and a pin's `source_repository:` that is no path or `owner/repo` name is named as the pin's (#1139) (#1166)

The value patterns the issue names refuse a value that ends in a line feed, by name, as #1126 (#1017) makes `COMMIT_RE`, `SHA256_RE` and `COMMIT_40` do:
- `DATE_RE` and `QUALIFIED_RE` in `validate-manifest.py` and `validate-family.py`, `MEMBER_RE` in `scripts/family.py`, `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`;
- `PROJECT_ID_RE`, read with `fullmatch` where the two validators and `family.py add` read an id from a file.

A shape pin's `source_repository:` that is neither a path on this machine nor an `owner/repo` name is refused in the pin's name, not `--upstream`'s. Values without a line feed keep their outcome, and no refusal code is added or renamed. Two inputs do move from one existing code to another (see "Behaviour changed", below).

**What this PR does not cover, and the title does not claim:**
- the naming-policy patterns that `scripts/repo_shape.py` reads from `contracts/repository-naming.yaml`, which `validate-repository-naming.py` still reads with `.match`;
- `update-upstream-not-a-repo`, which still says `--upstream` when a pin names a folder that exists and is not a repository.

Both are follow-ups, with the other findings of the independent review, under "Follow-ups (not in this PR)".

This does not need #1126 and does not touch its files or its test file.

Measured on main `038dafba` (Python 3.12.3, git 2.43.0) by a probe of each tool, before any change. Each value is written in the file as a YAML double-quoted `"...\n"`, which the reader takes as the value and a line feed.

| Read from | Value | On main |
|---|---|---|
| `project.yaml` (`validate-manifest.py`) | `id: "atlas\n"` | only `FINDING manifest-topic: topic is 'xf-project-atlas', expected 'xf-project-atlas\n' derived from id 'atlas\n'`, exit 1 |
| `project.yaml` | `elected_on: "2026-09-02\n"` | `manifest ok`, exit 0 |
| `project.yaml` | a leg's `repository: "testorg/Atlas-spec\n"` | `manifest ok`, exit 0 |
| `family.yaml` (`validate-family.py`) | `id: "inkrouter\n"` | `family ok: InkRouter (inkrouter` and a line break, exit 0 |
| `family.yaml` | `created_on: "2026-09-04\n"` | `family ok`, exit 0 |
| `family.yaml` | a member's `repository: "InkRouter/IRRS\n"` | `FINDING member-repository-name: ... whose name is 'IRRS\n'`, exit 1 |
| a member's `project.yaml`, read by `family.py add` | `id: "irqs\n"` | mounted and committed, exit 0, with the row `id: "irqs\n"`; the holder's gate then said `family ok` |
| `contracts/shape-pin.yaml`, `update-shape.py check` with no `--upstream` | `source_repository: "opensoft/openRepoShape\n"` | `REFUSED unsafe-value: --upstream is 'opensoft/openRepoShape\n'`, exit 2 |
| the same | `source_repository: "opensoft/open Repo"` | `REFUSED update-upstream-unresolvable: --upstream 'opensoft/open Repo' is neither ...`, exit 2 |
| the same | `source_repository: "-opensoft/openRepoShape"` | `REFUSED unsafe-value: --upstream is '-opensoft/openRepoShape'`, exit 2 |
| an adoption plan (`adopt-project.py check`) | `id: "northwind\n"` | `FINDING plan-manifest-invalid: id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-topic: ...`, exit 1; `execute` refused it, exit 2, before any leg |

In-process on the same main: `repo_shape.PROJECT_ID_RE.match('atlas\n')`, `shape_advisory.DATE_RE.match('2026-09-02\n')` and `REPOSITORY_RE.match('a/b\n')` each matched, and `date.fromisoformat('2026-09-02\n')` raised `ValueError`.

- **`DATE_RE` and `QUALIFIED_RE`** in `templates/assembly-root/scripts/validate-manifest.py` and `templates/family-root/scripts/validate-family.py` end in `\Z`. So do `MEMBER_RE` in `scripts/family.py` (the third spelling `tests/test_family_member_name.py` holds equal to the two `QUALIFIED_RE`), `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`.
- **`PROJECT_ID_RE` keeps its `$`.** Every id refusal prints its text (`must match ^[a-z0-9][a-z0-9-]*$`, pinned in six test files), the validators run against each project's own copy of `repo_shape.py`, and its definition sits inside #1126's hunk. The three callers that read an id from a file ask it with `fullmatch` instead: `validate-manifest.py` (`manifest-id`), `validate-family.py` (`family-id`) and `family.py`'s reading of a member's `project.yaml` (`member-bad-id`, which `add` now says before anything is mounted).
- **`update-shape.py`.** `open_upstream(spec, pin_path=None)` takes the pin the value was read from, which `prepare` passes when no `--upstream` was given. Then `update-upstream-unresolvable` says ``<root>/contracts/shape-pin.yaml: `source_repository:` is '...', which is neither a path on this machine nor an `owner/repo` name``, with a remedy to write the pin's field or pass `--upstream`, and `checked_value` names the same field. **Code kept:** `update-upstream-unresolvable`, the existing code for a value that is not an `owner/repo` name. A line feed now reaches it, since the pattern no longer takes one. Before, `checked_value` refused it as `unsafe-value`.
- **One existing test line:** `tests/test_family_init_values.py` finds validate-family's `DATE_RE` line by its text, which now ends `\Z`.
- **One existing stand-in:** the `temporaries` fixture in `tests/test_update_shape_unreadable.py` stands in for `open_upstream` with `def opening(spec)`, which the new two-argument call refused with `TypeError: ... takes 1 positional argument but 2 were given`. It now takes `pin_path=None` and hands it on to the real `open_upstream`, so `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]` asks what it asked before.
- **The rows** are in a new file of their own, `tests/test_value_pattern_line_break.py`.
- **A second guard, in a second commit (`5102b4294`, a test only, no production file).** The first guard holds the four pattern names this PR ends in `\Z`, and nothing held `PROJECT_ID_RE`, which keeps its `$`. `test_every_reader_of_the_id_pattern_is_in_the_table` reads every tracked `.py` outside `tests/` with `ast` (it runs none) and holds each call of a method of `PROJECT_ID_RE` (`PROJECT_ID_RE.x(...)` or `repo_shape.PROJECT_ID_RE.x(...)`) to the table `ID_READERS`:
  - the three `fullmatch` readers of an id from a file (`validate-manifest.py`, `validate-family.py`, `scripts/family.py`);
  - the modules that still `match` it, each with its reason: argv `--id` in `scaffold-project.py` and `scripts/family.py`, where `checked_value` refuses a line feed after the pattern; argv `--id` in `adopt-project.py`, which the project's own `validate-manifest.py` refuses (`adopt-manifest-invalid`, below; `ID_READERS` still gives it the `checked_value` reason); and `adopt-project.py`'s two `plan-bad-id` readers, of the plan's `id:` and of the id derived from `legs.assembly`, which a line feed never passes (below).

  A new reader of `PROJECT_ID_RE`, `fullmatch` included, fails it by file and method until it is listed in `ID_READERS` with its reason. The table is exact both ways, so a reader taken away fails it too.

**Left as they are, and why:**
- the argv readers of `PROJECT_ID_RE` (`--id` in `scaffold-project.py`, `family.py init` and `adopt-project.py`). The first two hold the id to `checked_value` after the pattern, which refuses a line feed as `unsafe-value` naming `--id`, and `tests/test_derived_id_refusals.py` pins that answer for them. `adopt-project.py` never calls `checked_value` on `--id`: `plan` with `--id` set to `northwind` and a line feed exits 2 with `adopt-manifest-invalid`, in the words of the project's own `validate-manifest.py` (`manifest-id`), and writes no plan (lane 5's re-check, issuecomment-6371408915, NIT 2; measured on `plan` only);
- `adopt-project.py`'s reading of a plan's `id:`: `check` and `execute` hold it to the project's own `validate-manifest.py`, which refused it on main (above) and now names it in `manifest-id`'s words. It is the same value, refused under the same code, `plan-manifest-invalid`, before any leg exists. The id derived from `legs.assembly` is read with `.match` too, and a line feed in `legs.assembly` is `unsafe-value` before that id is asked (the review measured both, on main and here alike). This keeps the open PRs on `adopt-project.py` (24 at 12:02Z) clear;
- `PIN_ARG_RE`, and `NAME_RE`/`REF_RE`/`PATH_RE` in `setup-project.py`, as the issue says;
- `update-upstream-not-a-repo` still says `--upstream`. A pin's value reaches it only when it names a path that exists, not through the pattern.

**Behaviour changed, stated plainly, and what the issue's Verify means by "No refusal code changes":** the issue's Verify line reads as no code added or renamed, and none is. Two inputs do move from one existing code to another, exit 2 both before and after:
- `update-shape.py --upstream "opensoft/openRepoShape<LF>"` was `unsafe-value` and is now `update-upstream-unresolvable`, still naming `--upstream`;
- a pin's `source_repository:` that ends in a line feed, with no `--upstream`, was `unsafe-value` naming `--upstream` and is now `update-upstream-unresolvable` naming the pin's field.

No test pinned either old answer: every gate below passed. `family.py add --member` is stripped before `MEMBER_RE`, so `add` answers as before.

**Not changed, seen while measuring:** with `--upstream` passed, `update-shape.py` does not read the pin's `source_repository:` through `REPOSITORY_RE` at all, and it keeps the value as the upstream's repository name. `validate-family.py` does not hold a member row's `id:` to `PROJECT_ID_RE`; it compares the row with the member's manifest. Neither is a `$` question. Both are under "Follow-ups (not in this PR)".

All in `tests/test_value_pattern_line_break.py`, run against main's production code: the five production files swapped in place from `origin/main` (`961640c68`), then put back with their sha256 matching (`dirty=5` in the run's START line is those five files). On the head `5102b4294` (30 rows) the result is **20 failed, 10 passed in 23.12s, git 2.43.0**. The 10 that pass are baselines: the seven `test_a_pattern_takes_its_value` rows, the spelling-table guard and the two double-quoted-without-a-line-feed rows.

The first 19 are the first commit's rows (19 failed, 10 passed in 22.12s on its 29 rows at `038dafba`). The independent review reproduced them from main's bytes of the same five files (19 failed, 10 passed in 26.78s, the same 19 ids). The 20th, `test_every_reader_of_the_id_pattern_is_in_the_table`, is the second commit's: it fails on main because the three readers of an id from a file are `.match` there, not `.fullmatch`.

- `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-QUALIFIED_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[family-DATE_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[family-QUALIFIED_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[family.py-MEMBER_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[shape_advisory-DATE_RE]`
- `test_a_pattern_refuses_its_value_and_a_line_feed[update-shape-REPOSITORY_RE]`
- `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]`
- `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]`
- `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[leg-repository]`
- `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id`
- `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]`
- `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]`
- `test_a_family_value_that_ends_in_a_line_break_is_a_finding[member-repository]`
- `test_add_refuses_a_member_whose_id_ends_in_a_line_break`
- `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]`
- `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[space]`
- `test_a_pins_source_checked_value_refuses_is_named_as_the_pins`
- `test_an_upstream_passed_with_a_line_feed_is_named_as_passed`
- `test_every_reader_of_the_id_pattern_is_in_the_table`

**13 of 13 killed.** Each mutant was applied alone to one file of the branch's production bytes (the same bytes as the first commit and as the head, which adds a test only), the rows were run, and the file was put back with its sha256 checked (`restored=yes` for all 13). Each run is `tests/test_value_pattern_line_break.py` plus `tests/test_family_member_name.py::test_every_spelling_of_the_member_name_rule_is_the_same_pattern`, git 2.43.0.

| Mutant | File | Killed by |
|---|---|---|
| M1 `DATE_RE` back to `$` | `validate-manifest.py` | 2: `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]`, `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]` |
| M2 `QUALIFIED_RE` back to `$` | `validate-manifest.py` | 3: `...[manifest-QUALIFIED_RE]`, `...[leg-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` |
| M3 the id read with `.match` | `validate-manifest.py` | 2: `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]`, `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id` |
| M4 `DATE_RE` back to `$` | `validate-family.py` | 2: `...[family-DATE_RE]`, `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` |
| M5 `QUALIFIED_RE` back to `$` | `validate-family.py` | 3: `...[family-QUALIFIED_RE]`, `...[member-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` |
| M6 the id read with `.match` | `validate-family.py` | 1: `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` |
| M7 `MEMBER_RE` back to `$` | `scripts/family.py` | 2: `...[family.py-MEMBER_RE]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` |
| M8 a member's id read with `.match` | `scripts/family.py` | 1: `test_add_refuses_a_member_whose_id_ends_in_a_line_break` |
| M9 `DATE_RE` back to `$` | `scripts/shape_advisory.py` | 1: `...[shape_advisory-DATE_RE]` (its pattern row alone: `date.fromisoformat` refuses the line feed after the pattern, so no tool row sees it) |
| M10 `REPOSITORY_RE` back to `$` | `update-shape.py` | 3: `...[update-shape-REPOSITORY_RE]`, `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]`, `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` |
| M11 `prepare` passes no pin | `update-shape.py` | 3: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]`, `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` |
| M12 `checked_value` names `--upstream` again | `update-shape.py` | 1: `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` |
| M13 the pin's refusal sentence removed | `update-shape.py` | 2: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]` |

No mutant is added for the stand-in in `tests/test_update_shape_unreadable.py`: it is a test change.

The independent review re-ran M3, M8, M11 and M12, and one mutant of its own (X1: `prepare` names the pin even when `--upstream` is passed). All five were killed, and each file was put back with its sha256 checked. The other nine of the 13 are this writer's runs alone.

**The second commit's guard: 6 of 6 probes caught.** Each was applied alone to one tracked file at `5102b4294`, `test_every_reader_of_the_id_pattern_is_in_the_table` was run, and the file was put back with its sha256 checked (`restored=yes` for all six). The first commit's guard, `test_every_spelling_of_a_value_pattern_is_in_the_table`, was run with each plant too: it passes every one of them, which is the gap the second guard closes.

| Probe | File | The new guard | The first guard |
|---|---|---|---|
| P1 a new `PROJECT_ID_RE.match(value)` reader appended | `update-shape.py` | failed: `('update-shape.py', 'match'): 1` is more than the table holds | passed |
| P2 a new `repo_shape.PROJECT_ID_RE.search(value)` reader appended | `scripts/shape_advisory.py` | failed | passed |
| P3 `fullmatch` back to `match` | `templates/assembly-root/scripts/validate-manifest.py` | failed | passed |
| P4 `fullmatch` back to `match` | `templates/family-root/scripts/validate-family.py` | failed | passed |
| P5 `fullmatch` back to `match` | `scripts/family.py` | failed | passed |
| P6 main's bytes of those three files together (the 20th row above) | the three | failed | not run |

**The second commit `5102b4294` (dirty=0)**, Python 3.12.3, with the same stub `gh`, bogus token and no global git config:

| Run | Git | Result |
|---|---|---|
| `tests/test_value_pattern_line_break.py` | 2.43.0 | **30 passed** in 23.00s |
| `tests/test_value_pattern_line_break.py` | 2.47.1 | **30 passed** in 23.30s |
| `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 61.54s |

That commit changes only the test file (62 lines added, no assert that joins conditions with `and` or `or`, ASCII), so the first commit's runs below are not repeated: they hold the production files and every other test file, and those are unchanged. CI on `5102b4294`, and an independent look at the second commit, were not done when this was written.

**The independent review of the first commit** (rv1166c, READY, read-only) reports these gates on `2593952c9`, all passing: the stand-in gate's 6 files (298 passed), the new file on 2.47.1 (29 passed), the five hygiene files (306 passed), 15 more test files that name a changed function, pattern or code (1611 passed, 19 skipped) and `tests/test_shape_doctor.py` (445 passed, 3 skipped). It also read CI on that head at about 20:04Z: `tests` (the full suite on 3.14), `tests-git-bash`, `package` and SonarCloud all passed.

**The first commit's gates ran on `2593952c` (dirty=0)**, Python 3.12.3, with the real `gh` unreachable (a stub first on `PATH`, a bogus token) and no global git config:

| Run | Git | Result |
|---|---|---|
| the files that held every failure and setup error of the shards below: `tests/test_family.py`, `tests/test_update_shape_pr_base.py`, `tests/test_update_shape_unreadable.py`, `tests/test_update_shape_header_comments.py`, `tests/test_update_shape_messages.py` and the new `tests/test_value_pattern_line_break.py` | 2.43.0 | **298 passed** in 343.60s |
| `tests/test_value_pattern_line_break.py` | 2.47.1 | **29 passed** in 21.31s |
| `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 69.12s |

**The two shards, on the uncommitted tree** holding the same production bytes as the first commit (the 7 files' sha256 checked before and after), git 2.43.0: the validators' test files, the `REPOSITORY_RE`/`source_repository` files and the `update-shape.py`, `shape_advisory.py` and `family.py` files, 125 in all.

- shard 1 (62 files): 4 failed, 5290 passed, 13 skipped in 56 min;
- shard 2 (63 files): 3 failed, 4 setup errors, 3761 passed, 29 skipped in 50 min.

Every failure and setup error is in the five existing files of the first row of the table above, which all pass on the first commit:
- 2 were real: `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]`, the `opening(spec)` stand-in under the two-argument call. The stand-in is fixed in the first commit.
- The other 5 failures and the 4 setup errors were `update-conflict` on `scripts/validate-family.py` ("not a verbatim copy ... at the pinned commit"). This is an artifact of an uncommitted tree: `tests/test_family.py`'s `upstream_clone` commits the working tree into its clone, while `family.py init` pins the holder at the real `HEAD`, so an uncommitted `templates/family-root/` file is never verbatim at its pin. On the first commit they pass.

Every other file of the two shards passed on these production bytes, so the shards were not re-run.

`git merge-tree --write-tree <other> 5102b42` (git 2.43.0) at 2026-10-23T12:08Z, after re-fetching main (still `961640c6`; read again at 12:10Z, just before the push) and re-listing the open PRs at 12:02Z (55 open). Main has moved from the `038dafba` that this body measured to `961640c6` by one file, `docs/release-notes/v1.2.0.md`. Each open PR that touches one of this PR's 8 paths, at its current head, plus #1126:

| Against | Head | Shares | Result |
|---|---|---|---|
| origin/main | `961640c6` | | clean, tree `4613729516ff` |
| #1098 | `dad7961a` | `scripts/family.py` | clean, tree `4d1ff9517588` |
| #1117 | `10dc011c` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only, which this PR does not touch (see below) |
| #1124 | `8dfa32bd` | `validate-family.py` | clean, tree `cb2ec1073646` |
| #1126 | `4c1f80cf` | none (its `scripts/repo_shape.py` and tests) | clean, tree `e82c8945ac37` |
| #1133 | `a7bfee1b` | `update-shape.py` | clean, tree `a710870d1ffc` |
| #1144 | `89486f69` | `scripts/shape_advisory.py` | clean, tree `2d3e3e1ef43d` |
| #1147 | `b75babee` | `validate-manifest.py` | clean, tree `a82ac3b47a3f` |
| #1159 | `66e68f94` | `scripts/shape_advisory.py` | clean, tree `4abeb7b38665` |
| #1164 | `74096eb4` | `scripts/family.py` | clean, tree `ee285e1cb8cf` |
| #1172 | `5aa0f116` | `validate-family.py` | clean, tree `aa32d41e6e2c` |
| #1187 | `cc8a87ee` | `update-shape.py`, `tests/test_update_shape_unreadable.py` | clean, tree `25711eb1cac4` |
| #1201 | `eac177af` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only (see below) |

- #1117 and #1201 conflict in `docs/handbook.html` alone. #1201 is stacked on #1117 (their merge-base is #1117's head `10dc011c`). Each of them against origin/main alone gives the same conflict in the same file, so it is theirs: #1117's, from its older base `6da757cb`, which #1201 inherits.
- #1187 and #1201 are new since the first merge-trees. #1187 shares `tests/test_update_shape_unreadable.py` with this PR; its diff adds or removes no line that names `open_upstream`, `def opening` or `REPOSITORY_RE`, so it brings no one-argument stand-in for the two-argument call. That was read for all 55 open heads at 12:12Z; only this PR's own lines name them.
- No other open PR adds or removes a call of a method of `PROJECT_ID_RE`, which the second guard counts: the lines each of the 55 open heads adds or removes outside `tests/`, `docs/` and `*.md` were read at 12:04Z. Only this PR's own name it, and #1201's one new `MEMBER_RE.fullmatch` is another pattern. So a sibling landing first does not move the guard's table.

Each is a candidate for its own issue. They come from the independent review (rv1166c, READY at `2593952c9`, issuecomment-6352173084), which measured each of them on main and at the head unless the item says it did not. The review's finding numbers are in brackets.

- **The naming-policy patterns [1].** `scripts/repo_shape.py` reads the families, the roles and the topic pattern of `contracts/repository-naming.yaml` with `.match` (`family["_re"].match(name)`, `family["_split_re"].match(name)`, `role["_re"].match(name)`, and `topic_pattern`, which defaults to `^xf-project-[a-z0-9-]+$`). `validate-repository-naming.py --project project.yaml` runs in the root template's `Makefile` and `validate.yml`. At the head, and on main, it exits 0 on a leg `repository: "testorg/Atlas\n"` and prints the line feed inside its table; `-spec` and `-code` the same. The project's gate as a whole still fails, because `validate-manifest.py` now refuses that manifest.
- **`update-upstream-not-a-repo` with a pin [3].** With no `--upstream`, a pin whose `source_repository:` names a folder that exists and is no repository (the review measured `contracts` and `scripts/repo_shape.py`) is `REFUSED update-upstream-not-a-repo: --upstream contracts exists but is not a git repository`, exit 2, on main and here. `pin_path` is in scope at `update-shape.py:686-687`, so that refusal can name the pin as `update-upstream-unresolvable` now does.
- **A pin's line feed when `--upstream` is passed [9].** `update-shape.py check --upstream <clone>` over a pin whose `source_repository:` ends in a line feed exits 0, `nothing to do`. The value is kept as `upstream.repository`, which reaches the README shape line's check, `apply`'s commit subject and the pull request words (`update-shape.py:2525`, `3017` and `5121`). The review measured `check`, not `apply`. It belongs with the previous item.
- **A member row's `id:` [4].** On main, `add` of a member whose `project.yaml` reads `id: "irqs\n"` commits the row `id: "irqs\n"`, and the holder's `validate-family.py` then says `family ok`. Under mutant M8 (this branch with main's `.match` in `add`) the same row is written, and the gate still says `family ok`. By code reading, not measured end to end: `validate-family.py` compares a row's id with the member manifest's by `str` equality (`member-id-mismatch`, `validate-family.py:1608`), and `_check_member_ids_unique` does the same, so neither asks `PROJECT_ID_RE` of a row. This PR makes `add` refuse such a member, so no new holder gets such a row.
- **Values that no pattern reads [8].** The review's sweep found these `manifest ok` or `family ok`, exit 0, with a trailing line feed, at the head and on main alike:
  - in `project.yaml`: `name`, a spec or code leg's `path`, `shape.repository` and `shape.digests.digest_algorithm`;
  - in `family.yaml`: `org`, the family's own `repository`, `tracking_branch`, and the shape's and each member pin's `digest_algorithm`.

  No `^...$` pattern reads them, so they are not this PR's. A leg's `path` is checked only by `_leg_path_findings`, which asks no pattern, and the family's `tracking_branch` is refused in `project.yaml` (`manifest-tracking-branch`, U+000A) but not in `family.yaml`. `shape.commit` and `tree_sha256` with a line feed are also `manifest ok`; that is #1126's.

The first commit's message says "the three readers of an id from a file ask it with `fullmatch` (`manifest-id`, `family-id`, `member-bad-id`)". That is true of those three, but it reads as if there were no others [2]. What the review measured instead:
- `adopt-project.py` still reads the id derived from `legs.assembly` (`:6780`) and the plan's `id:` (`:6791`), both `plan-bad-id`, with `.match`;
- neither lets a line feed through. A plan `id: "northwind\n"` is `plan-manifest-invalid`, now in `manifest-id`'s words. A plan with no `id:` and `legs.assembly: "NorthWind\n"` is `REFUSED unsafe-value: legs.assembly is 'NorthWind\n'`, exit 2, before the derived id is asked, on main and here alike.

So it is the wording alone that overstates. The second commit's `ID_READERS` lists both readers with that reason.

The title's pin clause is the narrower one (the pin's name only where the value is no path and no `owner/repo` name), and two inputs change from `unsafe-value` to `update-upstream-unresolvable` (see "Behaviour changed", above).

Review: rv1166 and rv1166c, lane 5's independent opus reviewers, at `2593952c` (issuecomment-6352173084); then rc1166b, lane 5's independent sonnet re-checker, which wrote none of #1166: READY at `5102b429`, H0 of this landing (issuecomment-6371408915), with no MUST or SHOULD; its NITs 1 and 2 are corrected in the body. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 5102b42 passed tests, tests-git-bash and SonarCloud; the head 5c8afef is a clean update-branch merge of main 93d2931, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 3c89065 over main 11560eb: the family_init_values, update_shape_unreadable, value_pattern_line_break, repo_hygiene, agents_md_codes, cli_reference sets, 365 passed, 19 skipped. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1139

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant