Skip to content

validate-repository-naming.py (_pins_from_project :302, _chain_from_leg :318), validate-manifest.py (_declared_pins :85) and validate-pins.py (PinSources :390, called at :698) iterate the project's own neutral_product_pins: and a leg's naming.referent_chain: without asking they are lists, so 5 (or true, 1.5) in either field ends the naming gate in TypeError: 'int' object is not iterable, exit 1, neutral_product_pins: 5 ends the other two validators the same way before validate-manifest.py reaches the manifest-neutral-product-pins finding it has for it, and make validate stops in a traceback at its first command; a scalar openXdox in neutral_product_pins: is read as the characters X, d, e, n, o, p, x by the naming gate, which classifies and exits 0 #443

Closed

Description

@brettheap

Lane: openreposhape-5 (openRepoShape-5)

(lane openRepoShape-5, 2026-10-12T18:05:40Z)

Found by lane 5's review of fix/link-source-values (the fix for #338) at 40762ca, whose SHOULD-1 was this class in a LINK's manifest (neutral_product_pins: 5 out of link_declarations; fixed at 4b21263), and then by the writer of that fix round, who ran the same value on the project's OWN manifest at 4b21263 and listed it as its candidate D: --project on a manifest with neutral_product_pins: 5 gave the traceback, exit 1, and a scalar openXdox read as characters and classified, exit 0. The writer did not check what the project's own validate-manifest.py says first. A second lane-5 review of 4b21263 did, and found that validate-manifest.py ends in the same TypeError, and that a leg's naming.referent_chain: 5 crashes the naming gate the same way; both are in this issue. It is pre-existing and outside #338, which is about the OTHER trees a chain reads (a link's manifest, link_declarations) and not the project's own declarations. The reproduction below was run on origin/main 45e01f4 (the neutral_product_pins: rows were run earlier on cd1fc79, which differs from it only by an added tests/test_agents_md_codes.py, with the same answers) and on a merge of 4b21263 with main: _pins_from_project and _chain_from_leg are unchanged there (:507 and :523, the second reached first, from _project_chain_links :593). Not claimed.

What is wrong. The project's own declarations are read by four unguarded iterations in three validators, each of the form for x in (value or []), which iterates whatever is there:

  • scripts/validate-repository-naming.py:302, _pins_from_project, reads neutral_product_pins: (called from _targets_from_project, :377).
  • scripts/validate-repository-naming.py:315-318, _chain_from_leg, reads a leg's naming.referent_chain: (called from _leg_target, :337). It has the rule for a string (:316-317, one link) and none for anything else that is not a list.
  • templates/assembly-root/scripts/validate-manifest.py:85, _declared_pins, reads neutral_product_pins: (called from _findings, :658).
  • templates/assembly-root/scripts/validate-pins.py:697-698: products = manifest.get("neutral_product_pins") or [] and then PinSources(pin_source_values, products), whose __init__ iterates it (:390).

The naming gate is both the standard's scripts/validate-repository-naming.py and the project's own copy of it.

A value that is not iterable (5, true, 1.5) raises TypeError. A string in neutral_product_pins: is iterable, so openXdox is read one character at a time.

  • neutral_product_pins:, the manifest validator's finding is never reached. validate-manifest.py has the refusal for both kinds of value: _neutral_product_pins_type_finding (:501-512) says FINDING manifest-neutral-product-pins: neutral_product_pins is <value>, expected a list of neutral product names for anything that is not None and not a list. But _findings calls _declared_pins(manifest) (:658) one line BEFORE it calls that function (:659), so for the values that crash, the finding built for them is never reached and the project's own manifest validator ends in the same traceback. tests/test_manifest_validator.py:286 (test_neutral_product_pins_must_be_a_list) pins only the string openChart, the value that does not crash.
  • referent_chain:, the manifest validator is already right and is not reached. _recorded_chain (validate-manifest.py:88-100) reads a string as a one-link chain and anything that is not a list as (), and _naming_chain_field_finding (:119-131) says FINDING naming-referent-chain: leg 'assembly': referent_chain is 5, expected a list of neutral-product names (tests/test_manifest_validator.py:441, test_a_chain_record_that_is_not_a_list_is_a_finding, value 7). The naming gate's _chain_from_leg is the copy of the same reader that has the string rule and not the list rule.

