Skip to content

adopt-project.py: Plan.__init__, allowed_empty_legs, names and open_source iterate or call .get on a hand-edited plan's pins:, allow_empty_legs:, paths:, legs: and source: without asking their type, so a scalar ends check and execute in a TypeError or AttributeError traceback, exit 1, and pins: openGlass passes check, adopts, and writes eight one-letter neutral-product pins that the project's own validate-pins.py refuses #386

Closed

Description

@brettheap

(lane openRepoShape-2, 2026-10-12T14:57Z)

Found by the writer of lane-2 PR #356 (the fix for #293 and #294), which fixes follow_ups: and elected_on: and says in its "Left out" section that pins: 5, allow_empty_legs: 5 and paths: end check in the same TypeError and "should be its own issue". Reproduced on origin/main ab623dd, and re-run on #356's head 5a5011d (ab623dd with #356 merged), where every row below except follow_ups: is the same. The rows for legs: and source:, and the string pins:, are this lane's own. Local remotes only. Not claimed.

What is wrong. Plan.load (adopt-project.py:1864) asks one question of a plan file, whether it is a mapping (plan-unreadable), and the constructor and four accessors read five keys of it without asking their type. A plan is a file a person or an AI edits between plan and execute on purpose (Plan.names' docstring says so), and check is the command that exists to say whether execute will accept the edit:

  • paths:: [e for e in (data.get("paths") or []) if isinstance(e, dict)] (Plan.__init__, :1858);
  • pins:: [str(p) for p in (data.get("pins") or []) if p] (:1861);
  • allow_empty_legs:: {str(role) for role in (self.data.get("allow_empty_legs") or []) if role} (allowed_empty_legs, :1920; the read :1925);
  • legs:: self.legs.get(role) (Plan.names, :1890), where the constructor has kept data.get("legs") or {} (:1860) whatever it is;
  • source:: (self.data.get("source") or {}).get(...) (source_commit, :1871, the read :1872; open_source, :1874, the read :1875).

x or [] rescues the falsy scalars (0, false, null), not the truthy ones. A truthy scalar (5, true, 1.5) is not iterable and has no .get, so check and execute both end in TypeError: 'int' object is not iterable ('bool', 'float') or AttributeError: 'int' object has no attribute 'get', exit 1, naming Python's error and not the key. Plan.load runs first in both commands, so no leg repository exists when they die. A non-empty string is iterable and is read one character at a time: pins: openGlass passes check (plan ok) and execute adopts and says adoption verified, and the root's project.yaml records neutral_product_pins: as eight pins, G, a, e, l, n, o, p and s, which nobody wrote. The adopted root's own scripts/validate-pins.py then refuses neutral-pin-missing for 'G'. (The list form, pins: [openGlass], ends the same way for openGlass, because adopt writes no contracts/<name>-pin.yaml; that is #273's defect, and this one is that a string is read as many names.)

