Skip to content

shape-doctor.py: an own-copy validator row whose crash is a root's lost scripts/repo_shape.py names git -C <root> checkout HEAD -- scripts/repo_shape.py as its NEXT (none where HEAD no longer holds it) and the validator in its WHY (#697) - #737

Merged
brettheap merged 1 commit into
mainfrom
l3-697-crash-cure-next
Oct 16, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

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

Closes #697.

What changed and why. Option C, chosen by Brett Heap at 2026-10-16T13:11:50Z ("C, and no objection to #678"). A root that lost scripts/repo_shape.py reads DRIFTED (1 copy-missing), and its own-copy manifest, pins and family rows each say the gate crashed on ModuleNotFoundError: No module named 'repo_shape'. The NEXT of those rows was the gate. Since #636/#342 the doctor's child has no inherited PYTHONPATH and no site; a pasted line has neither guard, so a reader whose own PYTHONPATH (or a .pth in whose site-packages) reaches a repo_shape ran the NEXT and saw manifest ok: Atlas (atlas), 3 legs, exit 0, under a FINDING row (measured on de92614; pins and family print a different finding rather than a pass under the same variable).

Tests (all assertions are on what a command does, plus the facts the issue asks the WHY to carry):

Run, with a stub gh first on PATH that exits 97, GH_TOKEN=bogus, an empty GH_CONFIG_DIR:
/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt -o tmp_path_retention_policy=failed <files>

  • tests/test_doctor_validator_rows.py: 49 passed.
  • tests/test_doctor_next_is_a_command.py tests/test_doctor_validator_site.py tests/test_doctor_validator_verdicts.py tests/test_repo_hygiene.py tests/test_cli_reference.py: 193 passed.
  • tests/test_doctor_gitlink_repairs.py -k "crashed or template_alone": 2 passed.
  • tests/test_shape_doctor.py: 405 passed, 3 skipped in 1295.81s

Docs. None state the crash row's NEXT (README.md, AGENTS.md, docs/handbook.html and docs/cli.md were grepped for crash, repo_shape.py, own-validator NEXT wording), so none changed; docs/cli.md is unaffected (no flag or help text moved).

Pinned copies. None touched. shape-doctor.py is in no copy list.