The order the gates run in decides what a person sees first, and it is the naming gate: make validate runs naming, manifest, pins (templates/assembly-root/Makefile:46-49) and stops at the first failure; validate.yml (:187, :190, :200) and bootstrap.py's step (b) (VALIDATORS, :76-79) run them in the same order. So for either field the first thing a person reads is the naming gate's traceback, exit 2 from make, and the validator that has a sentence for the field (validate-manifest.py) is never run by make validate. bootstrap.py runs all three and prints every traceback before bootstrap FAILED: ....

# usage: bash non-list-fields.sh "$U"      ($U: a checkout of openRepoShape)
# Local remotes only; the real `gh` is kept out of reach.
U=$1
STUB=$(mktemp -d); printf '#!/bin/sh\nexit 97\n' > "$STUB/gh"; chmod +x "$STUB/gh"
export PATH="$STUB:$PATH" GH_TOKEN=bogus GH_CONFIG_DIR=$(mktemp -d) PYTHONDONTWRITEBYTECODE=1
export GIT_AUTHOR_NAME=T GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=T GIT_COMMITTER_EMAIL=t@example.invalid
D=$(mktemp -d)
python3 "$U/scaffold-project.py" --org testorg --project Atlas --elected-by 'T H' --elected-on 2026-09-02 \
    --local-remote-dir "$D/remotes" --work-dir "$D/work" > /dev/null; echo "scaffold exit $?"
git -c protocol.file.allow=always clone -q --recurse-submodules "$D/remotes/Atlas.git" "$D/Atlas" && cd "$D/Atlas" || exit 1
grep -n '^neutral_product_pins' project.yaml
set_field() {   # set_field pins|chain VALUE: reset project.yaml, then put VALUE in the field
  git checkout -q -- project.yaml
  if [ "$1" = pins ]; then sed -i "s/^neutral_product_pins: \[\]\$/neutral_product_pins: $2/" project.yaml
  else sed -i "0,/^      also_matches: \[\]\$/s//      also_matches: []\n      referent_chain: $2/" project.yaml; fi
}
for field in pins chain; do
  for value in '[]' 5 openXdox; do
    set_field $field "$value"
    [ $field = pins ] && name=neutral_product_pins || name="the assembly leg's naming.referent_chain"
    echo "=== $name: $value"
    for cmd in "scripts/validate-repository-naming.py --project project.yaml" scripts/validate-manifest.py scripts/validate-pins.py; do
      python3 $cmd > "$D/o" 2> "$D/e"; rc=$?
      echo "  $cmd: exit $rc, Traceback: $(grep -c '^Traceback' "$D/e"), last line: $(cat "$D/e" "$D/o" | grep -v '^$' | tail -1 | cut -c1-110)"
    done
    make validate > "$D/o" 2> "$D/e"; echo "  make validate: exit $?, stopped in: $(grep '^python3' "$D/o" | tail -1)"
  done
done
for value in openXdox 5; do   # the doctor, over each field with each value
  for field in pins chain; do
    set_field $field "$value"
    python3 "$U/shape-doctor.py" --root "$D/Atlas" --json 2>/dev/null | python3 -c '
import json, sys
p = json.load(sys.stdin); rows = {x["id"]: x for x in p["rows"]}
print("doctor,", sys.argv[1], "=", sys.argv[2] + ":", p["verdict"], "exit", p["exit"])
for k in ("naming", "manifest", "pins"): print("   ", k, rows[k]["status"], rows[k]["reason"][:120])' "$field" "$value"
  done
done

Observed on origin/main 45e01f4, and unchanged on 3ef059c, which adds #314's changes to scaffold-project.py and adopt-project.py (Linux, git 2.43.0, Python 3.12.3; <T> is the temporary directory):

scaffold exit 0
97:neutral_product_pins: []
=== neutral_product_pins: []
  scripts/validate-repository-naming.py --project project.yaml: exit 0, Traceback: 0, last line:   Atlas-code                       project-leg/code
  scripts/validate-manifest.py: exit 0, Traceback: 0, last line: manifest ok: Atlas (atlas), 3 legs
  scripts/validate-pins.py: exit 0, Traceback: 0, last line: pins ok
  make validate: exit 0, stopped in: python3 scripts/validate-pins.py
=== neutral_product_pins: 5
  scripts/validate-repository-naming.py --project project.yaml: exit 1, Traceback: 1, last line: TypeError: 'int' object is not iterable
  scripts/validate-manifest.py: exit 1, Traceback: 1, last line: TypeError: 'int' object is not iterable
  scripts/validate-pins.py: exit 1, Traceback: 1, last line: TypeError: 'int' object is not iterable
  make validate: exit 2, stopped in: python3 scripts/validate-repository-naming.py --project project.yaml
=== neutral_product_pins: openXdox
  scripts/validate-repository-naming.py --project project.yaml: exit 0, Traceback: 0, last line:   Atlas-code                       project-leg/code
  scripts/validate-manifest.py: exit 1, Traceback: 0, last line: 1 finding(s) in <T>/Atlas/project.yaml
  scripts/validate-pins.py: exit 2, Traceback: 0, last line: Remediation: `scaffold-project.py --pin o@<commit>` writes that file. A declaration in project.yaml with no pi
  make validate: exit 2, stopped in: python3 scripts/validate-manifest.py
=== the assembly leg's naming.referent_chain: []
  scripts/validate-repository-naming.py --project project.yaml: exit 0, Traceback: 0, last line:   Atlas-code                       project-leg/code
  scripts/validate-manifest.py: exit 0, Traceback: 0, last line: manifest ok: Atlas (atlas), 3 legs
  scripts/validate-pins.py: exit 0, Traceback: 0, last line: pins ok
  make validate: exit 0, stopped in: python3 scripts/validate-pins.py
=== the assembly leg's naming.referent_chain: 5
  scripts/validate-repository-naming.py --project project.yaml: exit 1, Traceback: 1, last line: TypeError: 'int' object is not iterable
  scripts/validate-manifest.py: exit 1, Traceback: 0, last line: 1 finding(s) in <T>/Atlas/project.yaml
  scripts/validate-pins.py: exit 0, Traceback: 0, last line: pins ok
  make validate: exit 2, stopped in: python3 scripts/validate-repository-naming.py --project project.yaml
=== the assembly leg's naming.referent_chain: openXdox
  scripts/validate-repository-naming.py --project project.yaml: exit 0, Traceback: 0, last line:   Atlas-code                       project-leg/code
  scripts/validate-manifest.py: exit 0, Traceback: 0, last line: manifest ok: Atlas (atlas), 3 legs
  scripts/validate-pins.py: exit 0, Traceback: 0, last line: pins ok
  make validate: exit 0, stopped in: python3 scripts/validate-pins.py
doctor, pins = openXdox: INVALID (manifest, pins) exit 1
    naming ok every leg name classifies, and each declared role agrees with its form
    manifest FINDING the project's own scripts/validate-manifest.py reports a finding (exit 1): FINDING manifest-neutral-product-pins: neutra
    pins FINDING the project's own scripts/validate-pins.py refuses (exit 2): REFUSED neutral-pin-missing: project.yaml declares a pin on
doctor, chain = openXdox: COMPLIANT exit 0
    naming ok every leg name classifies, and each declared role agrees with its form
    manifest ok the project's own scripts/validate-manifest.py passes
    pins ok the project's own scripts/validate-pins.py passes
doctor, pins = 5: INVALID (naming, manifest, pins) exit 1
    naming FINDING the naming policy is not satisfied (exit 1): TypeError: 'int' object is not iterable
    manifest FINDING the project's own scripts/validate-manifest.py reports a finding (exit 1): TypeError: 'int' object is not iterable
    pins FINDING the project's own scripts/validate-pins.py reports a finding (exit 1): TypeError: 'int' object is not iterable
doctor, chain = 5: INVALID (naming, manifest) exit 1
    naming FINDING the naming policy is not satisfied (exit 1): TypeError: 'int' object is not iterable
    manifest FINDING the project's own scripts/validate-manifest.py reports a finding (exit 1): FINDING naming-referent-chain: leg 'assembly'
    pins ok the project's own scripts/validate-pins.py passes

The traceback of the naming gate, for each field (the standard's copy and the project's copy are the same at the same lines):

  File ".../scripts/validate-repository-naming.py", line 377, in _targets_from_project
    pins = _pins_from_project(data)
  File ".../scripts/validate-repository-naming.py", line 302, in _pins_from_project
    return {str(pin) for pin in (data.get("neutral_product_pins") or []) if pin}
TypeError: 'int' object is not iterable

  File ".../scripts/validate-repository-naming.py", line 337, in _leg_target
    target = Target(name, declared, pins, _chain_from_leg(leg), root)
  File ".../scripts/validate-repository-naming.py", line 318, in _chain_from_leg
    return tuple(str(link) for link in (recorded or []) if str(link).strip())
TypeError: 'int' object is not iterable

and the one in the project's own validate-manifest.py for neutral_product_pins: 5, which has the finding for the value and dies first:

  File ".../scripts/validate-manifest.py", line 658, in _findings
    pins = _declared_pins(manifest)
  File ".../scripts/validate-manifest.py", line 85, in _declared_pins
    return {str(pin) for pin in (manifest.get("neutral_product_pins") or []) if pin}
TypeError: 'int' object is not iterable

What the readers that take the dict do with each value, on the pure functions (no project, no files):

# usage: python3 values.py "$U"      ($U: a checkout of openRepoShape; pure functions, no files, no network, no gh)
import importlib.util, sys
from pathlib import Path

U = Path(sys.argv[1]).resolve()
sys.dont_write_bytecode = True
sys.path.insert(0, str(U / "scripts"))
sys.path.insert(0, str(U / "templates" / "assembly-root" / "scripts"))


