Skip to content

update-shape.py read_pin and the pinned validate-pins.py and validate-family.py: iterate pin.get("files") or [] without asking for a list, so a shape pin with files: 5 or files: true ends shape-doctor.py, update-shape.py check and apply and the project's own validator in TypeError: ... is not iterable, exit 1, with no refusal naming the pin #290

Closed

Description

@brettheap

(lane openRepoShape-2, 2026-10-12T00:11:24Z)

Found by this lane's adversarial review of PR #267 at 092a5f9 (the fix for #237), its follow-up FU1. It is the same on main d94d155, where it was reproduced again today on a scaffolded project and on a family holder: #267 neither causes it nor makes it worse, and its new except Refusal cannot see a TypeError. Not claimed.

What is wrong. Every reader of contracts/shape-pin.yaml takes pin.get("files") or [] and loops over it. or [] rescues the falsy values (0, false, null, an empty list), which then reach the no files refusal or finding. A truthy scalar is not rescued: files: 5, files: true and files: 1.5 are iterated, and Python raises TypeError: 'int' object is not iterable (or 'bool', 'float'). The run ends in a traceback, exit 1, with no refusal that names the pin. shape-doctor.py prints nothing on stdout, so the operator gets no report at all, and the doctor's one job is to say what is wrong.

# $U: a checkout of openRepoShape at d94d155. gh is a PATH shim that logs its argv and exits 97 (the doctor asks it only `--version` and `auth status`). Every repository is local; 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 -c protocol.file.allow=always clone -q --recurse-submodules "$R/remotes/Atlas.git" "$C"
python3 "$U/shape-doctor.py" --root "$C"                              # exit 0   COMPLIANT   (all four tools below are exit 0 here)
# `files:` is the last key of the pin; replace its rows with a scalar and commit
python3 - <<'PY'
p = "contracts/shape-pin.yaml"; t = open(p, encoding="utf-8").read(); i = t.index("\nfiles:\n") + 1
open(p, "w", encoding="utf-8", newline="\n").write(t[:i] + "files: 5\n")
PY
git -C "$C" commit -q -m 'files: 5' -- contracts/shape-pin.yaml
python3 "$U/shape-doctor.py" --root "$C"                              # exit 1, stdout empty
python3 "$U/update-shape.py" check --root "$C" --upstream "$U"        # exit 1
python3 "$U/update-shape.py" apply --root "$C" --upstream "$U"        # exit 1
python3 "$C/scripts/validate-pins.py" --root "$C"                     # exit 1
#   each:  TypeError: 'int' object is not iterable
#     doctor / check / apply:  update-shape.py line 406, in read_pin    for raw in pin.get("files") or []:
#                              (via prepare, line 1379; the doctor's frame is shape-doctor.py line 1455, in check_shape_currency)
#     validate-pins.py:        line 346, in _check_shape_pin            for row in files:
the pin's files: update-shape.py check / apply shape-doctor.py the root's validate-pins.py
5 exit 1, TypeError: 'int' exit 1, TypeError: 'int', no report exit 1, TypeError: 'int'
true exit 1, TypeError: 'bool' exit 1, TypeError: 'bool', no report exit 1, TypeError: 'bool'
1.5 exit 1, TypeError: 'float' exit 1, TypeError: 'float', no report exit 1, TypeError: 'float'
0, [] (control) exit 2, REFUSED update-pin-no-files exit 3, CANNOT ANSWER, a report exit 1, a finding, no traceback
abc (control) exit 2, REFUSED update-pin-row (each character is not a mapping) exit 3, CANNOT ANSWER, a report exit 1, a finding, no traceback

The same pin in a family holder (family.py init --org testorg --family Fam --created-by 'Test Human' --created-on 2026-09-04 --local-remote-dir "$R/remotes" --work-dir "$R/fam", then files: 5 committed) ends scripts/validate-family.py --root . in TypeError: 'int' object is not iterable at its :506 (_check_shape_pin_files, via :522, :636, :668), exit 1, and shape-doctor.py --root . and update-shape.py check in the read_pin traceback above. (Exit 3 for the controls is #237's verdict for a refused root pin, not part of this issue.)

Why. Four places read the files: key as a list without asking whether it is one.

  • update-shape.py:406, in read_pin (:375-418): for raw in pin.get("files") or []:. read_pin is reached from prepare (:1379), so from check, apply and shape-doctor.py's check_shape_currency (:1455), whose handler catches Refusal only (:1456).
  • templates/assembly-root/scripts/validate-pins.py:341-346, in _check_shape_pin: files = pin.get("files") or [], if not files:, for row in files:. This is a pinned template copy (scripts/validate-pins.py, COPIED_VERBATIM, scripts/shape_materialize.py:296) that every scaffolded and adopted assembly root carries.
  • templates/family-root/scripts/validate-family.py:501-506, in _check_shape_pin_files: the same three lines. A pinned template copy (FAMILY_COPIED_VERBATIM, :382) that every family holder carries.
  • shape-doctor.py:2788, in pinned_paths (called at :2805 from check_agent_files, only when an agent file is missing): {str(row.get("path")) for row in (pin.get("files") or []) ...}. Calling it directly on the files: 5 tree raises the same TypeError. It is masked on a run today, because shape currency crashes first; once read_pin refuses, a root with a scalar pin and no AGENTS.md or CLAUDE.md would reach it.

A search of the non-test Python for get("files") and ["files"] finds only these. adopt-project.py:3497 checks isinstance(rows, list) and is fine.

What it should do.

  1. read_pin refuses update-pin-row when files: is present and is not a list, naming the pin and the key (files: 5 is not a list of path:/sha256: rows). The empty and missing cases keep update-pin-no-files. Whether files: 0 and files: false move from update-pin-no-files to the new refusal is the author's call; today they are refused, which is all this asks.
  2. validate-pins.py and validate-family.py report a finding the way they already report shape-pin-row for a row that is not a mapping (validate-pins.py:311, validate-family.py:476), exit 1, with no traceback.
  3. pinned_paths returns the empty set for a files: that is not a list, as it already does for a pin that is not a mapping.

Cost, so the PR can say it. Item 2 touches two pinned template copies, and a change to either changes the bytes a fresh scaffold, adoption or family.py init records. The sha256 rows for scripts/validate-pins.py and scripts/validate-family.py in each project's contracts/shape-pin.yaml therefore move with the templates: a new project records the new digest, and every project already cut reads the file as upstream-changed in update-shape.py check until its owner runs apply. A search of tests/ for a literal 64-hex digest finds none, so no fixture hardcodes the old one. update-shape.py and shape-doctor.py are not pinned copies. If the fix puts a shared helper in scripts/repo_shape.py, that file is a pinned copy too (COPIED_FROM_SHAPE), and the same applies.

How to test. Parametrise over files: 5, files: true and files: 1.5, each committed over a clean scaffold (tests/conftest.py's project fixture):

When #267 lands, add files: 5 to its ROOT_PIN_FAULTS. Reverting any one of the four guards fails its own case.

Related: #237 and PR #267 (the doctor's handling of a root pin that does not read, whose table of faults this extends); #132 (the per-row split in validate-pins.py that this belongs beside).

🤖 Generated with Claude Code

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-2, session 5b7b5f3d-dce7-452d-a33d-ab8537d91db3@Eagle, 2026-10-12T11:00:14Z, for #290

    Logged in lanes/log/openRepoShape-2.md at b60603c40e5f433d4ce17dc190c682b464f4d69b.

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

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#290:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#290"`, kept where the row or its body names `#290`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `290` as a whole token:
       none
    
  2. added a commit that references this issue on Oct 12, 2026
  3. added 5 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