Not settled. The legs and members rows' lockstep_next also print the own validate-pins.py / validate-family.py line for a gitlink that is off its pin; in a root that has also lost scripts/repo_shape.py that line crashes the same way. It is another row's NEXT (issue scope was the own-copy validator rows' crash path) and is left as it is.

🤖 Generated with Claude Code

Summary by Sourcery

Ensure validator crash findings provide a safe recovery path when the root’s own repo_shape.py is missing.

Bug Fixes:

  • Make own-copy validator crash rows recommend restoring a missing scripts/repo_shape.py from HEAD before rerunning the validator, preventing misleading results from readers’ Python environments.
  • Suppress the restore command when HEAD no longer contains the missing file and explain how to recover it instead.

Enhancements:

  • Centralize and reuse the shell-specific command used to restore files from HEAD across doctor rows.

Tests:

  • Add coverage for restore guidance across manifest, pins, and family validators, including committed deletions, unrelated crashes, shell dialects, command safety, and successful post-restore validation.

…st `scripts/repo_shape.py` names `git -C <root> checkout HEAD -- scripts/repo_shape.py` as its NEXT (none where HEAD no longer holds it) and the validator in its WHY, so a reader whose own PYTHONPATH reaches another `repo_shape` is not sent to a line that passes for them (#697)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR changes own-validator crash findings caused by a missing root scripts/repo_shape.py to recommend restoring that file from HEAD before rerunning the validator, while handling committed deletions explicitly, centralizing command quoting, preserving other crash behavior and verdicts, and adding broad regression coverage.

Flow diagram for missing repo_shape.py validator recovery

flowchart TD
    A[Own validator crashes] --> B{Root scripts/repo_shape.py missing?}
    B -- No --> C[Keep validator command as NEXT]
    B -- Yes --> D{HEAD holds the file?}
    D -- Yes --> E[Run git -C root checkout HEAD -- scripts/repo_shape.py]
    E --> F[Run validator named in WHY]
    D -- No --> G[No NEXT]
    G --> H[Restore from parent of deleting commit]
    H --> F
Loading

File-Level Changes

Change Details Files
Make own-validator crash findings caused by a missing root scripts/repo_shape.py recoverable without executing an unrelated environment-provided module.
  • Add a shared, shell-quoted git -C <root> checkout HEAD -- scripts/repo_shape.py command generator.
  • Use the restore command as NEXT when HEAD still contains the missing file, and explain the validator rerun in WHY.
  • Omit NEXT when HEAD no longer contains the file and explain recovery from the deleting commit’s parent.
  • Preserve the existing validator NEXT and crash explanation for all other crash causes.
  • Reuse the shared restore-command generator for unloaded-manifest findings without changing its output.
shape-doctor.py
Add comprehensive regression coverage for recovery behavior, command safety, and unchanged crash handling.
  • Cover manifest, pins, and family rows, including restore execution, validator reruns, shell pasting, committed deletions, and alternate crash causes.
  • Assert POSIX and Windows quoting plus the command-property constraints.
  • Verify unloaded-manifest output remains identical and overall verdicts and exit codes remain unchanged.
  • Update the existing paste-based crash test to execute the validator line described in WHY rather than the new recovery NEXT.
tests/test_doctor_validator_rows.py
tests/test_doctor_next_is_a_command.py
tests/test_shape_doctor.py

Assessment against linked issues

Issue Objective Addressed Explanation
#697 For own-copy validator rows that crash because the root lacks scripts/repo_shape.py, provide a NEXT command that restores the file from HEAD when available, and explain in WHY to rerun the validator afterward. ✅
#697 When HEAD does not contain scripts/repo_shape.py, provide no ineffective restore NEXT and explain why recovery is unavailable and what should happen next. ✅
#697 Preserve the existing validator NEXT and crash explanation for other crash causes, and leave verdicts and exit codes unchanged. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-16T23:10:12Z) Lane 3's independent review of #737 at 688a2cf4 (RV-a, ruling 8): READY. There is no MUST. One SHOULD becomes a v1.1.1 follow-up and four NITs are optional. Every claim in #697 measures as stated, on old main de926142 and on current main 66b230ed.

Measured with a fixture probe over roots that lost scripts/repo_shape.py, either left in the tree or with the deletion committed, across the manifest, pins and family gates:

main #737
verdict DRIFTED (1 copy-missing), exit 1 unchanged
crash rows' NEXT the gate's own line, which crashes ModuleNotFoundError git -C <root> checkout HEAD -- scripts/repo_shape.py
the cure pasted through sh exit 0, the file back, git status clean; then each gate passes
committed deletion no NEXT; the WHY names the file, HEAD, the parent of the commit and the gate's line

Unchanged where it should be:

  • The shape-currency and machine rows are identical to main's.
  • unloaded_manifest_row is byte-identical in 6 cases: project and family, each with no loss, a tree loss and a committed loss.
  • The Windows spellings are asserted exactly by RESTORE_SPELLINGS.

SHOULD → follow-up (v1.1.1): lockstep_next (shape-doctor.py:4384-4409, used by legs at :5156 and members at :7541) still prints the own validate-pins.py line in a root that has also lost repo_shape.py. That line exits 1 with ModuleNotFoundError, while the pins and manifest rows in the same report print the cure. #697 kept every other NEXT, so this is filed separately.

NITs (optional):

Mutants:

  • drop the cure: 16 tests fail;
  • cure every own-copy crash: 8 fail;
  • ignore the dialect: 2 fail;
  • swap the dialects: 3 fail;
  • drop the head_holds check: 3 fail;
  • cure the template copy too: an equivalent mutant, because the doctor cannot start without its own repo_shape.

Tests (stub gh):

Merges: clean with main (tree 70ff552e), and with #734, #736, #740 and lane 5's #730. The only edit #730 shares is in a docstring.

CI at the head: green.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 6 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="shape-doctor.py" line_range="1801-1802" />
<code_context>
+            "repo_shape sees it run on that one, which says nothing about "
+            "this root's own copy")
+    if head_holds(ctx.root, relative):
+        return (restore_from_head(ctx.root, relative),
+                f"it puts back the {relative} HEAD holds, the copy this "
+                f"project committed and {own} imports, where update-shape.py "
+                f"apply would copy a newer one. Then {then}")
</code_context>
<issue_to_address>
**Staged changes are discarded**

When the working-tree file is missing but the index contains staged changes for it, `restore_from_head` emits `git checkout HEAD -- <path>`, which resets both the worktree and index to HEAD and discards the staged version. Running the suggested NEXT loses those staged changes.

Restore only the working-tree path from HEAD, leaving the index unchanged.

Also at `shape-doctor.py:1309-1310`.
</issue_to_address>

### Comment 2
<location path="shape-doctor.py" line_range="1914" />
<code_context>
                        "imports from beside itself, is not there"
                        if own_copy else f"; {lost} is not there")
+            if own_copy:
+                command, why = lost_repo_shape_cure(ctx, own, lost, command)
         if not own_copy:
             detail["environment"] = True
</code_context>
<issue_to_address>
**Restore advice misses the crash**

When the own validator has an independent crash before importing `repo_shape` while `repo_shape.py` is absent or not a regular file, `validator_row` routes the crash through `lost_repo_shape_cure` without establishing that the missing module caused it. The restore command leaves the independent crash in place, while the row no longer presents the validator command as its NEXT.

Offer the restore cure only when the crash is caused by the missing or unusable `repo_shape.py`.

Also at `shape-doctor.py:1915`.
</issue_to_address>

### Comment 3
<location path="shape-doctor.py" line_range="1805-1808" />
<code_context>
+                f"it puts back the {relative} HEAD holds, the copy this "
+                f"project committed and {own} imports, where update-shape.py "
+                f"apply would copy a newer one. Then {then}")
+    return None, (f"HEAD does not hold {relative}, so no git checkout HEAD "
+                  f"puts it back and {own} would only crash again: a "
+                  "deletion already committed comes back from the parent of "
+                  f"the commit that made it. Then {then}")
+
+
</code_context>
<issue_to_address>
**Recovery source does not exist**