def load(name, path):
    spec = importlib.util.spec_from_file_location(name, path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


naming = load("naming_gate", U / "scripts" / "validate-repository-naming.py")
manifest = load("manifest_gate", U / "templates" / "assembly-root" / "scripts" / "validate-manifest.py")
VALUES = [("5", 5), ("true", True), ("1.5", 1.5), ("openXdox", "openXdox"), ("{a: b}", {"a": "b"}),
          ("0", 0), ("false", False), ("''", ""), ("[]", []), ("null", None), ("[openXdox]", ["openXdox"])]


def show(read, value, key="neutral_product_pins", nested=False):
    data = {key: value}
    try:
        return "reads " + repr(sorted(read({"naming": data} if nested else data)))
    except Exception as exc:  # the point of the probe
        return f"raises {type(exc).__name__}: {exc}"


print(f"{'neutral_product_pins:':23}{'validate-repository-naming.py _pins_from_project':58}validate-manifest.py _declared_pins")
for label, value in VALUES:
    print(f"{label:23}{show(naming._pins_from_project, value):58}{show(manifest._declared_pins, value)}")

print()
print(f"{'referent_chain:':23}{'validate-repository-naming.py _chain_from_leg':58}validate-manifest.py _recorded_chain")
for label, value in VALUES:
    print(f"{label:23}{show(naming._chain_from_leg, value, 'referent_chain', True):58}{show(manifest._recorded_chain, value, 'referent_chain')}")

Observed on origin/main 45e01f4, and identical on a merge of 4b21263 with it:

neutral_product_pins:  validate-repository-naming.py _pins_from_project          validate-manifest.py _declared_pins
5                      raises TypeError: 'int' object is not iterable            raises TypeError: 'int' object is not iterable
true                   raises TypeError: 'bool' object is not iterable           raises TypeError: 'bool' object is not iterable
1.5                    raises TypeError: 'float' object is not iterable          raises TypeError: 'float' object is not iterable
openXdox               reads ['X', 'd', 'e', 'n', 'o', 'p', 'x']                 reads ['X', 'd', 'e', 'n', 'o', 'p', 'x']
{a: b}                 reads ['a']                                               reads ['a']
0                      reads []                                                  reads []
false                  reads []                                                  reads []
''                     reads []                                                  reads []
[]                     reads []                                                  reads []
null                   reads []                                                  reads []
[openXdox]             reads ['openXdox']                                        reads ['openXdox']

referent_chain:        validate-repository-naming.py _chain_from_leg             validate-manifest.py _recorded_chain
5                      raises TypeError: 'int' object is not iterable            reads []
true                   raises TypeError: 'bool' object is not iterable           reads []
1.5                    raises TypeError: 'float' object is not iterable          reads []
openXdox               reads ['openXdox']                                        reads ['openXdox']
{a: b}                 reads ['a']                                               reads []
0                      reads []                                                  reads []
false                  reads []                                                  reads []
''                     reads []                                                  reads []
[]                     reads []                                                  reads []
null                   reads []                                                  reads []
[openXdox]             reads ['openXdox']                                        reads ['openXdox']

Together with the CLI runs above and validate-manifest.py measured on each value in a scaffolded project, that gives:

neutral_product_pins: _pins_from_project (naming) _declared_pins (manifest) _neutral_product_pins_type_finding
5, true, 1.5 TypeError ('int', 'bool', 'float') the same would be a finding; never reached
openXdox reads {'X', 'd', 'e', 'n', 'o', 'p', 'x'} the same FINDING manifest-neutral-product-pins (reached)
{a: b} reads {'a'} the same a finding (reached)
0, false, '' reads set() the same a finding (0 is not None; reached)
[], null set() the same none
[openXdox] {'openXdox'} the same none
referent_chain: _chain_from_leg (naming) _recorded_chain (manifest) _naming_chain_field_finding
5, true, 1.5 TypeError reads () FINDING naming-referent-chain (reached)
openXdox one link, openXdox the same none
{a: b} reads the KEY a as a one-link chain reads () FINDING naming-referent-chain
0, false () () FINDING naming-referent-chain
'', [], null () () none
[openXdox] openXdox the same none

On validate-pins.py a string in neutral_product_pins: is read the same way and the first character is the finding: REFUSED neutral-pin-missing: project.yaml declares a pin on 'o' but contracts/o-pin.yaml ...; it does not read referent_chain:. The doctor reports each crash as "reports a finding (exit 1)" with the TypeError line as the quoted finding (PR #357 teaches the manifest and pins rows to say a validator crashed, #278; the naming row reads only the exit code, #410). With referent_chain: 5 it says INVALID (naming, manifest): the naming row quotes the TypeError, and the manifest row quotes the right sentence.

Why. YamlError's docstring is the standard's contract for these files: "Raising rather than guessing is the whole point: a validator that silently mis-reads its own contract file is worse than one that will not start." A traceback is a validator that will not start without saying why, and a manifest the project's own gate calls a list of characters, or a mapping whose keys are a chain, is the silent misreading. The standard already decided the first half: validate-manifest.py has a finding for a non-list neutral_product_pins: and one for a non-list referent_chain:, and #338's fix gives the same value in a LINK's manifest the answer "a tree that cannot answer, named with its reason" (link_declarations). The project's own declarations are the places the idiom is left, and the order of the gates makes the naming gate's traceback the first thing read.

What it should do. The author decides; a value that is not a list must end in a sentence naming the field and no traceback, in every validator that reads it, and the sentence a person reads first should be the right one:

  1. validate-manifest.py reaches its own finding for neutral_product_pins:. _declared_pins returns an empty set for a value that is not a list (or _findings calls _neutral_product_pins_type_finding first and returns early), so 5 gives FINDING manifest-neutral-product-pins: neutral_product_pins is 5, expected a list of neutral product names as openXdox does. referent_chain: needs nothing here: _recorded_chain and its finding are the model.
  2. The naming gate asks the same of both fields, and says so. Either it treats a non-list as declaring nothing (no pins, no chain, as _recorded_chain does) and leaves the finding to the manifest validator (a name that needs the pin or the chain then classifies without it, exit as the classification gives it, and the project still fails make validate at its second command), or it refuses the project with the same sentence so that the FIRST command of make validate says it. The second is the one the order of the gates favours; the exit (1, a finding, or 2, a refusal, as manifest-unreadable is) is the author's. Either way a string in neutral_product_pins: must not be read as characters and a mapping in referent_chain: must not be read as its keys.
  3. validate-pins.py asks it before it builds PinSources, and refuses with the same sentence (it already refuses with neutral-pin-missing for the first character of a string, which names 'o' and no field).
  4. One reader, one place. The four iterations (five with _recorded_chain) share two questions, "what does this manifest declare?" and "what chain does this leg record?"; a helper in scripts/repo_shape.py (which every project carries, and which validate-manifest.py, validate-pins.py and the naming gate already import) that returns the list, with _recorded_chain's string rule, or raises the refusal removes the idiom from all of them. templates/assembly-root/scripts/validate-manifest.py, validate-pins.py, scripts/validate-repository-naming.py and scripts/repo_shape.py are all digest-pinned copies in a scaffolded root's contracts/shape-pin.yaml, so the change reaches existing projects through update-shape.py apply.

How to test. In tests/test_manifest_validator.py, widen test_neutral_product_pins_must_be_a_list (:286) from the string to 5, true, 1.5, {a: b}, 0, false and '', each asserting manifest-neutral-product-pins in stderr, exit 1, and no Traceback; the chain test at :441 already covers 7 and should cover true, 1.5 and {a: b} too. In tests/test_naming_policy.py, beside the --project tests, run validate-repository-naming.py --project on each of those values for each field and on openXdox for neutral_product_pins:, asserting no Traceback, the chosen answer, that a string never classifies as characters (no pin o) and that {a: b} never reads as a chain a. In tests/test_neutral_pin_validator.py or tests/test_pin_validator.py, run validate-pins.py on each and assert the sentence and no neutral-pin-missing naming 'o'. In the doctor's tests, one row per field: 5 gives INVALID with the rows quoting a sentence and not a TypeError. Controls: [], a missing key, null, [openXdox], and the one-link string openXdox for referent_chain: keep their answers. Reverting the fix fails each. A test must not pin [5] or [[a]] (entries that are not names), which this issue did not examine.

Not in this issue.

Related. #338 and fix/link-source-values (4b21263; _pins_from_project and _chain_from_leg are untouched there), and the two other follow-ups from the review of that branch, #441 and #442, which are about how a --link-source value is read and not about these fields. #132 (the split of validate-manifest.py's _findings, where _neutral_product_pins_type_finding was written). #290, #293, #386 (the same class on other fields). #278, #410 (how the doctor shows the crash).

Activity

  1. brettheap commented on Oct 15, 2026

    @brettheap
    ContributorAuthor

    (lane openRepoShape-2, 2026-10-15T17:27:31Z)

    A fact this issue does not yet hold, from the checklist triage of 2026-10-15 (Brett Heap approved its recommendations at 2026-10-15T16:49:38Z): since #557, the doctor answers this crash CANNOT ANSWER, exit 3, which outranks the manifest's own finding. At main 26c1600, where #557 is a2243f1f, all three validators still end in the TypeError (the triage's probe).

    The checklist line that reported it on #459 (comment 6175509630) is ticked there as a duplicate of this issue.

  2. added this to the v1.1.0 milestone on Oct 15, 2026
  3. brettheap commented on Oct 15, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-4, session 11c6c0ef-495a-4a05-94cf-811b7b0a1012@Eagle, 2026-10-15T23:09:49Z, for #443

    Logged in lanes/log/openRepoShape-4.md at 569360fc96bf4e6e4bf83ac7c326c9c3ab2e6a80.

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

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#443:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#443"`, kept where the row or its body names `#443`:
    453	validate-repository-naming.py, repo_shape.py: a `--link-source` value is `PRODUCT=PATH` only when its key names a chain link, and a link source that cannot be entered is a WARNING with the system's reason, not a traceback (#338)	fix/link-source-values	MERGED
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `443` as a whole token:
       none
    
  4. added 2 commits that reference this issue on Oct 16, 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

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions