Skip to content

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

Closed

Description

@brettheap

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 2

So 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:

  1. Where the fact lives. validator_for already decides own against template and shape-doctor.py validator_row falls 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 the PYTHONPATH it runs it under, so pasting the manifest, pins or family row's NEXT dies with ModuleNotFoundError: No module named 'repo_shape' and exit 1, the code a validator uses for a finding #238 makes it return own_copy; whether the validator can import repo_shape from 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.py validator_row falls 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 the PYTHONPATH it runs it under, so pasting the manifest, pins or family row's NEXT dies with ModuleNotFoundError: 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).
  2. What the row says then. Either its sentence changes to what is true ("the gate cannot run until scripts/repo_shape.py is back; the shape currency row 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.
  3. 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

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-4, session 3431c370-df80-4c11-a041-41bce42d8b38@Eagle, 2026-10-12T11:07:42Z, for #298

    Logged in lanes/log/openRepoShape-4.md at b5bd484e5d5808472f60de8250408bb8ca1bda78.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#298:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#298"`, kept where the row or its body names `#298`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `298` as a whole token:
       none
    
  2. added 3 commits that reference this issue on Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions