Repository navigation
shape-doctor.py check_shape_currency takes every refusal update-shape.py prepare raises over the ROOT's own contracts/shape-pin.yaml (a merge conflict, a commit: that is not 40 hex, a deleted file) for this checkout failing to ask, so the verdict is CANNOT ANSWER, exit 3, outranking INVALID, and its NEXT git -C <standard> fetch --all blames the standard for a fault in the repository #237
Description
Lane: openreposhape-4 (openRepoShape-4)
Found by this lane's adversarial review of the #205 branch (50aa967, its SHOULD S1), re-verified on main a5b09c3 today, and again on 54aa69a (the one commit since, #173, changes adopt-project.py and its tests only, so every file named below is byte-identical at both). It is pre-existing on main and not a regression of that branch. Not claimed.
What is wrong. shape-doctor.py answers CANNOT ANSWER, exit 3, for a root whose own contracts/shape-pin.yaml does not load, and its NEXT sends the reader to git -C <standard> fetch --all. AGENTS.md (Checking a repository's compliance, item 1) says CANNOT ANSWER is "THIS CHECKOUT failing to ask, never a statement about their repository", and the doctor's own exit-code text (shape-doctor.py:186-190) limits it to "a pin naming a commit it does not carry". A pin file with a conflict marker in it is a statement about the repository. The verdict ladder tests cannot_answer before it reaches invalid_verdict (:3650-3652), so the one row that is wrong outranks the two that are right (pins, which runs the project's own validate-pins.py and names the real refusal; manifest kinds, which names shape-pin.yaml unreadable), and a reader who follows AGENTS.md reads exit 3 as "this checkout is at fault, not the repository" and runs a fetch that succeeds, changes nothing and leaves the next run identical.
# $U: a clean checkout at a5b09c3. gh is a PATH shim that logs its argv and exits 97 (the doctor's `machine` row asks `gh --version` and `gh auth status` and nothing else).
# Every repository is a bare repository under $R/remotes; no network.
python3 $U/scaffold-project.py --org testorg --project Atlas --elected-by 'Test Human' --elected-on 2026-09-02 \
--local-remote-dir $R/remotes --work-dir $R/work
git clone -q --recurse-submodules $R/remotes/Atlas.git $C
python3 $U/shape-doctor.py --root $C
# exit 0 COMPLIANT
# two branches each rewrite the pin's `commit:` line; the merge conflicts contracts/shape-pin.yaml and nothing else
git -C $C checkout -q -b edit-a main
sed -i -E 's|^commit: .*|commit: "1111111111111111111111111111111111111111"|' $C/contracts/shape-pin.yaml
git -C $C commit -q -m 'edit A' -- contracts/shape-pin.yaml
git -C $C checkout -q main && git -C $C checkout -q -b edit-b
sed -i -E 's|^commit: .*|commit: "2222222222222222222222222222222222222222"|' $C/contracts/shape-pin.yaml
git -C $C commit -q -m 'edit B' -- contracts/shape-pin.yaml
git -C $C merge edit-a # exit 1, CONFLICT; `git status --porcelain` is `UU contracts/shape-pin.yaml`
python3 $U/shape-doctor.py --root $C
# exit 3
# stderr REFUSED shape-doctor-cannot-answer: the copies cannot be compared: $C/contracts/shape-pin.yaml: line 30: merge keys are not supported
# Remediation: git -C $U fetch --all # this checkout of the standard does not carry the commit the pin names; fetch it, or run the doctor from a clone that has it
# rows ok manifest the project's own scripts/validate-manifest.py passes
# FINDING pins the project's own scripts/validate-pins.py refuses (exit 2): REFUSED yaml-unparsable: $C/contracts/shape-pin.yaml: line 30: merge keys are not
# NEXT python3 $C/scripts/validate-pins.py --root $C # read its output in full; it names what it refused
# FINDING manifest kinds unreadable as YAML: contracts/shape-pin.yaml
# FINDING shape currency the copies cannot be compared: $C/contracts/shape-pin.yaml: line 30: merge keys are not supported
# NEXT git -C $U fetch --all # this checkout of the standard does not carry the commit the pin names; ...
# verdict CANNOT ANSWER (shape currency: the copies cannot be compared: $C/contracts/shape-pin.yaml: line 30: merge keys are not supported) (exit 3)
python3 $U/shape-doctor.py --root $C --json # the shape-currency row's detail: {"error": "yaml-unparsable", "environment": true}
python3 $U/update-shape.py check --root $C --upstream $U
# exit 2 REFUSED yaml-unparsable: $C/contracts/shape-pin.yaml: line 30: merge keys are not supportedIt is not only a conflict. Each way the ROOT's pin can fail read_pin gives the same verdict, exit 3 and NEXT on a5b09c3 (one fixture per row, each edit made to a clean Atlas; the code is the row's detail.error):
the root's contracts/shape-pin.yaml |
refusal | doctor |
|---|---|---|
| holds a merge conflict | yaml-unparsable |
CANNOT ANSWER, exit 3 |
commit: "zzzz" |
update-pin-commit |
CANNOT ANSWER, exit 3 |
digest_definition: other-v9 |
update-pin-digest-definition |
CANNOT ANSWER, exit 3 |
files: [] |
update-pin-no-files |
CANNOT ANSWER, exit 3 |
| deleted | update-pin-missing |
CANNOT ANSWER, exit 3 |
| a list, not a mapping | update-pin-unreadable |
CANNOT ANSWER, exit 3 |
| a non-UTF-8 byte | yaml-unreadable |
CANNOT ANSWER, exit 3 |
commit: names 40 hex this checkout does not hold (control) |
update-revision-unresolvable |
CANNOT ANSWER, exit 3, and RIGHT: this is the case the code is written for |
On a5b09c3 a root whose project.yaml is conflicted too reads NOT A SHAPE ROOT, exit 2 (that is #205, fixed by PR #232). The review of #232's head (50aa967) reports that tree there as CANNOT ANSWER, exit 3, which is this verdict: after #232, the realistic two-shape-update merge, which conflicts both files, reaches exactly this issue. That run was the reviewer's and is not repeated here.
Why. check_shape_currency calls us.prepare(args) (shape-doctor.py:1097-1098) and catches Refusal (:1099) as one thing: a Row whose detail carries "environment": True (:1112) and whose NEXT is the fetch --all (:1108-1111). Its comment (:1100-1105) argues the case it was written for, a pin naming a commit THIS checkout does not carry. But prepare (update-shape.py:1278-1300) raises two kinds of refusal. open_upstream and Upstream.resolve raise about the STANDARD (update-revision-unresolvable, update-upstream-*), and read_pin (:364-405) and load_yaml raise about the ROOT (yaml-unparsable, yaml-unreadable, update-pin-missing, -unreadable, -commit, -digest-definition, -row, -no-files). Nothing in the except arm tells them apart. cannot_answer (:3502-3507) then returns the first row carrying environment, verdict_for returns exit 3 for it before any other rung (:3650-3652), and the stderr block (:3713-3717) prints the row's NEXT under REFUSED shape-doctor-cannot-answer.
What it should do. environment belongs to the refusals about the standard, and only to them. A refusal that names the ROOT's pin is a FINDING on shape currency with no environment, so the ladder reaches invalid_verdict (:3589-3607) and the verdict names the gates that are red. On a throwaway clone of a5b09c3, the one condition exc.code != "update-revision-unresolvable" (a plain FINDING otherwise) turned the conflict tree into INVALID (pins, manifest-kinds), exit 1, turned the commit: "zzzz" tree into INVALID (pins), exit 1, and left the unresolvable-commit control at CANNOT ANSWER, exit 3. That is a probe and not the fix; two choices are the author's. One is how to tell the two kinds apart: by code, or by splitting the root-side read out of prepare so the doctor can call the two halves separately (which keeps the knowledge in one place and survives a new refusal). The other is the NEXT for the root-side case, which should name the file that has to be fixed and the project's own scripts/validate-pins.py, as the pins row already does, and never fetch --all. The manifest kinds row's own NEXT for the same file (register the kind in shape-doctor.py's MANIFEST_VALIDATORS ... or remove the file from the root) fits a conflicted pin no better, and is a separate row's remedy, not part of this issue.
How to test. In tests/test_shape_doctor.py, beside test_a_pin_this_standard_cannot_resolve_is_exit_three (:2179), which must still pass unchanged: (a) a contracts/shape-pin.yaml holding a conflict marker, committed over a clean project fixture: exit 1, payload["verdict"] starts INVALID, the shape-currency row's detail has no environment, stderr holds neither shape-doctor-cannot-answer nor fetch --all, and the pins row still names validate-pins.py; (b) the same assertions parametrised over the other root-side breakages in the table (commit: not 40 hex, digest_definition, files: [], deleted, not a mapping, a non-UTF-8 byte), each with the row's detail.error asserted so the case under test is the case reached; (c) the control, the pin naming a commit the standard lacks, still exit 3 with fetch --all; (d) restoring the blanket environment: True makes (a) and (b) fail.
Related: #205 (PR #232; its description says a follow-up issue will carry this), #96
Activity
brettheap commentedon Oct 11, 2026
CLAIMED — lane openRepoShape-2, session 224cb7ce-54c8-4501-bfa8-7a7e398984a0@Eagle, 2026-10-11T22:15:41Z, for #237
Logged in
lanes/log/openRepoShape-2.mdat537011ad4c97b6faae80daf13151886183554f85.The three reads (lane-collision-protocol Rule 1):
README.md: take back the shape-pin sentence added to the exit-3 parag…
shape-doctor.py: a root's own `contracts/shape-pin.yaml` that does no…
tests/test_shape_doctor.py: a bad pin staged where HEAD holds none is…
shape-doctor.py: a merge's NEXT takes whole only the pin and the copi…
tests/test_shape_doctor.py: the doctor leaves a stat-dirty index byte…
Merge 8badae0 (#267 fix round 2) into fix/doctor-manifest-kinds-unrea…
shape-doctor.py: a pin or manifest kept as a symlink, or under one, i…
shape-doctor.py: lane 2's five NEXT tails from #267, `pin_restore`'s …
Merge origin/main 75052cb into fix/adopt-root-kept-symlinks: #188's r…
Merge main 75052cb into fix/adopt-plan-quote-yaml-keywords (#181)
shape-doctor.py: the `manifest kinds` row puts back a shape or leg pi…
shape-doctor.py: a NEXT is only the command, and the sentence that en…