Skip to content

shape-doctor.py: a validator row prints the PYTHONPATH it ran the standard's TEMPLATE copy under, in sh's or PowerShell's dialect, and runs the root's OWN copy under none, so the manifest, pins and family rows' NEXT, pasted anywhere, prints the validator's own finding and exits with detail.exit instead of dying on ModuleNotFoundError: No module named 'repo_shape', and a root that lost its own repo_shape.py is no longer told its gate passes (#238) - #287

Merged
brettheap merged 3 commits into
mainfrom
fix/doctor-template-validator-next-on-main
Oct 12, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)
Claim: #238 (comment)

What changes

shape-doctor.py's manifest, pins and family rows (and the unloaded-manifest row #205 added) print a NEXT command. When a root has no scripts/validate-<x>.py of its own, that command ran the standard's TEMPLATE copy as a bare command, and the template imports repo_shape from beside itself, where there is none: pasted from any directory it died on ModuleNotFoundError: No module named 'repo_shape' with exit 1, which is also a validator's exit for "findings". The doctor's own child only ran it because run_validator puts <standard>/scripts on the child's PYTHONPATH, so the row quoted a process the reader's paste was not.

  • A new validator_command spells the line, for the reader's platform, from the one own_copy that validator_for returned: PYTHONPATH=<standard>/scripts python3 <template> --root <root> in sh, $env:PYTHONPATH = '<standard>\scripts'; python <template> --root <root> in PowerShell. validator_row and unloaded_manifest_row both build their NEXT through it. quote_arg takes a keyword-only always for the PowerShell value, which follows = and would otherwise be run as a command.
  • validator_scripts(shape, own_copy) decides the directory AND the condition once: the standard's scripts/ for the template copy, nothing for the root's OWN copy. run_validator sets PYTHONPATH only when a caller asks (pythonpath), and validator_row asks with that answer. So the run and the printed line apply PYTHONPATH under one condition.
  • A root's own validator that cannot import repo_shape from beside itself (it lost scripts/repo_shape.py) is now a FINDING quoting the validator's ModuleNotFoundError, where the doctor used to say its gate passes, or quote the validator's own finding, over a gate that dies on the import when the project runs it. Its own-copy NEXT line is unchanged byte for byte.
  • The other run_validator callers (check_naming, check_not_a_root_naming, check_machine) get no PYTHONPATH: the naming script inserts its own directory ahead of its repo_shape import and the machine preflight never imports repo_shape at all. check_manifest is not one of them; it goes through validator_row.
  • The last commit corrects run_validator's docstring, which said the machine preflight "finds repo_shape itself" (it needs nothing from it).

Review history

This is a REPLAY, not a merge, of the two commits reviewed on fix/doctor-template-validator-next (stacked on PR #232's head 1c9282e, #205), onto main after #232 squash-landed as d94d155. A merge of main would have kept a second copy of MANIFEST_ROW, refusal_line, unloaded_manifest_row and waiting_row outside the conflict markers.

  1. 59724c9: review MERGEABLE WITH NITS (review238-59724c9). Its S1 (the run and the printed line applied PYTHONPATH under different conditions for a root's own copy) was taken; its S2 (replay, never merge) is this replay.
  2. 88c81e0: the S1 fix round and its PowerShell test. Re-check MERGEABLE WITH NITS (review238-88c81e0), no MUST. Its S2 (the head's message said UNTESTED and carried WIP(...)) is why the replayed commit carries a corrected message; its S1 (main moved, shape-doctor.py: a project.yaml or family.yaml that is there and does not load is INVALID (manifest) or INVALID (family), exit 1, quoting load_yaml's own refusal and naming the project's own validator, instead of NOT A SHAPE ROOT, exit 2, with an adopt-project.py plan NEXT and a Triad row calling an assembly root a single repository (#205) #232 conflicted) is why this waited for shape-doctor.py: a project.yaml or family.yaml that is there and does not load is INVALID (manifest) or INVALID (family), exit 1, quoting load_yaml's own refusal and naming the project's own validator, instead of NOT A SHAPE ROOT, exit 2, with an adopt-project.py plan NEXT and a Triad row calling an assembly root a single repository (#205) #232.
  3. The replay: git range-diff 1c9282e..88c81e0 d94d155..HEAD~1 shows the first commit = and the second ! with a message-only difference (no file differs); git patch-id is equal for both pairs and for the net diff.
  4. The branch is fix/doctor-template-validator-next-on-main; the old branch is left where it is.

Follow-ups, none in this PR: the re-check's N1 (an operator's own exported PYTHONPATH still reaches a root's own validator, so it can bring the false pass back for that operator), N2 (unloaded_manifest_row's NEXT promises "refuses it the same way until it loads", and pasted over a root that also lost scripts/repo_shape.py it dies on the import), O5 (the installed openRepoShape --doctor prints NEXT lines that name a clone its EXIT trap has removed), and review238-59724c9's O1-O4.

Tests

env GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t PATH=<gh shim that exits 97>:<git-filter-repo 2.47.0>:$PATH /usr/bin/python3 -m pytest tests/test_shape_doctor.py tests/test_shape_advisory.py tests/test_cli_reference.py -q: 295 passed, 3 skipped in 236.55s, over the final tree (2ed5dd0). The 3 skips are test_a_template_validators_next_runs_when_pasted_into_powershell, which runs only on a Windows host. The reviewers' pre-replay merge run was 285 passed, 3 skipped; main has gained tests since.

NOTE: that PowerShell test has not run anywhere yet. tests-windows is its first run, so this head is green only when tests, tests-macos AND tests-windows all pass.

Closes #238

Summary by Sourcery

Make validator findings and remediation commands consistent across doctor runs and pasted shell or PowerShell invocations.

New Features:

  • Include the required import path in printed validator NEXT commands for both sh and PowerShell when using standard template validators.

Bug Fixes:

  • Keep validator execution and pasted NEXT commands under the same environment so template findings are reproduced correctly.
  • Report missing repo_shape imports from a project's own validator as findings instead of incorrectly passing the gate.
  • Ensure unloaded-manifest remediation commands use the same corrected validator invocation.

Enhancements:

  • Centralize validator environment selection and command generation while preserving existing own-validator commands.
  • Allow PowerShell assignment values to be quoted even when they would otherwise be emitted bare.

Tests:

  • Add coverage for template and own validator NEXT commands, missing imports, unloaded manifests, shell quoting, and PowerShell command generation.

brettheap and others added 3 commits October 11, 2026 23:55
…y prints the `PYTHONPATH` it ran it under in its NEXT, in sh's or PowerShell's dialect, so pasting the `manifest`, `pins` or `family` row's NEXT prints the validator's own finding and exits with `detail.exit` instead of dying on `ModuleNotFoundError: No module named 'repo_shape'` with exit 1, a finding's code (#238)

A root with no `scripts/validate-<x>.py` of its own is checked by the
standard's template copy, and the row's NEXT named that template as a bare
command. The template imports `repo_shape` from beside itself, and
`repo_shape.py` is copied INTO a project out of the standard's `scripts/`, so
the doctor's own child ran it only because `run_validator` puts
`<standard>/scripts` on the child's `PYTHONPATH`. The printed line was a
different process from the one whose output the row quoted: pasted from any
directory with no `PYTHONPATH` in the environment it printed a traceback and
exited 1, which is also what a validator exits with for "findings", so
anything keyed on the exit code read a crash as the finding the row had
already reported. A reader whose shell exported a `PYTHONPATH` reaching the
standard never saw it.

The fix is in the one definition #205 split out, not in a row.
`validator_for` now also returns whether the validator is the root's own
copy, and a new `validator_command` spells the line from that, for the
reader's platform, the way `copy_command` and `scaffold_command` already do
(`platform` is passed on, `None` is the host):

  sh          PYTHONPATH=<standard>/scripts python3 <template> --root <root>
  PowerShell  $env:PYTHONPATH = '<standard>\scripts'; python <template> --root <root>

`validator_row` (the `manifest`, `pins` and `family` rows) and
`unloaded_manifest_row` both build their NEXT through it, so there is no
per-row patch. The root's own copy is unchanged byte for byte: it sits beside
its own `repo_shape.py`, and an environment in front of it would send a
project's pinned gate at the standard's `repo_shape.py` instead.

`validator_scripts` names the directory once, for `run_validator`'s `env` and
for the printed line, so the two cannot name different places.

Decisions the issue left open:
- The PowerShell value is quoted whether or not `quote_arg` would leave it
  bare. What follows `=` is a pipeline, so `$env:PYTHONPATH = C:\shape\scripts`
  would RUN the path as a command. `quote_arg` takes a keyword-only `always`
  for that position and nothing else changes, so the doubling of an
  apostrophe stays in the one function that knows it.
- ONE directory, assigned, not spliced: the doctor's child prepends to the
  reader's `PYTHONPATH`; a printed line cannot see it and sets only the path
  the import needs. `PYTHONIOENCODING` and `PYTHONDONTWRITEBYTECODE` are the
  doctor's own, for quoting prose and for writing nothing, and are not
  printed.
- PowerShell's `$env:` is the session's, so the assignment outlives the line.
  That is the dialect's property; undoing it would delete a `PYTHONPATH` the
  reader may have meant to keep.
- The row's reason is untouched: it already says "the standard's <template>".

Tests (tests/test_shape_doctor.py): for a root with no validator of its own
and a gate that fails on a file that still loads (a wrong `topic:`, a zeroed
pin digest, a `Bad Id` family id), the row's NEXT is pasted into `sh -c` from
the filesystem root with `PYTHONPATH` removed and must print the finding, exit
with `detail.exit` and show no traceback; the same line without its
environment is shown to die on the import, so the test can see the defect.
The same for `unloaded_manifest_row` over each manifest breakage and a
holder; from a standard checked out at a path with a space and an apostrophe;
`--json` and the text report carry one line; the root's own copy is asserted
byte for byte in both rows; and the Windows spelling is asserted as text
through the builder with `platform="nt"`, the doctor's own spelling of the
platform, including the apostrophe doubling and the always-quoted value.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… prints and under no other, so only the standard's TEMPLATE copy gets `<shape>/scripts`, and a root's OWN validator that cannot import `repo_shape` from beside itself is a FINDING quoting the validator's `ModuleNotFoundError` instead of a "passes" its own gate would never give (#238)

Review S1 (fix round 1). `run_validator` put `<shape>/scripts` on the
child's `PYTHONPATH` for EVERY validator run, while `validator_command`
printed it only for the standard's template copy. While a root keeps
`scripts/repo_shape.py` beside its own validator the difference is inert,
because the validator inserts its own directory first. A root that lost that
file got a row that quoted what the doctor's child saw -- a pass, or the
validator's own finding -- over a gate that dies on `ModuleNotFoundError`
when the project runs it, and a pasted line that did exactly that.

`validator_scripts(shape, own_copy)` is now the one place the CONDITION is
decided as well as the directory: it answers the standard's `scripts/` for
the template copy and None for the root's own. `validator_row` hands that to
`run_validator` (a new `pythonpath` argument, off unless a caller asks) and
`validator_command` prints from the same answer, each asked with the one
`own_copy` that `validator_for` returned, so there is no second test of it to
drift. The own-copy NEXT line stays byte-identical. The two docstring
sentences the review quotes ("the line and the run cannot name two different
places", "it sits beside its own `repo_shape.py`") are rewritten so each is
true.

Decision the brief left open: the other `run_validator` callers
(`check_naming`, `check_not_a_root_naming` and `check_machine`) now get no
`PYTHONPATH` either. `check_manifest` is not one of them: it goes through
`validator_row` and gets the decision above. The two naming callers run the
standard's own `validate-repository-naming.py`, which inserts its own
directory ahead of its `repo_shape` import. The machine preflight needs
nothing from `repo_shape` at all: `setup-project.py --preflight` returns
before the only `import repo_shape` (deferred) and its module imports are the
standard library's, so it never imports it. So it was inert for all three and
printed nowhere.

Review N1. The PowerShell spelling is kept. Its docstring now says what it
does: the `$env:PYTHONPATH` assignment lasts for the PowerShell session and
REPLACES any `PYTHONPATH` the reader had, which is accepted because the issue
and the plan named this spelling and the effect ends with the session. The
backwards reason ("would delete a `PYTHONPATH` the reader may have meant to
keep") is deleted.

Review N3. A Windows-only test runs each template NEXT line for real in a
fresh `powershell -NoProfile -NonInteractive -Command "<line>; exit
$LASTEXITCODE"` (cwd the filesystem root, `PYTHONPATH` removed, the row's
trailing `#` comment cut first so it cannot swallow the `exit`, output read
as bytes) and asserts the validator's finding and the row's `detail.exit`.
Skipped elsewhere with the reason beside it; it has not been run anywhere
until tests-windows runs it.

Tests: a root with its own validator and no `scripts/repo_shape.py`, for the
manifest, pins and family rows -- (a) with the file edited red, the row is a
FINDING quoting the `ModuleNotFoundError` and its own-copy NEXT pasted into
`sh -c` from `/` with no `PYTHONPATH` exits with `detail.exit`; (b) with a
valid file, the row no longer says the validator passes and agrees with the
project's own gate run directly. Mutation: `PYTHONPATH` put back on the
own-copy run only -- all 6 new tests fail (log mut2.log). The template-copy
tests are unchanged. Replayed onto main d94d155 (after #232 landed), the
targeted run (tests/test_shape_doctor.py, tests/test_shape_advisory.py,
tests/test_cli_reference.py) gives 295 passed, 3 skipped; the 3 are the
Windows-only PowerShell test. Not here: N2, O1-O4.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ight needs nothing from `repo_shape` and never imports it, instead of that it "finds `repo_shape` itself", so the note on which of the standard's own scripts get no `PYTHONPATH` is true of the preflight as well as of the naming script (#238)

Re-check N3 on the fix round. The sentence said "The standard's own scripts
(naming, the machine's preflight) find `repo_shape` themselves". That is
true of `validate-repository-naming.py`, which inserts its own directory
ahead of its `repo_shape` import. It is not true of `setup-project.py
--preflight`: that returns before the only `import repo_shape` (deferred) and
its module imports are the standard library's, so it finds nothing because
it looks for nothing. A reader taking the old wording would think the
preflight needed `repo_shape` on the path and would restore a `PYTHONPATH` it
has no use for.

Docstring only; no code, no test and no `--help` text changes.

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

sourcery-ai Bot commented Oct 12, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days and 18 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR synchronizes validator execution with the NEXT command it prints: template validators receive the standard scripts directory through an explicitly generated shell or PowerShell command, while root-owned validators run without injected PYTHONPATH and now correctly surface missing-local-module failures. Extensive regression tests cover pasted commands, unloaded manifests, platform-specific quoting, and root/template ownership cases.

Sequence diagram for synchronized validator execution and NEXT commands

sequenceDiagram
    participant Doctor as shape_doctor
    participant Selection as validator_for
    participant Environment as validator_scripts
    participant Validator as validator
    participant Reader as Reader

    Doctor->>Selection: validator_for(ctx, own, template)
    Selection-->>Doctor: used, which, own_copy
    Doctor->>Environment: validator_scripts(shape, own_copy)
    Environment-->>Doctor: scripts or None
    Doctor->>Validator: run_validator(ctx, used, args, pythonpath)
    Validator-->>Doctor: code, quoted, output
    Doctor->>Environment: validator_scripts(shape, own_copy)
    Environment-->>Doctor: same scripts or None
    Doctor->>Doctor: validator_command(shape, used, own_copy, args)
    Doctor-->>Reader: NEXT command
    Reader->>Validator: paste NEXT command
    Validator-->>Reader: validator finding or successful output
Loading

File-Level Changes

Change Details Files
Make validator execution and printed NEXT commands use the same copy-selection and PYTHONPATH policy.
  • Added a single helper to assign PYTHONPATH only to standard template validators.
  • Made run_validator accept optional PYTHONPATH instead of always injecting the standard scripts directory.
  • Propagated the validator ownership flag from validator_for into validator_row and unloaded_manifest_row.
  • Added shell- and PowerShell-specific command generation, including assignment quoting and platform-aware path quoting.
  • Preserved bare commands for root-owned validators and left unrelated validator callers without PYTHONPATH.
shape-doctor.py
Report failures from root-owned validators accurately when their local repo_shape module is missing.
  • Run root-owned validators without the standard's PYTHONPATH so import failures match the project's own gate.
  • Ensure missing local repo_shape.py produces a FINDING rather than a false pass.
  • Keep the root-owned NEXT command unchanged and consistent with the doctor's execution.
shape-doctor.py
tests/test_shape_doctor.py
Add regression coverage for template fallback commands, ownership behavior, shell dialects, and import failures.
  • Verify pasted sh commands reproduce template findings without traceback or ModuleNotFoundError.
  • Add Windows/PowerShell command-text assertions and a Windows-only execution test.
  • Test unloaded-manifest NEXT commands and unchanged root-owned commands.
  • Cover awkward paths, apostrophes, forced PowerShell quoting, and platform override behavior.
  • Validate that missing repo_shape.py in an own validator is reported as a finding and does not pass.
tests/test_shape_doctor.py

Assessment against linked issues

Issue Objective Addressed Explanation
#238 Ensure NEXT commands for validator rows that fall back to standard template validators include the standard's scripts directory on PYTHONPATH, using the appropriate sh or PowerShell syntax, so pasted commands run the same validator and report the recorded exit status. ✅
#238 Apply the fallback-command fix consistently to the manifest, pins, family, and unloaded-manifest validator rows while leaving commands for roots' own validator copies without an added PYTHONPATH. ✅
#238 Ensure a root's own validator is executed without the standard's PYTHONPATH, so missing repo_shape.py files produce a finding rather than an incorrect pass, and add coverage for command execution, quoting, platform dialects, and exit/output behavior. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit ede15ce into main Oct 12, 2026
5 checks passed
@brettheap
brettheap deleted the fix/doctor-template-validator-next-on-main branch October 12, 2026 00:37
brettheap added a commit that referenced this pull request Oct 12, 2026
…s `_entry_findings` composed with #255's, and this PR's pair renamed `unwritable_findings`/`unwritable_remediation`

#231 (6f45bf9) puts each entry's path first in `_entry_findings` and
returns `unsafe_remediation` ahead of the rest; this branch appends each
value `execute` writes into a file that no file can hold. The two hunks
are its docstring's order sentence and its return. The sentence keeps
main's order and appends this PR's clause; the return is
`(findings + submodule_findings + unwritable_findings,
  unsafe_remediation + remediation + unwritable_remediation)`, so the
paths still come first, as #196 needs.

This PR's local pair `value_findings, value_remediation` is renamed
`unwritable_findings, unwritable_remediation`. #261's branch (ca21b57)
binds `value_findings, value_remediation` in the same function, from
`_manifest_findings`: a resolution keeping both lines under one name
would silently drop one PR's findings. With the rename it cannot.

The rest of main (#254/#281, #238/#287, #203/#236, #204/#227,
#249/#250, #241, #215 and the earlier merges) merges with no conflict.
The resolution is the lane's re-verify one (`resolve-main.py`, 385
passed on 6f45bf9), rename aside.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…#241, #249/#250, #204, #203 and #238 merge with no conflict, and a `.gitmodules` value git refuses is answered before #190's relative-url check, which reads no url from a file git cannot read

Main since d94d155, the main GitHub's update-branch merged at 8ce5614:
c2ff2b0 (#215 via #240), c3a9645 (#241 via #256), b30595c (#249/#250 via
#284), be2790e (#204 via #227), 6d8d88b (#203 via #236) and ede15ce (#238
via #287).

No conflict hunk. Three files were changed on both sides; `git merge`
auto-merged each, and every hunk of each side lies apart from the other's.
Each side's +/- lines are identical before and after the merge in all
three (`git diff -U0` d94d155..8ce5614 against ede15ce..merge, and
d94d155..ede15ce against 8ce5614..merge). Line numbers are the merged
file's.

adopt-project.py
- main, c2ff2b0 (#215): 384-506, `SUBMODULE_TYPED_KEY_RE` to `_refused_value`
  and `_who_refuses_a_boolean`; 510-515 and 522-524, `_registrations`'
  docstring and its return of `_refused_value`'s words; 791-797,
  `Source.registered_submodules`' docstring; 2181-2185, `_where_git_fails`'
  docstring.
- #190: 36-38 and 51-53, the module docstring; 102-103, the `repo_shape`
  import list; 334-339, `Registrations.url`; 2252-2501, `RELATIVE_URL` to
  `_relative_url_findings`; 2503-2548, `_submodule_findings` and
  `_entry_findings` with `local_remote_dir` and the NOTEs; 2608 and 2630,
  `cmd_check`; 3070-3092, `_repository_urls` and `_remote_urls`;
  3111-3216, `_refuse_what_check_finds` and `_as_resolved_here`; 3443,
  `cmd_execute`.

scripts/repo_shape.py
- main, b30595c (#249/#250): 173-535, the YAML reader's quote boundaries,
  from the comment after `YamlError` to `_split_flow`.
- #190: 1975-1985, `REMOTE_PORT_RE` and `REMOTE_HOST_ALIASES`; 2099-2116,
  `remote_key`'s docstring and body.

tests/test_shape_doctor.py
- #190: 2662-2693, `test_a_port_and_githubs_ssh_host_are_not_part_of_a_remotes_key`.
- main, ede15ce (#238): 5209-5808, the validator-PYTHONPATH tests after
  `test_a_drifted_roots_next_command_runs_when_it_is_pasted`.

Every other file is one side's blob: main's for the 17 main changed, and
8ce5614's for tests/test_adopt_relative_submodule_url.py and
tests/test_adopt_submodules.py. The merge's diff against main is #190's
five files, with the stat it had against d94d155 (1097+, 41-). No
function or class is bound twice at the top level of adopt-project.py,
scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py,
shape-doctor.py, scripts/family.py or the three test files. The helpers
c2ff2b0 moved into tests/conftest.py (`planned`, `findings`, ...) are
plain functions, and tests/test_adopt_relative_submodule_url.py imports
seven named helpers from conftest and defines its own `planned`.

HOW #215 AND #190 MEET. `_submodule_findings` lists
`submodule_plan_problems` first, which holds #215's
`plan-gitmodules-unreadable`, then `_relative_url_findings`. Where git's
submodule reader refuses a value of the source's `.gitmodules`,
`registered_submodules` answers with git's words, a string, and
`_kept_relative_urls` reads no url from a string. So a source with both
problems gets #215's finding alone, and no NOTE either. #190's finding is
not lost: once the value is fixed, `check` reports it, and `execute`
refuses it through the same list. #190 adds lines only after #215's, so it
can hide nothing of #215's. A relative url kept in a leg is always beside a
gitlink kept in a leg, where `_where_git_fails` names a failure, so the
unreadable finding is always printed whenever #190's would be skipped.
Measured on this tree with a source holding `upstream/dependency`,
`url = ../dependency` and `update = bogus`, with `.gitmodules` and
`upstream/` kept in code for testorg:
- `origin` in otherorg: `check` 1 with `plan-gitmodules-unreadable` alone;
  `execute --local-remote-dir` 2, `REFUSED plan-gitmodules-unreadable`,
  with no remotes directory made. With `update = checkout`: `check` 1,
  `plan-submodule-relative-url`.
- `origin` in testorg: the same `check` 1 and `execute` 2, with no NOTE.
  With `update = checkout`: `check` 0 and the NOTE.
- At 8ce5614, before #215, the first source was `plan-submodule-relative-url`.
  The second was `check` 0 with the NOTE, over a file `git fast-export`
  dies on.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…, #242 via #308, #262 and #263 via #313) into fix/adopt-record-submodule-placement

Brings in ede15ce (#238), 6f45bf9 (#196), b421e6b (#254), c0ed615 (#242)
and 71b58a1 (#262, #263) on top of 6d8d88b. No rebase. ede15ce changes
shape-doctor.py. 6f45bf9 and 71b58a1 change adopt-project.py and
scripts/shape_materialize.py. b421e6b changes scripts/shape_materialize.py
too, giving a project's validators an explicit PYTHONIOENCODING. All of
these are files this branch changes. c0ed615 changes scripts/family.py
alone.

No conflicts. Checked:

- adopt-project.py: #196 gives `check` and `execute` one test for an
  entry's path, `entry_path_problem`, as the finding `plan-unsafe-path`.
  #262 and #263 add `_root_shape_findings` to `_leg_findings`, for a kept
  root path where the shape writes. Both reach `execute` through
  `_refuse_what_check_finds`, before any leg exists and before
  `_commit_the_split` appends the record. This branch's `submodule_record`,
  `_resolution_text`, the record's preamble, `_answered_legs`,
  `_kept_in_a_leg`, `_commit_the_split`, `cmd_execute`,
  `submodule_plan_problems`, `_submodule_findings`, `Plan`, `y`, `emit`
  and `walk` are unchanged by the merge. `_root_shape_findings` is asked
  only beside a `legs:` mapping, so the doctor's placement plan, whose
  `legs:` is a list, still goes through `_leg_findings(plan, None)`.
- shape-doctor.py: #238 changes `validator_row` and `run_validator`. This
  branch's `check_placement`, `recorded_submodules`, `audit_leg`,
  `placement_row`, `placement_verdict` and `Context` are unchanged by the
  merge.
- scripts/repo_shape.py is untouched by these commits, so the record's
  writer and reader, and `double_quoted` in `submodule_record`, are
  3e86200's.
- Both sides added lines to the module docstrings of adopt-project.py and
  shape-doctor.py. Every line either side added is in the merge.
- No top-level name is bound twice in adopt-project.py,
  scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py,
  shape-doctor.py, validate-manifest.py, update-shape.py, scripts/family.py
  or scripts/bump-leg.py. Each merged module's names are the base's plus
  what either side added. Every definition only one side changed is that
  side's text.
- AGENTS.md is untouched: 472 lines, under the cap of 472.
- The branch's diff against main is still #189 alone: 12 files, +1670
  -157, file by file the same as against 6d8d88b.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
Main brings five commits since 6d8d88b:

- ede15ce (#238 via #287): shape-doctor.py's validator rows;
- 6f45bf9 (#196 via #231): `entry_path_problem`, one test of an entry's
  path for `check` and `execute`;
- b421e6b (#254 via #281): bump-leg.py and update-shape.py give the
  validators an explicit PYTHONIOENCODING (scripts/shape_materialize.py
  gains `captured_child_environment` and `inherited_child_environment`);
- c0ed615 (#242 via #308): family.py's `add` and `bump` under excludes;
- 71b58a1 (#262, #263 via #313): `check` finds, and `execute` refuses, a
  kept root path where the shape writes a directory or its `shape/`
  copy, and the materializer never writes over a copy.

It merged with no conflict. Only adopt-project.py changed on both sides.
Every other file the merge changes against this branch is main's, and
every file it changes against main is this branch's. In adopt-project.py
the merge's diff against this branch is main's own diff (559 lines), and
its diff against main is this branch's own diff (784 lines). Main's
twenty hunks there, by where they land:

1-2. the module docstring: `check` validates each entry's path and keeps
     no path in the root where the shape writes (6f45bf9, 71b58a1);
3. the `from shape_materialize import` block: `CASE_ASIDE`,
   `MOVE_THE_LEG`, `first_and_more` (71b58a1);
4-6. `_leg_findings`: a docstring line, the call to
     `_root_shape_findings`, and the new block after it (`ROOT_BLOCKS_SHAPE`,
     `ROOT_BLOCKS_COPY`, `_KeptTree`, `_shape_blocks`, `_blocked_sentence`,
     `_fix_the_kept_path`, `_root_shape_findings`) (71b58a1);
7. after `_submodule_findings`: `NOT_EXTRACTED`, `entry_path_problem`,
   `_misread`, `_refused_by_policy` and `_unsafe_path_findings` (6f45bf9);
8-11. `_entry_findings`: its docstring, and the entries' path findings
      first (6f45bf9);
12-13. `_extract_leg`: `literal:` on every `--paths-from-file` line
       (6f45bf9). This branch changed `_extract_leg` as well, further
       down: it reads the commit count and the digest before
       `_push_the_leg`. The two sets of lines do not touch, and the
       merged function has both;
14-15. `_mount_the_legs`: `git --literal-pathspecs rm` (6f45bf9);
16-17. `_leg_paths`: collected, not judged (6f45bf9);
18-20. `_refuse_what_check_finds`: its docstring and remediation
       (6f45bf9, 71b58a1).

The new findings of 6f45bf9 and 71b58a1 are `_entry_findings`, which
`cmd_execute` reaches only through `_refuse_what_check_finds`, before
`_create_leg_remotes`. So they are refused before any leg exists, and no
notice is due. The materializer's new `materialize-copy-blocked` is
raised after both legs are pushed, inside `_commit_the_split`, when a run
reaches the materializer without `check`'s finding. That makes it a late
failure, which this branch reports with the notice and exit 2. Main's
`test_with_check_skipped_execute_stops_at_the_copy_and_overwrites_nothing`
expects it to escape `cmd_execute`, so this merge breaks that test, as
9092fda's merge of #200 broke the mount-guard tests. The next commit
makes the test read the reported refusal.

No top-level name is bound twice in adopt-project.py, scaffold-project.py,
scripts/repo_shape.py, scripts/shape_materialize.py or tests/conftest.py.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…, #238, #196, #254, #242 and #262/#263 merge with three adopt-project.py hunks kept on both sides, and #262's `_CASE_ASIDE` import kept as the name's one binding

origin/main moved b30595c -> 71b58a1:
- be2790e (#204 via #227), update-shape.py;
- 6d8d88b (#203 via #236), family.py;
- ede15ce (#238 via #287), shape-doctor.py;
- 6f45bf9 (#196 via #231): adopt-project.py `entry_path_problem`,
  scripts/shape_materialize.py;
- b421e6b (#254 via #281): scripts/shape_materialize.py, bump-leg.py,
  update-shape.py;
- c0ed615 (#242 via #308), family.py;
- 71b58a1 (#262 and #263 via #313): adopt-project.py
  `_root_shape_findings` and scripts/shape_materialize.py
  `copy_in_the_way`. It fixes what this branch's pull request listed as
  follow-ups (i) and (ii): a source's own `shape/` where the shape
  writes its copy, and a plain FILE where the shape needs a directory.

Conflicts, all in adopt-project.py, each resolved by keeping both sides:
- The module docstring's `check` paragraph. 71b58a1 adds "no path kept
  in the root where the shape writes ..." before the submodule clause,
  and this branch adds the symlink clause after it. Both are kept, in
  that order.
- `_entry_findings`' docstring. 6f45bf9 and 71b58a1 give the order
  "each entry's path, then coverage, then each entry's `leg:`, the two
  mount paths and what the root keeps where the shape writes, then the
  source's own submodules". This branch adds "then each symlink the plan
  keeps in the root that a shape file cannot be written through".
- `_entry_findings`' return line. 6f45bf9 returns
  `unsafe_remediation + remediation`, and this branch appends
  `_root_symlink_findings(plan, source, tree)` to the findings. The
  merge returns both.

Found after the textual merge, and resolved here so that no commit binds
a name twice: 71b58a1 (through 65a9262) imports #198's phrase as
`_CASE_ASIDE` in the "#172's shape half" import block, and b37159a had
imported it on a line of its own. Main's import is kept; this branch's
line and its five-line comment go.

scripts/shape_materialize.py merges with no conflict. `place` gets
71b58a1's `copy_in_the_way` inside its collision block and this branch's
`_refuse_unwritable` after it, as 71b58a1's message foresaw.

Checked on the merged tree:
- `_entry_findings` still hands `_leg_findings` the tree, and the
  symlink findings still come last in its list.
- AST: no top-level name is bound twice in adopt-project.py,
  scaffold-project.py, scripts/repo_shape.py or
  scripts/shape_materialize.py, and none is loaded but bound nowhere.
  Main changed the signature of no function this branch calls.
- Both sides are intact by +/- line sets, apart from the three hunks and
  the import above.

The next two commits fix what this merge breaks, each with a revert
line. #262's rule and this branch's both name a symlink kept AT the
copy under `shape/`. And where the shape's copy lands is now
`copy_in_the_way`'s question, asked before this branch's own check of
it.

Lane: openreposhape-1 (openRepoShape-1)
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