Repository navigation
shape-doctor.py: for a root whose project.yaml does not load AND whose scripts/repo_shape.py is gone, the manifest row's NEXT promises its validator "refuses it the same way until it loads", but pasted it dies on ModuleNotFoundError: No module named 'repo_shape', exit 1, so the person is sent to a command that cannot say what the row quoted #298
Description
Lane: openreposhape-4 (openRepoShape-4)
Found by this lane's re-check of #238's fix round (head 88c81e0; its nit N2, which that review found in #232's row and left for a follow-up), and re-verified on main d94d155 (#232's squash, the commit this row lands on) with a scaffolded root and a doctor run, gh a shim that logs and exits 97. It is pre-existing and not caused by any open branch. Not claimed: the family row's version (the same function builds it, check_family, shape-doctor.py:1132-1138; not probed), PowerShell, and a root with no repo_shape.py AND no validator of its own (that line is the standard's template copy, which is #238's).
What is wrong. unloaded_manifest_row (shape-doctor.py:989-1036) is the manifest row for a project.yaml that is there and does not load (#205). It runs no validator: it quotes the loader's refusal that Context already caught and prints the gate to re-run, picked by validator_for (:1018), with a sentence promising what that gate does: "...; this refuses it the same way until it loads" (:1030-1036). A root that also lost its own scripts/repo_shape.py has a gate that cannot do that:
# $U: a clean checkout at d94d155. Atlas scaffolded from it and cloned to $C. Two copies of the clone with line 160 of project.yaml
# (` - role: spec`) given a tab for its indentation, so `project.yaml` does not load. In $C scripts/repo_shape.py was moved away; $K keeps it (the control).
cd / && env -u PYTHONPATH python3 $U/shape-doctor.py --root $C --json # the compound case
# verdict: DRIFTED (1 copy-missing) exit 1
# [manifest] FINDING :: project.yaml does not load: REFUSED yaml-unparsable: $C/project.yaml: line 160: ...
# NEXT: python3 $C/scripts/validate-manifest.py --root $C # the file to fix is $C/project.yaml, where the line above says; this refuses it the same way until it loads. What it should say there -- a merge conflict resolved, a hand edit put right -- is the person's call
# [shape currency] FINDING :: 10 unchanged / 0 upstream-changed / 0 locally-modified / 0 both / 0 upstream-added (...): scripts/repo_shape.py
sh -c "$NEXT" # the manifest row's NEXT, pasted from /, no PYTHONPATH
# Traceback (most recent call last):
# File "$C/scripts/validate-manifest.py", line 64, in <module>
# from repo_shape import ( # noqa: E402
# ModuleNotFoundError: No module named 'repo_shape' exit 1
cd / && env -u PYTHONPATH python3 $U/shape-doctor.py --root $K --json # the control: the same file, repo_shape.py kept
# verdict: INVALID (manifest) exit 1 the same row and the same NEXT with $K
sh -c "$NEXT"
# REFUSED yaml-unparsable: $K/project.yaml: line 160: tab used for indentation
# Remediation: run `git submodule update --init --recursive`, then `python3 scripts/validate-pins.py`. ... exit 2So in the compound case the pasted line neither "refuses it the same way" (exit 2, the loader's own refusal) nor tells the person anything about the manifest: it is a traceback about a missing module, which is the other problem, and the row has sent the person to it.
Why. Every row that RUNS a validator (validator_row, :928-965) shows what happens when the gate cannot import repo_shape: it runs the gate, gets exit 1 and a ModuleNotFoundError, and reports that as a finding. unloaded_manifest_row cannot, because it never runs the gate, and its sentence (:1033-1034) states as fact what depends on a file it never checks for (the own validators import repo_shape from beside themselves: validate-manifest.py:64 in the project's copy). The compound case is not an exotic one: a project.yaml left unparsable by a merge conflict is the very case the row exists for (#205), and scripts/repo_shape.py is the file the shape currency row reports as copy-missing when a root has lost it. The verdict is DRIFTED (1 copy-missing) by design: "Drift outranks a red validator on that line for one concrete reason: an edited shape copy is exactly what makes validate-pins.py red" (the module docstring, :206-208; verdict_for's docstring, :4027-4030, says the same), so the verdict line sends the reader to update-shape.py and the manifest row's NEXT sends them to the gate: two different first moves for one root, and only the first can work.
#238 (not on main yet) changes this row to print its command through validator_command, and adds a docstring sentence that is true of validator_row and not of this row: "A root that has lost that file has a gate that dies on the import, pasted or not, and the row says so as a finding, because the doctor ran it the same way" (validator_command, :1006-1008 on 88c81e0).
What it should do. The row does not promise what its command cannot do, and a root in this state is told the first thing to fix. The decisions are the author's:
- Where the fact lives.
validator_foralready decides own against template and shape-doctor.pyvalidator_rowfalls back to the standard's TEMPLATE validator when a root carries no copy of its own, and prints that path as the NEXT command without thePYTHONPATHit runs it under, so pasting themanifest,pinsorfamilyrow's NEXT dies withModuleNotFoundError: No module named 'repo_shape'and exit 1, the code a validator uses for a finding #238 makes it returnown_copy; whether the validator can importrepo_shapefrom beside itself is one more fact of the same kind (for an own copy:(used.parent / "repo_shape.py").is_file()), decided once there and handed to both the run and the line, not tested a second time in this row (shape-doctor.pyvalidator_rowfalls back to the standard's TEMPLATE validator when a root carries no copy of its own, and prints that path as the NEXT command without thePYTHONPATHit runs it under, so pasting themanifest,pinsorfamilyrow's NEXT dies withModuleNotFoundError: No module named 'repo_shape'and exit 1, the code a validator uses for a finding #238's review showed by mutation that a second inline test of the same file can drift from the first). - What the row says then. Either its sentence changes to what is true ("the gate cannot run until
scripts/repo_shape.pyis back; theshape currencyrow names the command that restores it"), with the NEXT pointing at that restore, or the command is kept and the "refuses it the same way" promise is made only when it can be kept. - The order the row hands over. The documented precedence already says the missing file is the first fix, so the row's NEXT should agree with it: restore the file, then re-run the gate; not whether the verdict changes (it should not, by the ruling above).
How to test. In tests/test_shape_doctor.py, with the fixtures #205/#232's tests use for an unloadable manifest: remove scripts/repo_shape.py from the root and RUN the manifest row's NEXT from a directory with no PYTHONPATH (the existing assert_next_runs_the_roots_own, tests/test_shape_doctor.py:1088, reads the line's words and never runs it, so no test sees the difference); assert that the run exits as the row says it will, or that the row says why it will not. The control (the file kept) keeps exit 2 and REFUSED yaml-unparsable.
Related: #205 and PR #232 (the row), #238 (its review's N2 and O6; the issue and its fix round change this row's builder), and the issue on NEXT lines ending in # <comment> tails (#277: this row's promise is such a tail, and a pasted line reads it as arguments in an interactive zsh).
Activity
brettheap commentedon Oct 12, 2026
CLAIMED — lane openRepoShape-4, session 3431c370-df80-4c11-a041-41bce42d8b38@Eagle, 2026-10-12T11:07:42Z, for #298
Logged in
lanes/log/openRepoShape-4.mdatb5bd484e5d5808472f60de8250408bb8ca1bda78.The three reads (lane-collision-protocol Rule 1):
shape-doctor.py: the CANNOT ANSWER block prints `Remediation:` only f…
Merge origin/main (doctor A, #354) into fix/doctor-validator-cannot-run
Merge #357's head e44d860 (fix/doctor-validator-cannot-run) into fix/…
Merge origin/main dd8d0ea into fix/adopt-root-kept-symlinks: #261's `…
shape-doctor.py: a NEXT is only the command, and the sentence that en…