# $U: a checkout of openRepoShape at ab623dd; git-filter-repo on PATH. $SRC: a three-file git repository (src/app.py, docs/spec.md, README.md), one commit. Local; no network.
python3 "$U/adopt-project.py" plan --source "$SRC" --project Northwind --org testorg --elected-by 'T H' --elected-on 2026-09-02 --out "$P"
# edit $P so that one top-level key reads `pins: 5` (its block replaced), then, for execute, point `local_path:` at a fresh copy of $SRC:
python3 "$U/adopt-project.py" check --plan "$P"
#   exit 1
#   Traceback: adopt-project.py line 1861, in __init__
#   TypeError: 'int' object is not iterable
python3 "$U/adopt-project.py" execute --plan "$P" --yes --local-remote-dir "$D/remotes" --work-dir "$D/work"
#   exit 1, the same traceback; ls "$D/remotes" is empty
# the same edit with `pins: openGlass`:
python3 "$U/adopt-project.py" check --plan "$P"            # exit 0, `plan ok`
python3 "$U/adopt-project.py" execute --plan "$P" --yes --local-remote-dir "$D/remotes" --work-dir "$D/work"   # exit 0, `adoption verified`
grep -A8 '^neutral_product_pins' "$D/work/Northwind/project.yaml"                  # - G / - a / - e / - l / - n / - o / - p / - s
( cd "$D/work/Northwind" && python3 scripts/validate-pins.py --root . )
#   REFUSED neutral-pin-missing: project.yaml declares a pin on 'G' but contracts/g-pin.yaml does not exist
the key, as edited into a plan plan wrote check --plan execute --plan --yes --local-remote-dir legs created
pins: 5 exit 1, TypeError: 'int' object is not iterable from Plan.__init__ :1861 exit 1, the same none
pins: true exit 1, TypeError: 'bool', :1861 exit 1, the same none
allow_empty_legs: 5 exit 1, TypeError: 'int' from allowed_empty_legs :1925 exit 1, the same none
allow_empty_legs: true exit 1, TypeError: 'bool', :1925 exit 1, the same none
paths: 5 exit 1, TypeError: 'int' from Plan.__init__ :1858 exit 1, the same none
paths: true exit 1, TypeError: 'bool', :1858 exit 1, the same none
legs: 5 exit 1, AttributeError: 'int' object has no attribute 'get' from Plan.names :1890 exit 1, the same none
source: 5 exit 1, AttributeError, from open_source :1875 exit 1, the same none
pins: openGlass (a string) exit 0, plan ok exit 0, adoption verified; neutral_product_pins: holds G a e l n o p s; the root's own validate-pins.py refuses neutral-pin-missing for 'G' both
follow_ups: 5 (the neighbour, #293) exit 1, TypeError from _plan_values :3913. On #356's head: FINDING plan-bad-follow-ups, exit 1 exit 1, the same. On #356's head: exit 2, REFUSED plan-bad-follow-ups none
pins: [], allow_empty_legs: [code] (controls) exit 0, plan ok exit 0, adoption verified both

Why. The constructor and the accessors treat a plan's values as the types plan writes. #356 fixes that for follow_ups: by reading it through one function that returns [] for anything but a list of text (_plan_follow_ups) and reporting what it left out as a finding (_follow_ups_findings, with _read_as to say "the number 5" or "the text 'abc'"), and notes that text on the key's own line "would be written one character per follow-up", which is the string row above for pins:. It touches neither Plan.__init__ nor allowed_empty_legs. The five keys here fail earlier than a finding can be built: the TypeError is raised by the constructor and the accessors themselves, before _entry_findings runs.

What it should do.

  1. check reports, and execute refuses before any leg exists, a plan whose pins: is not a list of text, allow_empty_legs: is not a list of leg names, paths: is not a list, legs: is not a mapping or source: is not a mapping. The finding names the key and what the reader gave it ("the number 5", "the text 'openGlass'", _read_as's words), check exits 1 and execute exits 2 under the same code (a code per key, as plan-bad-follow-ups is adopt-project.py: check finds, and execute refuses before any leg exists, a plan follow_ups: that is not a list of text and an elected_on: that election_date does not read, with or without a reference:, so follow_ups: 5 is no longer a traceback and elected_on: someday is no longer refused under a flag neither command takes (#293, #294) #356's for its key, or one code naming the key; the author's choice). Every bad key is listed in one run, not the first.
  2. A string is refused, never iterated. pins: openGlass must not plan ok and must not write eight one-letter pins. The same for allow_empty_legs: code, which allowed_empty_legs reads as four "legs" (c, o, d, e) that consent to nothing (read from the code, not run).
  3. No TypeError or AttributeError from a plan file. The reads at :1858, :1861, :1925, :1890 and :1875 must not iterate or call .get on what they have not checked; where the guard sits is the author's choice, with one constraint: shape-doctor.py --placement-plan writes legs: as a LIST of the mounted legs it audited, and tests/test_shape_doctor.py feeds such a plan through Plan and _leg_findings (:3453, :4127). A constructor that refused every non-mapping legs: would break that, so the legs: rule belongs where a plan FILE is loaded (Plan.load, :1864) or in names(), not in Plan.__init__ for every caller.
  4. Reuse adopt-project.py: check finds, and execute refuses before any leg exists, a plan follow_ups: that is not a list of text and an elected_on: that election_date does not read, with or without a reference:, so follow_ups: 5 is no longer a traceback and elected_on: someday is no longer refused under a flag neither command takes (#293, #294) #356's shape if it lands first: _read_as, and the finding plus _refuse_what_check_finds (:3415) route its plan-bad-follow-ups takes. If not, the two issues share one reader for "a plan key that must be a list" and whichever lands second imports it.

How to test. In tests/test_adopt_execute_refusals.py, where #356's follow_ups: tests sit and where the helpers are: the world fixture (:123, a source and an answered plan), write_edited (:183), run_check and check_findings (:196, :201), execute_args and refused_by (:147, :158), no_leg_was_created (:172), source_branches (:166), the adopter fixture (:105, which loads the module) and the autouse no_filter_repo_probe (:114, which stubs the filter-repo probe, so the execute cases need no git filter-repo). Edit the text of world.answered with the file's own habit (a regex over the key's block, as #356's with_follow_ups does).

  • (a) check on each of pins: 5, pins: true, pins: openGlass, allow_empty_legs: 5, allow_empty_legs: true, paths: 5, paths: true, legs: 5 and source: 5: exit 1, a FINDING that names the key and what it read, no Traceback, no plan ok;
  • (b) two bad keys in one plan: both are named in the one run;
  • (c) execute on each (through refused_by(adopter, execute_args(...))): the refusal carries the same code, no_leg_was_created(world.tmp) holds and source_branches(world.source) == ["main"];
  • (d) controls: a list of text, an empty list, a bare key and the key absent still pass for pins:, allow_empty_legs: (with spec_only_source, :209, for [code]) and paths:, and a plan whose legs: is the doctor's list still constructs as tests/test_shape_doctor.py:3453 needs;
  • (e) pins: openGlass is the one end-to-end case (needs git filter-repo, so needs_filter_repo as in tests/test_adopt_leg_paths.py:43): execute refuses and the --local-remote-dir is empty, where it adopted before.
    Reverting a key's guard fails that key's cases. A targeted run of changes to adopt-project.py includes tests/test_shape_doctor.py and tests/test_scaffold_leg_path_collisions.py (it guards the module's attributes).

Not in this issue. follow_ups: and elected_on: (#293, #294, PR #356). The list form's missing pin file (#273). paths: entries that are not mappings, which Plan.__init__ drops without a word (if isinstance(e, dict)). Other keys of the plan that are read with .get and compared (mode, kind), which already have refusals of their own.

Related: #293, #294 and PR #356 (the neighbouring keys; its "Left out" names this); #273 (--pin writes the name and no pin file); #261 and PR #314 (other values a plan carries that the project's own validators refuse).

Lane: openreposhape-2 (openRepoShape-2)

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-2, session 0279452a-ea8e-486f-bf99-698cb0a9bb2f@Eagle, 2026-10-12T15:33:54Z, for #386

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

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

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