When the root has no usable HEAD, Git cannot inspect HEAD, or the path was never present in its history, `head_holds` returns false, so `lost_repo_shape_cure` describes a committed deletion and points to its commit's parent even when no such commit or recovery source exists. The reader gets inapplicable recovery instructions.

Distinguish these cases from a committed deletion and give recovery guidance that applies to the actual state.
</issue_to_address>

### Comment 4
<location path="shape-doctor.py" line_range="1801-1802" />
<code_context>
+            "repo_shape sees it run on that one, which says nothing about "
+            "this root's own copy")
+    if head_holds(ctx.root, relative):
+        return (restore_from_head(ctx.root, relative),
+                f"it puts back the {relative} HEAD holds, the copy this "
+                f"project committed and {own} imports, where update-shape.py "
+                f"apply would copy a newer one. Then {then}")
</code_context>
<issue_to_address>
**Checkout is blocked by obstruction**

When a directory or untracked obstruction occupies the missing path while HEAD holds the file, `restore_from_head` recommends checking out the file over the obstruction, but Git cannot write the tracked file at that path. The cure fails and the validator remains broken.

Detect the obstruction and avoid recommending checkout until it is safely handled.
</issue_to_address>

### Comment 5
<location path="shape-doctor.py" line_range="1800-1801" />
<code_context>
+            "reader whose own PYTHONPATH or site-packages reaches another "
+            "repo_shape sees it run on that one, which says nothing about "
+            "this root's own copy")
+    if head_holds(ctx.root, relative):
+        return (restore_from_head(ctx.root, relative),
+                f"it puts back the {relative} HEAD holds, the copy this "
+                f"project committed and {own} imports, where update-shape.py "
</code_context>
<issue_to_address>
**Dangling symlink stays unusable**

When HEAD tracks `scripts/repo_shape.py` as a symlink whose target is absent, `head_holds` accepts the symlink and `restore_from_head` checks it out, but restoring the dangling link does not provide an importable module. The validator continues to fail.

Check whether the tracked symlink resolves to a usable module before offering the restore cure.
</issue_to_address>

### Comment 6
<location path="tests/test_doctor_validator_rows.py" line_range="815" />
<code_context>
+    assert row["detail"]["exit"] == 1, row
+    assert "ModuleNotFoundError" in row["reason"], row
+    assert result.returncode == 1, result.stdout + result.stderr
+    assert payload_of(result)["verdict"].startswith("DRIFTED"), result.stdout
+    assert command_of(row)[:2] == ["git", "-C"], row["next"]
+    assert Path(command_of(row)[2]).resolve() == root, row["next"]
</code_context>
<issue_to_address>
**Wrong drift count passes**

When the doctor reports a DRIFTED verdict with an incorrect copy-missing count, `test_a_gate_that_lost_repo_shape_is_sent_to_put_it_back` accepts any verdict beginning with `DRIFTED`; a broken verdict such as `DRIFTED (2 copy-missing)` therefore passes despite the expected `DRIFTED (1 copy-missing)`.

Assert the exact expected verdict string.
</issue_to_address>

Sourcery assessment

Approval pending. 6 findings to address first.

Blocking findings: shape-doctor.py:1802, shape-doctor.py:1914, shape-doctor.py:1808, shape-doctor.py:1802, shape-doctor.py:1801, and 1 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread shape-doctor.py
Comment on lines +1801 to +1802
return (restore_from_head(ctx.root, relative),
f"it puts back the {relative} HEAD holds, the copy this "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Staged changes are discarded

When the working-tree file is missing but the index contains staged changes for it, restore_from_head emits git checkout HEAD -- <path>, which resets both the worktree and index to HEAD and discards the staged version. Running the suggested NEXT loses those staged changes.

Restore only the working-tree path from HEAD, leaving the index unchanged.

Also at shape-doctor.py:1309-1310.

Prompt for AI agents
In `shape-doctor.py` at lines 1801-1802:

**Staged changes are discarded**

When the working-tree file is missing but the index contains staged changes for it, `restore_from_head` emits `git checkout HEAD -- <path>`, which resets both the worktree and index to HEAD and discards the staged version. Running the suggested NEXT loses those staged changes.

Restore only the working-tree path from HEAD, leaving the index unchanged.

Also at `shape-doctor.py:1309-1310`.

Comment thread shape-doctor.py
"imports from beside itself, is not there"
if own_copy else f"; {lost} is not there")
if own_copy:
command, why = lost_repo_shape_cure(ctx, own, lost, command)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Restore advice misses the crash

When the own validator has an independent crash before importing repo_shape while repo_shape.py is absent or not a regular file, validator_row routes the crash through lost_repo_shape_cure without establishing that the missing module caused it. The restore command leaves the independent crash in place, while the row no longer presents the validator command as its NEXT.

Offer the restore cure only when the crash is caused by the missing or unusable repo_shape.py.

Also at shape-doctor.py:1915.

Prompt for AI agents
In `shape-doctor.py` at line 1914:

**Restore advice misses the crash**

When the own validator has an independent crash before importing `repo_shape` while `repo_shape.py` is absent or not a regular file, `validator_row` routes the crash through `lost_repo_shape_cure` without establishing that the missing module caused it. The restore command leaves the independent crash in place, while the row no longer presents the validator command as its NEXT.

Offer the restore cure only when the crash is caused by the missing or unusable `repo_shape.py`.

Also at `shape-doctor.py:1915`.

Comment thread shape-doctor.py
Comment on lines +1805 to +1808
return None, (f"HEAD does not hold {relative}, so no git checkout HEAD "
f"puts it back and {own} would only crash again: a "
"deletion already committed comes back from the parent of "
f"the commit that made it. Then {then}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Recovery source does not exist

When the root has no usable HEAD, Git cannot inspect HEAD, or the path was never present in its history, head_holds returns false, so lost_repo_shape_cure describes a committed deletion and points to its commit's parent even when no such commit or recovery source exists. The reader gets inapplicable recovery instructions.

Distinguish these cases from a committed deletion and give recovery guidance that applies to the actual state.

Prompt for AI agents
In `shape-doctor.py` at lines 1805-1808:

**Recovery source does not exist**

When the root has no usable HEAD, Git cannot inspect HEAD, or the path was never present in its history, `head_holds` returns false, so `lost_repo_shape_cure` describes a committed deletion and points to its commit's parent even when no such commit or recovery source exists. The reader gets inapplicable recovery instructions.

Distinguish these cases from a committed deletion and give recovery guidance that applies to the actual state.

Comment thread shape-doctor.py
Comment on lines +1801 to +1802
return (restore_from_head(ctx.root, relative),
f"it puts back the {relative} HEAD holds, the copy this "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Checkout is blocked by obstruction

When a directory or untracked obstruction occupies the missing path while HEAD holds the file, restore_from_head recommends checking out the file over the obstruction, but Git cannot write the tracked file at that path. The cure fails and the validator remains broken.

Detect the obstruction and avoid recommending checkout until it is safely handled.

Prompt for AI agents
In `shape-doctor.py` at lines 1801-1802:

**Checkout is blocked by obstruction**

When a directory or untracked obstruction occupies the missing path while HEAD holds the file, `restore_from_head` recommends checking out the file over the obstruction, but Git cannot write the tracked file at that path. The cure fails and the validator remains broken.

Detect the obstruction and avoid recommending checkout until it is safely handled.

Comment thread shape-doctor.py
Comment on lines +1800 to +1801
if head_holds(ctx.root, relative):
return (restore_from_head(ctx.root, relative),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Dangling symlink stays unusable

When HEAD tracks scripts/repo_shape.py as a symlink whose target is absent, head_holds accepts the symlink and restore_from_head checks it out, but restoring the dangling link does not provide an importable module. The validator continues to fail.

Check whether the tracked symlink resolves to a usable module before offering the restore cure.

Prompt for AI agents
In `shape-doctor.py` at lines 1800-1801:

**Dangling symlink stays unusable**

When HEAD tracks `scripts/repo_shape.py` as a symlink whose target is absent, `head_holds` accepts the symlink and `restore_from_head` checks it out, but restoring the dangling link does not provide an importable module. The validator continues to fail.

Check whether the tracked symlink resolves to a usable module before offering the restore cure.

assert row["detail"]["exit"] == 1, row
assert "ModuleNotFoundError" in row["reason"], row
assert result.returncode == 1, result.stdout + result.stderr
assert payload_of(result)["verdict"].startswith("DRIFTED"), result.stdout

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Wrong drift count passes

When the doctor reports a DRIFTED verdict with an incorrect copy-missing count, test_a_gate_that_lost_repo_shape_is_sent_to_put_it_back accepts any verdict beginning with DRIFTED; a broken verdict such as DRIFTED (2 copy-missing) therefore passes despite the expected DRIFTED (1 copy-missing).

Assert the exact expected verdict string.

Prompt for AI agents
In `tests/test_doctor_validator_rows.py` at line 815:

**Wrong drift count passes**

When the doctor reports a DRIFTED verdict with an incorrect copy-missing count, `test_a_gate_that_lost_repo_shape_is_sent_to_put_it_back` accepts any verdict beginning with `DRIFTED`; a broken verdict such as `DRIFTED (2 copy-missing)` therefore passes despite the expected `DRIFTED (1 copy-missing)`.

Assert the exact expected verdict string.

brettheap pushed a commit that referenced this pull request Oct 17, 2026
…alidate-family.py` and not the assembly root's `validate-pins.py` (and the holder gets #737's cure for a lost `repo_shape.py`), says to run the gate once the file is back "to confirm the digest" where HEAD lacks it, says the standard's copy also reports the root's missing gate, and the tests parse the lines they run with `shlex` (#776)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
main gained 25 pull requests since de92614: #634, #699, #671, #719,
#701, #717, #700, #702, #722, #720, #725, #721, #728, #705, #727, #718,
#744, #743, #749, #740, #737, #734, #742, #751 and #736. No conflict:
AGENTS.md, adopt-project.py and docs/cli.md merged as text, and
README.md and docs/handbook.html are unchanged by the merge. README.md
is 1473 lines, its cap; AGENTS.md 494, its cap; every handbook marker is
current; `render-cli-reference.py --check` is ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…es every reason THIS checkout cannot answer, and the notes are current to main bf1f099 with the 25 pull requests since de92614 (#563, #683)

- W683 and RV-f's N4 on #742: README's exit-3 list (Keeping a project's
  shape current, :1208-1220) and its handbook twin (`doctor`) named only a
  pin whose commit this checkout does not carry. They now say, as
  shape-doctor.py's EXIT CODES does: a pin's commit it lacks, a partial
  clone missing what is compared (#683), a read that fails
  (`update-upstream-unreadable`), a crashed validator of its own (#278,
  #410), or its own naming policy not loading (#678). +1 line, with
  tests/readme-cap.d/683.txt; README.md is 1474, its cap (1422 + 2 + 5 +
  12 + 22 + 10 + 1). The two `Keeping a project's shape current` markers
  move: `doctor` text and marker, `current` marker only.
- The notes, "What changed since v1.0.1", one entry per pull request from
  `git log --first-parent de92614..bf1f099`, each from its squash commit and
  the issues GitHub says it closes:
  - adopt-project.py: #699, #719, #749, #727, #717, #728 (with #621's
    hint), #743 + #720 + #718, #722, #721;
  - update-shape.py, bump-leg.py and a project's own scripts: #702 (with
    the pinned validators), #634;
  - scaffold, setup, family and the shared readers: #725, #671, #744;
  - shape-doctor.py: #742, #701, #737, #751, #736, #734, #700 + #705,
    #740;
  - install, CI and the docs: the exit-3 sentence above.
- Counts (99 pull requests since bfed143; 109 of 135 milestone issues
  closed at 10:28Z), the "current to main" sha and the open list (26) are
  at main bf1f099, still placeholders for tag time.

Left for round B: lane 5's eight lines (#630 issuecomment-6227129879),
whose pull requests #726, #747, #682, #724, #746, #745 and #738 are open
drafts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 17, 2026
… verification row's colon, `run_validator`'s "costs no child an import" and the decoy test's `sitecustomize` claim say what main does (#666) (#730)

Four comments and docstrings said something main no longer does. They now say what the code beneath them does. A fix round after review added two more of the same kind (see "Fix round"). Comments and docstrings only: no behaviour, no `--help` text, no pinned copy, so no README, handbook or `docs/cli.md` change.

- **`adopt-project.py`, the module docstring.** It said the verification table reads `code: 0 of N source paths (seeded from template)`. The printed row has no colon (`  {leg:<6} {count:>5} of {N} source paths{note}`). It now says the table's `code` row reads `0 of N source paths (seeded from template)`, the words `AGENTS.md` and `README.md` use since #505 and the docstring of the verification function uses too. `git grep 'code: 0 of'` finds nothing now.
- **`shape-doctor.py`, `submodule_config`.** The last sentence of the docstring gave a reason that is true of the string and not decisive: "It is also what the row PRINTS, and a lone surrogate is a string no UTF-8 stream can write." The table goes through `ascii_text` and `--json` through `json.dumps`, so neither would raise on a lone surrogate. I dropped the sentence, the issue's first direction. The comparison with `origin_url`'s reading of a clone's remote is the reason, and the paragraph already gives it first. **This hunk carries the bytes of lane 3's #700**, which landed as `55c6ba7d`: the two removed lines and the one added line (`    working clone.`) are the same. So the hunk is on main already and drops out of the squash (see "Evidence").
- **`shape-doctor.py`, `run_validator`.** "It costs no child an import" holds for an unedited copy only. A new paragraph says a root whose own copy of a validator gained an `import yaml` read `passes` while `site` was on, and now reads `crashed before reaching a verdict (exit 1): ModuleNotFoundError: No module named 'yaml'`, the crash its own CI gets, because `validate.yml` installs nothing. It calls that row a fair answer and says `-S` does not ask whether a copy was edited. No code change.
- **`tests/test_shape_doctor.py`, the decoy test's two comments** (the block above `DECOY_REPO_SHAPE` and the inline comment before `reached`). `run_validator` starts every child as `[sys.executable, "-S", script, ...]`, so no child imports a `sitecustomize`. The comments now say that, say that replace-not-prepend and the #306 review S1 case are held by `test_run_validator_gives_the_child_exactly_the_pythonpath_it_is_given`, and say that the decoy test holds the template row's real verdict under an exported `PYTHONPATH`.

The review of the first two commits approved with nits. Two new commits take S1 and N1: `7f2bb36b` makes both edits, and `426877bf` rewrites the last sentence of the N1 comment, which did not parse ("...a future removal of `-S`, which `test_run_validator_starts_every_child_with_no_site` ... is the test that holds"). It now ends "...a future removal of `-S`; `test_run_validator_starts_every_child_with_no_site` (in `tests/test_doctor_validator_site.py`) is the test that holds `-S`." Both are comments and docstrings only.

- **S1, a third stale sentence.** The docstring of `test_run_validator_gives_the_child_exactly_the_pythonpath_it_is_given` said that for the callers which pass no `pythonpath`, "only a probe like this one, or (b)'s decoy, can see that they have none". That is false under `-S`: a `run_validator` that keeps the variable for those callers passes the decoy test in all three cases and fails the probe test twice (the second row of the table below). It now says only a probe like this one can see that they have none, and that the decoy of (b) could before `-S` (#342). It is the same defect as the issue's, in a place the issue did not name. "Could" holds for the `manifest` and `pins` cases: with `-S` removed, 2 of the 3 decoy cases fail, and the `family` case never caught it.
- **N1, the inline comment overstated the loop.** The comment above `reached` said "What this loop still holds is that no row quotes the decoy or says it exited 86." Under `-S` the loop cannot fire. It now says so, says the loop is kept as a tripwire for a future removal of `-S`, and names `test_run_validator_starts_every_child_with_no_site` in `tests/test_doctor_validator_site.py` as the test that holds `-S`. That test is on main.
- **N2, this body.** The merge checks and the notes on #630 were stale (they cited `de926142` and a conflict that is gone). They are rerun and rewritten below.

**Only comments and docstrings changed.** `git diff 8062810 426877b` has 74 changed lines (44 added, 30 removed) in three files. A script checked each changed line against the file's comment tokens and docstring spans (both sides of the diff): 0 outside. A second check parsed each file at the base and at the head, blanked every docstring, and compared the ASTs: identical in all three files. For the two fix-round commits together, the AST of `tests/test_shape_doctor.py` with docstrings blanked is identical to the one at `a2113cff`, `git diff --check` is clean, and the longest added line is 75 columns (73 in the second commit).

**What the squash holds.** `git merge-tree --write-tree` of this head with origin/main `20c8873b` gives the tree `6ead2ffc`. `git diff 20c8873 6ead2ffc` is 71 changed lines in three files, and `shape-doctor.py` has 8 of them: the new `run_validator` paragraph and nothing else. `git diff` of the two trees has no hunk that mentions `submodule_config`, `surrogate`, `PRINTS` or `working clone`. On main, `submodule_config`'s docstring has no "PRINTS" sentence and ends in `working clone.`. The same two checks on that tree: 0 lines outside comments and docstrings, ASTs identical. On main `2d988200` with the head `426877bf` the squash is the same: 71 changed lines, 8 of them in `shape-doctor.py`, 0 outside comments and docstrings, ASTs identical, and no hunk that mentions `submodule_config`.

**`python3 scripts/render-cli-reference.py --check`**, at `426877bf`: `[ok] docs/cli.md is what the generator produces`. Only the first line of a module docstring is help text, and none of the first lines changed.

**The decoy comments, measured.** I did not take the issue's reading on trust. In a throwaway clone at the base I put each #306 regression into `run_validator` and ran the two tests (`test_an_exported_pythonpath_does_not_reach_a_template_validator`, three cases, and `test_run_validator_gives_the_child_exactly_the_pythonpath_it_is_given`, six):

| mutant | with `-S` (main) | with `-S` removed |
|---|---|---|
| the standard's `scripts/` put in FRONT of the operator's value | decoy test: 3 of 3 pass. Probe test: 1 of 6 fails | decoy test: 3 of 3 fail |
| the variable kept for the callers that pass none (S1) | decoy test: 3 of 3 pass. Probe test: 2 of 6 fail | decoy test: 2 of 3 fail |
| the operator's directory AHEAD of the standard's | decoy test: 3 of 3 fail. Probe test: 1 of 6 fails | not run |

So on main the decoy test catches neither #306 regression and the probe test catches both, as lane 4's R342 measured on #636's head. The third row is why the comment says the raising `repo_shape.py` "still bites": it kills an operator-first order. The review re-ran these mutants on `a2113cff` and got the same counts.

**The `run_validator` paragraph, measured.** A root whose own `validate-manifest.py` is `import yaml` plus a print, run through `run_validator` from this tree: exit 1 and `ModuleNotFoundError: No module named 'yaml'`. The same call with `-S` taken out of the child's argv: exit 0, `manifest ok`. `yaml` is importable from the machine's site-packages and not without `site`.

With the real `gh` unreachable (a stub first on PATH, a bogus token, an empty `GH_CONFIG_DIR`).

On `a2113cff`, before the fix round:

- `tests/test_cli_reference.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_scaffold_leg_path_collisions.py`: 250 passed (57.5 s).
- `tests/test_doctor_gitmodules_not_utf8.py`, `tests/test_doctor_validator_rows.py`, `tests/test_doctor_validator_site.py`, `tests/test_doctor_validator_verdicts.py`, `tests/test_adopt_spec_only.py`, `tests/test_adopt_entry_paths.py`, `tests/test_adopt_submodules.py` (the files that drive `run_validator`, `submodule_config` or the seeded-leg row): 334 passed (28 min 18 s).
- `tests/test_shape_doctor.py`, in two halves run side by side (the first 204 test ids, then the last 204): 204 passed in 11 min 31 s, and 201 passed, 3 skipped in 4 min 58 s. That is all 408 tests. The 3 skips are the PowerShell-line tests, which need `powershell.exe` on a Windows host.
- The review ran all of `tests/test_shape_doctor.py` there: 405 passed, 3 skipped (512 s). On the predicted merge with main `2fac9790` it ran `test_shape_doctor.py`, `test_doctor_validator_site.py`, `test_doctor_validator_rows.py`, `test_doctor_validator_verdicts.py`, `test_doctor_gitmodules_not_utf8.py`, `test_repo_hygiene.py`, `test_cli_reference.py` and `test_agents_md_codes.py`: 632 passed, 3 skipped (760 s).

On `426877bf`, after the fix round, which changes comments and docstrings only, so the runs above stand:

- The two tests the fix round touches, `test_an_exported_pythonpath_does_not_reach_a_template_validator` (3 cases) and `test_run_validator_gives_the_child_exactly_the_pythonpath_it_is_given` (6 cases), run by node id: 9 passed (11.7 s). The same 9 also passed (16.1 s) at `7f2bb36b`.
- `tests/test_repo_hygiene.py`: 77 passed (1.9 s). It also passed (2.0 s) at `7f2bb36b`.

No test fails on the base, because nothing here changes behaviour. The AST comparison above is the proof in their place.

`git merge-tree --write-tree` of this head against origin/main and against every ref on origin that could meet it. The full run was on 2026-10-16, with the head `7f2bb36b` (the one before the last comment commit) and main `20c8873b`: the 41 branches (`fix/*`, `l3-*`, `wip/*`, `docs/*` and `release/v1.0`) and the 55 PR heads from #630 up. That is 96 refs. I ran #731's head `ab55c29f` through the same list. After `426877bf` I re-ran main, now `2d988200`, and the six heads that edit `tests/test_shape_doctor.py`, the one file that commit touches (#705, #737, #740, `l3-669-members-row`, `l3-680-doctor-signals` and `l3-697-crash-cure-next`), and #630: all clean.

- **origin/main: clean.** At `20c8873b`, with the head `7f2bb36b`, the predicted tree is `6ead2ffc`. At `2d988200` (three `adopt-project.py` commits later, with their tests and a change in `scripts/shape_materialize.py`), with the head `426877bf`, it is `7ee5caf4`, and the squash is the same three files (43 added, 28 removed). #731's head `ab55c29f` is clean with `2d988200` too. This head with #731's head: clean.
- **#630's head `6fd33a93`: clean.** It conflicted with main in `docs/handbook.html` when this body was first drafted, and no longer does, with or without this change.
- **#700 and #702 have landed** (`55c6ba7d` and `20c8873b`), and so has #701, and #699, #717 and #719 for `adopt-project.py`. Their heads still merge clean with this head.
- **Every ref that edits `shape-doctor.py`, `tests/test_shape_doctor.py` or `adopt-project.py` merges clean with this head**, except the two old heads named in the bullet after next. They include the heads of PRs not on main yet (#705, #721, #737 and #742, and lane 3's `l3-680`, `l3-683`, `l3-687`, `l3-691` and `l3-697`, which were not on origin when this body was first drafted). #673 has no branch or PR head on origin.
- **Four refs conflict with origin/main on their own**, and so with this head: `docs/release-notes-v1.0.0` and `release/v1.0`, and the pre-squash heads of the landed #637 and #641. Their conflicts do not come from this change.
- **Two landed pre-squash heads conflict with this head only because they are old.** #632 conflicts with main on its own. #636 merges clean with main, but its old head conflicts with this head in `run_validator`'s docstring, next to the text #636 added; main has that text, and this head merges clean with main.
- **Refs that conflict with main on their own are not made worse.** #682, #745, #747, `fix/648`, `fix/681` and `fix/704` conflict with main in `templates/family-root/scripts/validate-family.py`; #742 and `l3-683` in `shape-doctor.py`. Each merges clean with this head.
- **Every other open head and branch: clean.**

Nothing from the issue. Its four lines are done. The decoy test's `sitecustomize` machinery is dead code under `-S` and I left it, because the issue says comment only. The comment above `reached` now says it is kept as a tripwire.

- **The decoy test's `sitecustomize.py` and exit-86 trap never run.** `DECOY_SITECUSTOMIZE`, `os._exit(86)` and the `(exit 86)` and `DECOY` clauses in `reached` can only fire for a child started without `-S`. They are kept as a tripwire and the comment says so. The other choice is to remove them; `test_run_validator_starts_every_child_with_no_site` already holds `-S`. Small, tests only.

Review: rv666, lane 5's independent review, approved a2113cf with nits; 7f2bb36 and 426877b take its S1 and N1 (above), and codex found no major issue at a2113cf. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 426877b passed tests, tests-git-bash and SonarCloud; the head 32722b3 is a clean update-branch merge of main bf1f099, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 55d5d3b over main dcf62ed: the shape_doctor, scaffold_leg_path_collisions, repo_hygiene, agents_md_codes, cli_reference sets, 686 passed, 3 skipped, 1 xfailed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts".

Closes #666

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…d A3 (three SHOULDs, four nits), and notes entries for the seven pull requests landed since bf1f099, current to main 2000488 (#563, #683)

Round A4, from lane 2's review (#630 issuecomment-6233033833, READY):
- SHOULD 1: the doctor's exit-3 list names "a validator of its own that
  crashed or could not be run", so the case of one that cannot be given
  its `PYTHONPATH` (shape-doctor.py's EXIT CODES, #278) is listed too:
  README.md:1208-1222, its handbook twin (`doctor`) and the notes' docs
  line. The paragraph re-wraps to one more line:
  tests/readme-cap.d/683.txt is +2, and its text now says which causes
  the EXIT CODES block names and that #742 added the partial clone and
  #701 the naming policy. README.md is 1475 lines, its cap (1422 + 2 + 5
  + 12 + 22 + 10 + 2). Both `Keeping a project's shape current` markers
  move to c728c970: `doctor` text and marker, `current` marker only.
- SHOULD 2: #727's entry names the leg description, whose em dash stopped
  every `execute` against GitHub at the first leg.
- SHOULD 3: #749's entry names `source-ambiguous`, for an `org/repo` that
  is also a directory here.
- Nits: the doctor section's three near-duplicate pairs are merged (#636
  with #737, #632 with #700 and #705, #586 with #736); #718 has its own
  bullet; #634's entry names `git_out`'s spelling for every caller and
  #744's the gate's `--link-source-label` and `--link-remedy`; the header
  comment says 2026-10-17 at main 2000488.

Wave B1, `git log --first-parent bf1f099..2000488`: #706 (#664), #703
(#696), #670 (#646), #682 (#648), #726 (#667), #730 (#666) and #724
(#665), each from its squash commit; #726's and #682's from lane 5's lines
(#630 issuecomment-6227129879), #682's with the unmerged `.gitmodules`
its last round added, and #724's with lane 5's note that v1.0.0's
test-hygiene line on the generator's `.pyc` files closes. The counts (106
pull requests; 116 of 135 milestone issues closed at 11:53Z), "current to
main" and the open list (19) are at main 2000488, still placeholders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…gs` and `members` lockstep NEXT is the `pins` and `family` rows' `git -C <root> checkout HEAD -- scripts/repo_shape.py` (none where HEAD no longer holds it), where it named the root's own gate, which dies on the import (#750) (#771)

Closes #750.

In a root that lost `scripts/repo_shape.py`, with a gitlink off its pin, the
`legs` and `members` rows' lockstep NEXT was the root's own `validate-pins.py`
or `validate-family.py` line. That gate dies on `import repo_shape` and exits
1, so the line could not do what its WHY said of it, while the `pins` and
`family` rows beside it already printed the `checkout HEAD` cure (#737).

Now `lockstep_next` in `shape-doctor.py` returns that cure where the root's
own copy cannot import `repo_shape`:
`git -C <root> checkout HEAD -- scripts/repo_shape.py`, or no NEXT where HEAD
no longer holds the file. Its WHY names the gate to run once the file is back.
Every other lockstep line, the verdict and the exit code are unchanged. The
cure's WHY, which the `pins`, `family` and `manifest` crash rows and the
unloaded-manifest row share, now says "the copy this root committed" where it
said "this project"; in `legs` and `members` it says "run" where the crash
rows say "run again". `tests/test_doctor_validator_rows.py` pastes the cure
and then the gate from `/`.

Lane 3's independent reviewer RV-a (opus) found it READY on `c90a137c` (#771
issuecomment-6227877943) and again on `d4ddac85` (-6229504137). Its one
SHOULD, the `agent-files` row's WHY, which still names a gate that dies, is
#776, in #780. The last commit, `f1234bc6`, takes round 2's one new NIT, the
unloaded-manifest row's "this project"; RV771n found it READY (#771
issuecomment-6244822339).

It lands on Brett Heap's word "merge lane 3's v1.1.1 drafts after the v1.1.0
tag" (2026-10-17T13:00:57Z).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…ent-files` row's WHY puts the file back before it names the root kind's own gate, `validate-pins.py` in an assembly root and `validate-family.py` in a family holder, to confirm the digest (or says why no command does), and names the standard's copy of that gate where the root has none, so a holder is no longer sent to a `validate-pins.py` it lacks (#776) (#780)

Closes #776.

In a root that lost `scripts/repo_shape.py`, the `agent-files` row for a
deleted pinned copy (`AGENTS-shape.md`) ended its WHY with "then
`python3 <root>/scripts/validate-pins.py --root <root>` to confirm the
digest". That line exits 1 with
`ModuleNotFoundError: No module named 'repo_shape'` there, and it named the
root's own `validate-pins.py` even where the root has none.

Now `confirm_digest` in `shape-doctor.py` ends the WHY by putting the file
back first, with #737's cure, and then naming the gate: the root kind's own,
`validate-pins.py` in an assembly root and `validate-family.py` in a family
holder, which has no `validate-pins.py`. Where HEAD no longer holds the file
it says why no command does and where the file comes back from. Where the
root has no gate of its own it names the standard's template copy of that
gate, and where no line can be named it names no gate. In an assembly root
that keeps its gate and `repo_shape.py` the WHY is byte for byte what it was;
in a holder that keeps its gate it names `validate-family.py` where it named
`validate-pins.py`. The row's NEXT, status and detail do not move in either.
`gate_line` is the reading `lockstep_next` ran, now shared.
`tests/test_doctor_validator_rows.py` runs the NEXT, the cure and the gate in
order, for an assembly root and a holder, and pastes the NEXT and the
standard's template line from `/`.

Lane 3's independent reviewer RV-a (opus) found it READY in its second and
third rounds (#780 issuecomment-6229763740 and -6229805934). After #771
landed, it was restacked onto #771's squash by three merges, and its changed
lines equal its delta over #771's head (#780 issuecomment-6246123568). 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