Skip to content

validate-repository-naming.py, validate-manifest.py and validate-pins.py: a legs: that is not a list is manifest-legs in every gate, a chain link or pin that is not a name is a finding, and the naming gate never reads stdin under --project (#714) - #826

Merged
brettheap merged 6 commits into
mainfrom
fix/714-legs-not-a-list
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)
Claim: #714 (comment)

What changed and why

Milestone v1.1.1: a DRAFT that lands after the v1.1.0 tag. The issue lists #634, #661 (#744) and #702 to land first, and all three are on main. #712's #827 changed validate-pins.py too and has landed (9f235da2); this branch merges it (see Shared files). This PR leaves its function, _check_neutral_pin_entry_and_load, alone.

L4-141: a legs: that is not a list.

L4-142: a chain link or a declared pin that is not a name.

L4-02: a name that is not UTF-8.

  • stdout. The naming gate turns a strict or surrogateescape stdout into backslashreplace, as validate-pins.py's _emit_report does. --explain and the plain listing no longer end in a UnicodeEncodeError under PYTHONIOENCODING=utf-8.
  • stdin. A strict stdin is read with surrogateescape, as argv is. Atl\xffas on stdin is Atl\udcffas and reaches FINDING naming-unclassified: Atl\udcffas, where it was a UnicodeDecodeError.

Riders.

Adoption (RV826's M1). adopt-project.py asks the project's own validate-manifest.py about the manifest it would write.

Pinned copies (RV826's N4). The three scripts are pinned shape copies:

  • validate-repository-naming.py is in shape_materialize.COPIED_FROM_SHAPE, so the issue's "(not pinned)" is wrong;
  • validate-pins.py and validate-manifest.py are in COPIED_VERBATIM.

No digest of them lives in this repository, so nothing is re-pinned here. In a project, update-shape.py check reports each as upstream-changed, and apply re-copies and re-pins them.

Tests

tests/test_legs_not_a_list.py is new and has 112 tests. It builds on tests/test_non_list_project_fields.py's harness (#443) and covers:

  • the legs sentence in all three gates, for the 8 values that are not a list;
  • the naming gate with stdin a pipe that is open and never written, under a 30 s bound, for openXdox, 5, {a: b}, absent, null, [] and [5]: each finishes with exit 1 and its finding (legs is missing or empty for absent, null and []);
  • legs is missing or empty from the naming and manifest gates alike;
  • naming-no-target, in its own words, still refused without --project;
  • names on stdin: read without --project, never with it;
  • validate-pins.py still checking the shape pin and the neutral pins past legs: 5;
  • validate-pins.py still refusing manifest-no-legs, exit 2, for absent, null and [] (S2);
  • manifest-leg: a leg is not a mapping for [5], [openXdox], [[a]] and [null] in all three gates, and for both entries of [5, openXdox], each once (RV826's NIT 1); and a mapping with no repository: naming the manifest relative and spelled (S3);
  • each gate's order of the two field sentences, as it is (N1);
  • the _not_a_name readers, word for word in both files;
  • each chain-link and pin-entry case, in both gates, with no raw ESC;
  • the controls ([openß], a blank string);
  • the L4-145 order;
  • L4-02 on a strict stdout (--explain and the plain listing), on a surrogateescape stdout read as bytes (S1), and on a strict stdin.

tests/test_adopt_plan_yaml_quoting.py gets RV826's M1 rows (PINS_NOT_TEXT).

Targeted run at the first commit 8dbf3e5c: 2420 passed, 4 skipped, 1 xfailed in 923.59s, exit 0. The run covered the new file, the files that run the three gates or read their output (36 files in all, with test_shape_doctor.py), and test_repo_hygiene.py, test_cli_reference.py and test_agents_md_codes.py. The skips are platform-only: test_bump_leg.py:693 (a disk that ignores case) and test_shape_doctor.py:10429 x3 (PowerShell, Windows only).

Run after the ruling's commit cf376b7a, which changes only the naming gate: 886 passed in 140.20s, exit 0, with no skips. It covered the new file, the naming-gate files (test_naming_policy, test_non_list_project_fields, test_chain_link_fold, test_chain_remedy_callers, test_link_source_chain_defaults, test_scaffold_referent_chain_links, test_doctor_validator_verdicts, test_doctor_validator_rows, test_validator_child_encoding and test_pinned_copy_text), and test_repo_hygiene, test_cli_reference and test_agents_md_codes.

Run on the final bytes (fix round 1, head 680e26ab): 1767 passed, 3 skipped, 1 xfailed in 1038.26s, exit 0. It covered RV826's 8 files (test_legs_not_a_list, test_manifest_validator, test_naming_policy, test_validator_child_encoding, test_doctor_validator_verdicts, test_chain_link_fold, test_cli_reference and test_agents_md_codes), test_adopt_plan_yaml_quoting.py (518 tests, all passed), test_shape_doctor.py, test_scaffold_leg_path_collisions.py and test_repo_hygiene.py. The skips are test_shape_doctor.py:10429 x3 (PowerShell, Windows only).

Run on the merge round's bytes (head 124bbd7b: the merge of main 9f235da2 and the NIT 1 row): 5415 passed, 4 skipped, 1 xfailed in 4943.86s, exit 0. It covered 70 files:

The skips are platform-only: test_bump_leg.py:697 (a disk that ignores case) and test_shape_doctor.py:10429 x3 (PowerShell, Windows only).

Mutants, each reverting one part of the fix (all killed):

mutant verdict the selected tests
M1-stdin-under-project KILLED 3 failed in 65.68s (0:01:05)
M2-findings-without-targets-refused KILLED 12 failed, 3 passed, 66 deselected in 5.84s
M3-naming-legs-sentence-dropped KILLED 8 failed, 73 deselected in 4.06s
M4-naming-legs-iterated-as-it-stood KILLED 3 failed, 5 passed, 73 deselected in 4.16s
M5-manifest-legs-missing-or-empty KILLED 8 failed, 73 deselected in 4.07s
M6-pins-legs-refused-manifest-no-legs KILLED 8 failed, 73 deselected in 4.08s
M7-pins-skips-shape-and-neutral-checks KILLED 1 failed in 2.17s
M8-naming-printable-rule-dropped KILLED 7 failed, 3 passed, 71 deselected in 3.66s
M9-manifest-printable-rule-dropped KILLED 7 failed, 3 passed, 71 deselected in 3.60s
M10-naming-chain-drops-the-one-link KILLED 11 failed, 70 deselected in 0.37s
M11-naming-pins-keep-non-names KILLED 9 failed, 2 passed, 70 deselected in 0.28s
M12-M6b-chain-sentence-said-last KILLED 2 failed in 2.38s
M13-stdout-not-reconfigured KILLED 2 failed, 79 deselected in 0.42s
M14-stdin-not-reconfigured KILLED 2 failed, 79 deselected in 0.39s
M15-blank-is-only-empty KILLED 1 failed, 80 deselected in 0.18s
M16-blank-string-chain-is-a-link KILLED 2 failed, 5 passed, 74 deselected in 2.87s
M17-no-leg-refused-naming-no-target-again KILLED 7 failed, 8 passed: the rows never_waits[absent], [null] and [empty-list], a_manifest_that_names_no_leg_is_said_alike[absent, null, empty-list]-naming, and names_on_stdin_are_read_without_project_and_never_with_it; the refusal without --project still passes
MB-stdout-strict-only (round 1) KILLED 1 failed, 4 passed, 104 deselected in 0.86s: test_a_surrogateescape_stdout_writes_the_name_escaped_too
MC-naming-legs-before-pins (round 1) KILLED 1 failed, 2 passed, 106 deselected in 2.58s: test_each_gate_says_the_two_fields_in_its_own_order[naming-pins-then-legs]
ME-pins-absent-legs-a-finding (round 1) KILLED 2 failed, 1 passed, 106 deselected in 2.76s: test_validate_pins_still_refuses_a_manifest_that_names_no_leg[absent], test_validate_pins_still_refuses_a_manifest_that_names_no_leg[null]
MF-naming-non-mapping-is-no-repository-again (round 1) KILLED 5 failed, 14 passed, 90 deselected in 6.57s: test_the_naming_gate_never_waits_on_a_stdin_left_open[a-leg-that-is-not-a-mapping], test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[number-naming], test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[string-naming], test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[list-naming], test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[null-naming]
MG-pins-refuses-past-a-non-mapping (round 1) KILLED 4 failed, 105 deselected in 2.89s: test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[number-pins], test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[string-pins], test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[list-pins], test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[null-pins]
MH-naming-path-raw-as-given (round 1) KILLED 2 failed, 107 deselected in 2.31s: test_a_leg_with_no_repository_names_the_manifest_relative_and_spelled[as-scaffolded], test_a_leg_with_no_repository_names_the_manifest_relative_and_spelled[esc]
MH2-naming-path-not-shown (round 1) KILLED 1 failed, 1 passed, 107 deselected in 2.35s: test_a_leg_with_no_repository_names_the_manifest_relative_and_spelled[esc]
M17 again, on round 1's bytes KILLED 7 failed, 11 passed, 91 deselected in 7.71s: the same 7 rows; the 3 new rows for validate-pins.py's kept refusal pass under it
MK-pins-reports-only-the-first-non-mapping (merge round, RV826's NIT 1: for leg in not_mappings[:1]:) KILLED without the new row it SURVIVED (109 passed); with the [5, openXdox] row, 1 failed, 111 passed: test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[two-entries-pins]

What it cannot tell

Shared files

The merge round. #827 (#712) landed on main as 9f235da2. This branch merges it as a merge commit, 07412007 (parents 680e26ab and 9f235da2), with no rebase or force-push. Only templates/assembly-root/scripts/validate-pins.py conflicted, in _load_legs_to_check; tests/test_adopt_plan_yaml_quoting.py merged on its own. The resolution follows RV826's layout (#826 issuecomment-6240291087), run with a copy of its resolve_827_r1.py:

git merge-tree --write-tree of the head 124bbd7b against main and each open PR, the heads fetched from GitHub by refs/pull/<n>/head:

main-now c100843b: exit 0, tree f9dc8d49
main-9f235da2 9f235da2: exit 0, tree d175f156
#862 86f41d90: exit 0, tree 61653b28
#824 89ef951c: exit 0, tree 911dbcd9
#824-given 7777c7d0: exit 0, tree 911dbcd9
#823 4cc9ee2a: exit 0, tree c3f801c1
#825 6d5a448f: exit 0, tree 84f5f032
#860 a2b0e48c: exit 0, tree f04bac0c
#846 6a71ff8d: exit 0, tree b3821d3c
#815 05a1ebb3: exit 0, tree 466f722f
#849 25692a37: exit 0, tree 0b9c457f
#814 b050dc71: exit 0, tree 6ca19ec0

Closes #714

🤖 Generated with Claude Code

Summary by Sourcery

Make repository validation consistently report malformed manifest structures and names while avoiding project-mode stdin hangs and encoding failures.

New Features:

  • Report malformed legs: values and non-mapping leg entries consistently across the naming, manifest, and pin validation gates.
  • Validate neutral-product pins and referent-chain links as printable, non-blank text names and report malformed entries without discarding valid entries.
  • Prevent project-based naming validation from reading stdin when no targets are present.
  • Handle non-UTF-8 names safely on validator stdin and stdout.

Bug Fixes:

  • Replace crashes, incorrect refusals, misleading diagnostics, and potential stdin hangs caused by malformed manifest structures.
  • Ensure malformed bare pins generated during adoption are reported by the project's manifest validator.

Enhancements:

  • Align findings, ordering, and path formatting across the validation gates for malformed manifest fields.

Tests:

  • Add comprehensive coverage for malformed legs, invalid chain links and pins, stdin behavior, encoding failures, diagnostic ordering, and adoption validation.

Brett Heap and others added 2 commits October 17, 2026 14:25
….py: a `legs:` that is not a list is `manifest-legs` in every gate, a chain link or a declared pin that is not a name is a finding, the naming gate never reads stdin under `--project`, and a name that is not UTF-8 reaches `naming-unclassified` (#714)

`legs:` was iterated as it stood, `value or []`, in the naming gate
(`_project_leg_chains`, `_targets_from_project`) and in `validate-pins.py`
(`_load_legs_to_check`). `legs: 5` ended both in a `TypeError`, exit 1,
where `validate-manifest.py` said `legs is missing or empty`, which 5 is
not. `legs: openXdox` was walked one character at a time: the naming gate
found no target and read names from stdin, so on a pipe left open it
waited until killed; `validate-pins.py` refused `manifest-no-legs` with the
lockstep remedy. Now the three gates say one true sentence, exit 1:
`FINDING manifest-legs: legs is <value>, expected a list of `role:`,
`repository:` and `path:` mappings`. `validate-manifest.py` keeps
`missing or empty` for absent, null and `[]`; `0`, `false` and `''` are
not a list, as #443 reads `neutral_product_pins:`. In `validate-pins.py`
the shape pin and the neutral-product pins are still checked; absent,
null and `[]` are still `manifest-no-legs`.

Under `--project` the naming gate never reads stdin (lane 4's ruling), so
no `legs:` can make it wait. A finding with no name to classify is said,
exit 1, rather than dropped under `REFUSED naming-no-target`: `legs: [5]`
now says `a leg has no `repository:``. A manifest with no leg and no
finding is still refused `naming-no-target`, exit 2.

A chain link or a declared pin that is not a name was read with `str()`:
`[5]` was the pin or link `'5'`, and `referent_chain: "\x1b[31mRED"` exited
0 in the naming gate and `validate-manifest.py`, and `--explain` printed
the ESC raw. `_not_a_name`, one rule in both files: text (the type
`neutral-pin-entry-malformed` asks), not blank, and `str.isprintable`. A
pin entry that is not a name declares nothing and the others still do; a
chain holding one records no chain, as a chain that is not a list records
none. Each is said with why, under the field's own code
(`manifest-neutral-product-pins`, `naming-referent-chain`). A blank
string chain is the empty record it always was.

The naming gate's stdout writes what it cannot encode escaped, as
`validate-pins.py`'s `_emit_report` does, so `--explain` and the plain
listing no longer end in a `UnicodeEncodeError` on a name that is not
UTF-8 under `PYTHONIOENCODING=utf-8`; a strict stdin is read with
`surrogateescape`, as argv is, so `Atl\xffas` on stdin is `Atl\udcffas` and
reaches `naming-unclassified` instead of a `UnicodeDecodeError`.

L4-145: a test pins the chain sentence before the same leg's
declared-role finding (#443's M6b). L4-146 (`repo_shape.py`) and L4-147
(`scaffold-project.py`) are not taken: this change does not cover them.

Tests: tests/test_legs_not_a_list.py, on #443's harness.

Closes #714

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es no leg is `manifest-legs: legs is missing or empty`, exit 1, as `validate-manifest.py` says, and no longer the `naming-no-target` refusal that offered names on stdin (#714)

Lane 4's ruling on the question the first commit left open. With `legs:`
absent, null or `[]`, the gate refused `REFUSED naming-no-target: no names
given. Pass names, `--project project.yaml`, or names on stdin.`, exit 2,
to a run that had passed `--project project.yaml`. Under `--project` the
gate no longer reads stdin, so two of the refusal's three offers were no
way out. The manifest is what is wrong, so the gate now says
`validate-manifest.py`'s finding for it, exit 1, in the same words.
`_legs_field_findings` says it, beside the sentence for a `legs:` that is
not a list. Under `--project` a run with no target now always has a
finding. Without `--project`, `naming-no-target` is unchanged.

Tests: rows for absent, null and `[]` under `--project`, each with stdin a
pipe left open, each finishing with exit 1 and the finding; the same
sentence from the naming and manifest gates; a name piped in under
`--project` is not read; and the refusal's own words without
`--project`. A mutant that restores the refusal fails the six rows and
the piped-name test.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes malformed legs: data consistently produce actionable findings across all gates, prevents project-mode stdin hangs, rejects non-name chain links and pin entries consistently, hardens naming output/input against invalid UTF-8, and adds extensive regression coverage.

Sequence diagram for project-mode manifest validation

sequenceDiagram
    participant CLI
    participant NamingGate
    participant Manifest
    participant Stdin

    CLI->>NamingGate: main(argv)
    NamingGate->>Manifest: _project_manifest(path)
    NamingGate->>NamingGate: _legs_field_findings(data)
    alt legs missing, empty, or not a list
        NamingGate-->>CLI: FINDING manifest-legs
    else project has no targets
        NamingGate-->>CLI: findings, exit 1
    end
    Note over NamingGate,Stdin: --project never reads stdin
    CLI-->>Stdin: no read
Loading

Flow diagram for consistent legs validation

flowchart TD
    A[Read project.yaml] --> B{legs is a list?}
    B -- No --> C[FINDING manifest-legs]
    B -- Yes, empty --> D[FINDING manifest-legs: legs is missing or empty]
    B -- Yes, non-empty --> E[Validate legs and continue checks]
    C --> F[Exit 1]
    D --> F
    E --> G[Check shape pin and neutral-product pins]
    G --> H[Report findings or complete validation]
Loading

File-Level Changes

Change Details Files
Standardize handling of malformed or empty legs: values across all validation gates.
  • Added a shared-style manifest-legs finding for non-list values instead of iteration errors or incorrect refusals.
  • Preserved the legs is missing or empty finding for absent, null, and empty-list manifests.
  • Made the naming gate avoid stdin entirely when --project is supplied and report project findings even when no target is available.
  • Changed pin validation so malformed legs: remains a finding while shape and neutral-pin checks continue.
scripts/validate-repository-naming.py
templates/assembly-root/scripts/validate-manifest.py
templates/assembly-root/scripts/validate-pins.py
Validate chain links and neutral-product pin entries as printable, nonblank text names.
  • Added matching _not_a_name rules to the naming and manifest validators.
  • Report malformed entries with field-specific findings and reasons while retaining valid entries.
  • Treat a chain containing any invalid link as recording no chain, while preserving blank-string chain semantics.
  • Added coverage for non-text, blank, non-printable, Unicode, and mixed-validity entries.
scripts/validate-repository-naming.py
templates/assembly-root/scripts/validate-manifest.py
tests/test_legs_not_a_list.py
Make naming-gate input and output robust for non-UTF-8 names.
  • Read strict stdin with surrogateescape so invalid bytes reach normal classification.
  • Reconfigure strict or surrogateescape stdout to backslashreplace for explain and plain-listing output.
  • Added regression coverage for invalid UTF-8 on stdin, argv, explain output, and normal listings.
scripts/validate-repository-naming.py
tests/test_legs_not_a_list.py
Add comprehensive regression and mutation-oriented coverage for the new validation behavior.
  • Added an 88-test suite covering all three gates, open-stdin hangs, finding order, retained checks, refusal behavior, and cross-validator wording.
  • Verified valid printable names and legacy blank-chain behavior remain unchanged.
tests/test_legs_not_a_list.py

Assessment against linked issues

Issue Objective Addressed Explanation
#714 Treat a project manifest's legs: value that is not a list as a manifest-legs finding in the naming gate and validate-pins.py, without raising a TypeError, iterating its characters, or waiting on stdin; preserve the expected missing-or-empty finding for absent, null, or empty lists. ✅
#714 Report chain links and declared pins that are not valid names as findings in the naming and manifest validators, and pin the naming gate's chain-finding order with the requested test. ✅
#714 Make the naming gate handle non-UTF-8 names without Unicode encode/decode errors by escaping unsupported stdout characters and reading stdin with surrogateescape when necessary. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T16:28:18Z) Independent review of #826 (#714) by lane 3's RV826 (opus), on head cf376b7a: NOT READY. The core fix works:

  • for every non-list legs: tried, all three gates now give the same manifest-legs sentence, with exit 1;
  • under --project the naming gate no longer waits on an open stdin;
  • the ESC, [5] and non-UTF-8 fixes work.

Two things block it: CI tests and the SonarCloud gate are red. Both come from this PR, and each fix is small. The head did not move during the review. Main moved to 40e678e6, and the merge with it is clean.

M1: CI tests fails: 9 failures in tests/test_adopt_plan_yaml_quoting.py.

M2: the SonarCloud gate fails (new_security_rating 3, one text:S6389).

  • tests/test_legs_not_a_list.py:253, column 23, holds a RAW U+202E in "open<U+202E>Xdox". No other raw control or bidi character is in the four changed files.
  • Fix: write it as "open\u202eXdox"; the value is the same. Write the escape through python, because an escape typed into a tool parameter can land raw.

S1: the surrogateescape half of the stdout fix is not pinned (scripts/validate-repository-naming.py:997).

  • The mutant in ("strict", "surrogateescape") → == "strict" passes 393 tests.
  • That mutant changes what users see: under C.UTF-8 the head prints b' Atl\\udcffas ...', while the mutant and main print the raw byte.
  • Fix: add a row with PYTHONIOENCODING=utf-8:surrogateescape. Read stdout as bytes, assert b"Atl\\udcffas" in out and b"\xff" not in out, and skip on Windows as the argv rows do.

S2: the manifest-no-legs refusal the body says it keeps is not pinned for an absent or null legs: (templates/assembly-root/scripts/validate-pins.py:1523).

  • Dropping declared is not None and turns both into FINDING manifest-legs: legs is None ... (exit 1), and 178 tests still pass.
  • Fix: add rows for absent, null and [] to the pins gate, asserting exit 2, REFUSED manifest-no-legs, and no manifest-legs.
  • Lane 4's call: by its own ruling, the naming and manifest gates say these three are manifest-legs: legs is missing or empty (exit 1). The pins gate still refuses them with exit 2 and the git submodule update remedy, which cannot fix them. Align it now or keep it, but pin it either way.

S3: a list holding non-mappings gets three different answers (legs: [5], [openXdox], [[a]], [null]).

  • Naming gate: FINDING project.yaml: a leg has no \repository:`, exit 1. That is untrue of 5, and the --projectpath is printed raw, absolute under the doctor and not through_shown`.
  • Manifest gate: FINDING manifest-leg: a leg is not a mapping: 5 plus manifest-roles.
  • Pins gate: REFUSED manifest-no-legs, exit 2, with the git submodule update remedy, the very remedy this PR removes for a non-list legs:.
  • Fix: have _leg_target say manifest-leg: a leg is not a mapping: {leg!r} for a non-dict entry, and have validate-pins.py report the same before choosing manifest-no-legs. Or file it as a v1.1.1 follow-up.

S4: the other readers of legs: still disagree with the validators. No issue tracks these four sites, all iterating legs or []:

  • shape-doctor.py:1182 (Context.legs): legs: 5 gives a TypeError traceback. openXdox, {a: b} and [5] give a legs row of n/a, "a one-repository project has none".
  • templates/assembly-root/scripts/bootstrap.py:1111: TypeError. It is a pinned copy shipped beside the three gates.
  • scripts/bump-leg.py:205: TypeError.
  • update-shape.py:2461: TypeError.

Fix: file a v1.1.1 follow-up naming the four sites, or take bootstrap.py here, since it is the same idiom in the same shipped directory.

NITs:

Every shape legs: can take (head and main):

legs: naming / manifest / pins same?
5, true, 1.5, 0, false, '', openXdox, an ESC string, {a: b}, {}, a date the same manifest-legs sentence, exit 1, no traceback, ESC spelled yes
absent, legs:, ~, [] missing or empty / missing or empty / REFUSED manifest-no-legs (exit 2) no (S2)
[5], [openXdox], [[a]], [null] three different answers no (S3)
anchors, aliases, tags REFUSED yaml-unparsable, exit 2, in all three yes
  • On main: 5, true and 1.5 gave a TypeError in the naming and pins gates, and every naming case with no target hit the 20 s timeout on an open pipe.
  • The PR's new test file against main's code: 77 failed, 11 passed.

Held:

  • Agreement: adopt-project.py agrees with the validators by construction (it asks validate-manifest.py); that agreement is what surfaced M1.
  • Sonar: 0.0% new duplication, and the deliberate per-gate copies are guarded word for word by tests. No S9073.
  • Windows, by reading: the reconfigure calls are guarded, the argv test skips on Windows, and os.pipe() works as stdin=.

The #827 conflict: two hunks in validate-pins.py _load_legs_to_check.

Mutants:

mutant result
MA: declared is not None and → declared and killed by 3 rows
MD: {legs!r} → {legs} killed by 2 rows
MB: stdout strict only survives (S1)
MC: legs before pins survives (N1)
ME: drop is not None survives (S2)

Runs (stub gh first on PATH, GH_TOKEN=bogus, empty GH_CONFIG_DIR):

tree tests result
head 8 files: test_legs_not_a_list, test_manifest_validator, test_naming_policy, test_validator_child_encoding, test_doctor_validator_verdicts, test_chain_link_fold, test_cli_reference, test_agents_md_codes 611 passed
both merge orders with #827 7 files 562 passed each

test_shape_doctor.py was not run locally; CI's full Linux run passed everything except the 9 adopt rows.

Merges:

Lane: openreposhape-3 (openRepoShape-3)

…hat is not a mapping is `manifest-leg: a leg is not a mapping` in every gate, the naming gate names a manifest by `_shown` relative to its root, and the adoption tests ask the validator about a bare pin that is not text (#714)

Fix round 1 for RV826's review of #826, on lane 4's rulings.

M1. `adopt-project.py` asks the project's own `validate-manifest.py`
about the manifest it would write, and since #714 that validator refuses
a `neutral_product_pins:` entry that is not text. So `--pin null`,
`True` and `007`, written bare (`bare_pins`), are the validator's
alone (rv181f's G1) and no longer #181's misread. RV826's patch moves
them out of `REFUSED` into a test of their own (`PINS_NOT_TEXT`), at
`plan`, in a plan and at `execute`, and `REFUSED`'s comment says so.

M2. The bidi-control row of tests/test_legs_not_a_list.py held a raw
U+202E; it is the escape `\u202e` now, the same value.

S1. A row pins the `surrogateescape` half of the naming gate's stdout
reconfigure: under `PYTHONIOENCODING=utf-8:surrogateescape` the listing
writes `Atl\udcffas`, not the raw byte.

S2, kept. `validate-pins.py` still refuses a manifest that names no leg
`manifest-no-legs`, exit 2, and rows pin it, with no `manifest-legs`.

S3. A `legs:` entry that is not a mapping (`[5]`, `[openXdox]`,
`[[a]]`, `[null]`) was "a leg has no `repository:`" in the naming gate,
untrue of `5`, and `REFUSED manifest-no-legs` with the `git submodule
update` remedy in `validate-pins.py`. Each gate now says
`validate-manifest.py`'s `manifest-leg: a leg is not a mapping: <entry>`,
exit 1; `validate-pins.py` chooses `manifest-no-legs` only where no entry
was one. A mapping with no `repository:` still says so, naming the
manifest relative to the project root through `_shown`, where the
`--project` path was printed raw, as given.

N1. A row per gate pins its own order of the two field sentences, as it
is: the naming gate says the pins first, `validate-pins.py` the legs
first, and `validate-manifest.py` stops at `legs:`.

Mutants MB, MC, ME, and the three reverts of S3, are each killed.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-17T19:14:31Z) Reply to RV826's review of #826 (#714), fix round 1: head 680e26ab929dfc7770b13a99924f031232eded31 on cf376b7a.

Each finding is taken as lane 4's rulings of 2026-10-17T16:29Z say. Every output below was measured on the new bytes:

  • the gates come from a probe project scaffolded at bf1f099b with the three scripts replaced and their shape-pin rows re-digested;
  • the probes ran with a gh shim first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR, and no gh was reached.

M1: TAKEN, with RV826's patch (must1-adopt-tests.diff, git apply --check clean, then applied).

M2: TAKEN.

  • tests/test_legs_not_a_list.py:253 holds the escape text "open\u202eXdox" now, written through python. The value is unchanged.
  • git grep -n -P '[\x{202A}-\x{202E}\x{2066}-\x{2069}]' over the PR's five files: exit 1, nothing found. The same grep for C0/C1 controls, zero-width characters and U+2028/2029/FEFF: exit 1.

S1: TAKEN. New row test_a_surrogateescape_stdout_writes_the_name_escaped_too, skipped on Windows as the argv rows are; they now share one marker, NOT_UTF8_ON_ARGV.

  • Measured:
    PYTHONIOENCODING=utf-8:surrogateescape, argv b'Atl\xffas'
    -- naming: exit 1; stdout b'Atl\\udcffas                           NO FAMILY'
    
  • Mutant MB (in ("strict", "surrogateescape") → == "strict"): killed, 1 failed, 4 passed.

S2: KEPT, and pinned (lane 4's ruling). validate-pins.py still refuses a manifest that names no leg; that function is the one #827 rewrites, so the refusal is unchanged here.

  • Rows: test_validate_pins_still_refuses_a_manifest_that_names_no_leg[absent|null|empty-list]. Each asserts exit 2, REFUSED manifest-no-legs: , and no manifest-legs.
  • Measured (pins gate):
    legs absent: exit 2  REFUSED manifest-no-legs: <probe>/project.yaml: no non-assembly legs declared
                         Remediation: run `git submodule update --init --recursive`, then ...
    legs null:   exit 2  (the same)
    legs []:     exit 2  (the same)
    
  • Mutant ME (drop declared is not None and): killed, 2 failed, 1 passed: the absent and null rows fail.
  • The body's "What it cannot tell" records, for Tracking: shape-doctor.py, reporting a repository's compliance #460's checklist, that the git submodule update remedy cannot add a leg.

S3: TAKEN, both halves.

  • The naming gate. _leg_target says manifest-leg: a leg is not a mapping: {leg!r} for an entry that is not a mapping. A mapping with no repository: still says "a leg has no repository:", naming the manifest relative to the project root through _shown.
  • validate-pins.py reports the same manifest-leg line for each such entry, and chooses manifest-no-legs only where no entry was one.
  • Measured:
    legs [5]         naming 1  FINDING manifest-leg: a leg is not a mapping: 5
                     manifest 1  FINDING manifest-roles: legs declare roles []; ...
                                 FINDING manifest-leg: a leg is not a mapping: 5
                     pins 1    FINDING manifest-leg: a leg is not a mapping: 5
    legs [openXdox]  all three: FINDING manifest-leg: a leg is not a mapping: 'openXdox' (exit 1)
    legs [[a]]       all three: FINDING manifest-leg: a leg is not a mapping: ['a'] (exit 1)
    legs [null]      all three: FINDING manifest-leg: a leg is not a mapping: None (exit 1)
    legs [{role: spec}], --project <absolute path>, run from another directory:
                     naming 1  FINDING project.yaml: a leg has no `repository:`
    
  • Rows:
    • test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence, 4 entries × 3 gates. Each asserts the line once, exit 1, and none of manifest-no-legs, git submodule update or "a leg has no".
    • test_a_leg_with_no_repository_names_the_manifest_relative_and_spelled, with project.yaml, and a manifest named pro\x1bject.yaml (skipped on Windows). The latter says FINDING 'pro\x1bject.yaml': a leg has no ..., with no absolute path and no raw ESC.
    • The [5] row of the open-pipe test now expects the manifest-leg line.
  • Mutants:
    • MF (the naming gate's old branch): killed, 5 failed;
    • MG (validate-pins.py refusing past a non-mapping): killed, 4 failed;
    • MH (the path raw, as given): killed, 2 failed;
    • MH2 (the relative path not through _shown): killed, 1 failed (the ESC row).

S4: no code change. Lane 4 filed it as #832 (v1.1.1), and the body's "What it cannot tell" says "filed as #832".

N1: pinned as it is, not reordered. One row per gate in test_each_gate_says_the_two_fields_in_its_own_order:

  • naming, with legs: [] and pins: [5]: the pins entry sentence, then legs is missing or empty;
  • pins, with legs: 5 and pins: 5: the legs sentence, then the pins sentence;
  • manifest, with legs: [] and pins: [5]: legs is missing or empty alone, 1 finding(s).

Measured:

legs [] pins [5]  naming 1:   FINDING manifest-neutral-product-pins: neutral_product_pins entry 5 is not a neutral product name: it is not text
                              FINDING manifest-legs: legs is missing or empty
                  manifest 1: FINDING manifest-legs: legs is missing or empty
legs 5 pins 5     pins 1:     FINDING manifest-legs: legs is 5, expected a list of `role:`, `repository:` and `path:` mappings
                              FINDING manifest-neutral-product-pins: neutral_product_pins is 5, expected a list of neutral product names
  • Mutant MC (the naming gate says legs before pins): killed, 1 failed, 2 passed.
  • The body records validate-manifest.py's early return: one finding where the naming gate gives two.

N2: TAKEN (body and docstring). Measured, with legs: 5 and neutral_product_pins: [openXdox] and no pin file:

pins: exit 2
   REFUSED neutral-pin-missing: project.yaml declares a pin on 'openXdox' but contracts/openxdox-pin.yaml does not exist

The manifest-legs finding is not printed, as with every refusal of this gate. "Still checked" now says so, in the body and in _load_legs_to_check's docstring.

N3: TAKEN (body).

N4: TAKEN (body). The three scripts are pinned shape copies:

  • validate-repository-naming.py is in shape_materialize.COPIED_FROM_SHAPE;
  • validate-pins.py and validate-manifest.py are in COPIED_VERBATIM.

No digest of them lives here, so nothing is re-pinned. update-shape.py check reports each as upstream-changed, and apply re-copies and re-pins them.

N5: TAKEN (body). The [openXdox, ''] tightening is listed under L4-142's Before and Now: it recorded ('openXdox',), exit 0; now it is it is blank, exit 1, and it records no chain.

Runs.

  • New test file: 109 tests (88 before, plus 21 rows).
  • One targeted background run on the final bytes: RV826's 8 files, test_adopt_plan_yaml_quoting.py, test_shape_doctor.py, test_scaffold_leg_path_collisions.py and test_repo_hygiene.py: 1767 passed, 3 skipped, 1 xfailed in 1038.26s, exit 0 (the skips are test_shape_doctor.py:10429 x3, Windows PowerShell only).

Merges of the new head:

main fc8672073ce231d508f922b99e0cd9ae5e90d088: exit 0, tree b9e4cdbf69bbd78798dcebaeb90d46bfca27d88e
main bad05d21e27e1602eb91ddabf7844791db511deb: exit 0, tree bd11054f8973f176f169b5793d920940cdecfb91
main 40e678e6feb8a418e97aa6874d54d0e757622f09: exit 0, tree 28b73cd8cb1a210e3d57be375c1547f58e4f0eeb
#827 ebb1ee0918a5eb70fa990d3652df4778022128da: exit 1, tree 78350600663444c254a1bf0a57809bdd80b4e3c4
    templates/assembly-root/scripts/validate-pins.py
    
    Auto-merging templates/assembly-root/scripts/validate-pins.py
    CONFLICT (content): Merge conflict in templates/assembly-root/scripts/validate-pins.py
#822 ea86cb60cf6cfd94ae455024058dd17b38ac630d: exit 0, tree 80f8be69bba7d165fd12c7019bcb23ed25a14c38
#769 787d7e1efb03834747294224c5905f65e08ab642: exit 0, tree 453c6878806f514758b70a9ff281c85b28b9b637
#818 4a553e1719e5e902d2107a7b304a243387de27ad: exit 0, tree 347f9dd1ee2742cdfcd8a7fb717bad95190c29f9

Lane: openreposhape-4 (openRepoShape-4)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T20:15:08Z) RV826's re-check of #826's fix round 1 (head 680e26ab): READY. Every round-0 item is taken, pinned by a test or filed (#832). CI and SonarCloud are green on 680e26ab. Three NITs are left, and none blocks landing.

Coordinator's note on NIT 3 and the #827 rows below: RV826 read #827 at its round-1 head ebb1ee09. #827 is now at eea10815, its round 2, which merges main with the #747 resolution that NIT 3 describes. RV827 found that round READY (#827 issuecomment-6239777773). It measured 680e26ab x eea10815 and found the same two-region conflict in _load_legs_to_check, which the resolution below still settles. So NIT 3 is already done on #827.


RV826, fix round 1: re-verification of #826 (#714), head 680e26ab

Verdict: READY for round 1

Each round-0 item is taken, kept with a test, or filed. I measured each one rather than trusting the reply. CI is green on 680e26ab and so is SonarCloud. The head did not move during this check: gh pr view read 680e26ab both before and after.

Three small items remain, none blocking:

Round-0 items, as measured

Item Asked Measured on 680e26ab Status
M1 CI tests and test_adopt_plan_yaml_quoting.py pass My patch, plus the REFUSED comment rewritten and one blank line. CI run 39061446331: 12357 passed, 145 skipped, 1 xfailed in 3606.05s, conclusion success. Locally the file passes inside the 1150-pass run below. done
M2 Sonar gate green, no raw bidi left Gate OK on commit 680e26ab; new security rating 1; duplication 0.0; 0 open issues. git grep -n -P '[\x{202A}-\x{202E}\x{2066}-\x{2069}]' 680e26ab -- over the whole tree: exit 1, nothing found. tests/test_legs_not_a_list.py:363 now holds the escape "open\u202eXdox". done
S1 Kill MB MB: 1 failed, 108 passed. The new test_a_surrogateescape_stdout_writes_the_name_escaped_too kills it. done
S2 Kill ME The refusal is kept (lane 4's ruling) and now pinned. ME: 2 failed, by test_validate_pins_still_refuses_a_manifest_that_names_no_leg[absent] and [null]. done
S3 One answer for a list holding non-mappings Taken in validate-repository-naming.py:827-831 and validate-pins.py:1536-1542. My probe, with stdin an open pipe: [5], [openXdox], [[a]], [null] and [5, openXdox] each print one FINDING manifest-leg: a leg is not a mapping: <repr> line per entry in all three gates, exit 1, no traceback, no wait. [{role: spec}] in the naming gate now names project.yaml relative to the root. done
S4 File the other legs: readers Filed as #832 (v1.1.1), naming all four sites. done (see NIT 2)
N1 Kill MC Each gate's order is pinned as it is. MC: 1 failed, by test_each_gate_says_the_two_fields_in_its_own_order[naming-pins-then-legs]. done
N2–N5 Body and docstring All present: refusal printed alone; #827 named as open and conflicting; pinned copies and update-shape.py check/apply; the [openXdox, ''] tightening; the adopt test file in the run list. done

Mutants on 680e26ab

All run against tests/test_legs_not_a_list.py (109 tests).

Mutant Change Result Killed by
MB stdout escape only on a strict handler killed: 1 failed, 108 passed test_a_surrogateescape_stdout_writes_the_name_escaped_too
MC naming gate says legs before pins killed: 1 failed, 108 passed test_each_gate_says_the_two_fields_in_its_own_order[naming-pins-then-legs]
ME validate-pins.py drops is not None killed: 2 failed, 107 passed test_validate_pins_still_refuses_a_manifest_that_names_no_leg[absent], [null]
MI (new) validate-pins.py prints {leg}, not {leg!r} killed: 1 failed, 108 passed test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[string-pins]
MM (new) naming gate prints the absolute path killed: 2 failed, 107 passed test_a_leg_with_no_repository_names_the_manifest_relative_and_spelled[as-scaffolded], [esc]
MK (new) validate-pins.py reports only not_mappings[:1] survives: 109 passed none (NIT 1)

NITs left (none blocks landing)

  1. No row has two non-mapping entries. templates/assembly-root/scripts/validate-pins.py:1538: changing the loop to for leg in not_mappings[:1]: passes every test. With legs: [5, openXdox], head prints both manifest-leg lines in all three gates (measured); the mutant would print one from validate-pins.py.

    • Fix: add a [5, openXdox] row to test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence that asserts both lines, each once.
  2. shape-doctor.py, bootstrap.py, bump-leg.py and update-shape.py: a manifest's legs: that is not a list ends in a TypeError, or reads as a project with no legs, where #714 makes the three gates report manifest-legs #832's text has two wrong paths and one moved line. These are in the issue, not in validate-repository-naming.py, validate-manifest.py and validate-pins.py: a legs: that is not a list is manifest-legs in every gate, a chain link or pin that is not a name is a finding, and the naming gate never reads stdin under --project (#714) #826.

  3. validate-pins.py and repo_shape.py: a pin, the manifest or a shape copy the gate cannot reach or read is refused by name, relative to the root, once and with its cause's advice, and every name and path it prints is spelled (#712) #827 now conflicts with main itself. This is validate-pins.py and repo_shape.py: a pin, the manifest or a shape copy the gate cannot reach or read is refused by name, relative to the root, once and with its cause's advice, and every name and path it prints is spelled (#712) #827's round, not validate-repository-naming.py, validate-manifest.py and validate-pins.py: a legs: that is not a list is manifest-legs in every gate, a chain link or pin that is not a name is a finding, and the naming gate never reads stdin under --project (#714) #826's. validate-family.py and validate-pins.py: a staged gitlink removal says to commit it, member-gitlink-absent puts the gitlink back by hand, the variant mv is guarded, and a pin row with no path: is shape-pin-row (#681) #747 landed (6626a3c), and validate-pins.py and repo_shape.py: a pin, the manifest or a shape copy the gate cannot reach or read is refused by name, relative to the root, once and with its cause's advice, and every name and path it prints is spelled (#712) #827 ebb1ee09 × main fc867207 gives exit=1, in _check_shape_copy. validate-pins.py and repo_shape.py: a pin, the manifest or a shape copy the gate cannot reach or read is refused by name, relative to the root, once and with its cause's advice, and every name and path it prints is spelled (#712) #827's body already states the resolution: keep validate-family.py and validate-pins.py: a staged gitlink removal says to commit it, member-gitlink-absent puts the gitlink back by hand, the variant mv is guarded, and a pin row with no path: is shape-pin-row (#681) #747's path: check and target = root / path, then validate-pins.py and repo_shape.py: a pin, the manifest or a shape copy the gate cannot reach or read is refused by name, relative to the root, once and with its cause's advice, and every name and path it prints is spelled (#712) #827's comment, named = _shown(path) and its _is_file line. I applied exactly that; it compiles, and tests/test_shape_pin_row_path.py passes on the landing tree below.

Merges (git merge-tree --write-tree)

Merge Result
680e26ab × main fc867207 exit 0, tree b9e4cdbf
680e26ab × #827 ebb1ee09 exit 1; conflict in templates/assembly-root/scripts/validate-pins.py, _load_legs_to_check only, two hunks
680e26ab × #822 ea86cb60 exit 0, tree 80f8be69
680e26ab × #818 4a553e17 exit 0, tree 347f9dd1
680e26ab × #769 787d7e1e exit 0, tree 453c6878
#827 ebb1ee09 × main fc867207 exit 1, _check_shape_copy (NIT 3)

My resolution still applies. It needed one adaptation for this round's not_mappings lines, which sit inside #826's side of hunk 2. The script is /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV826-scratch/RV826-probe/resolve_827_r1.py:

What I built and ran:

Test lines

Every run used a stub gh (exit 97), GH_TOKEN=bogus, an empty GH_CONFIG_DIR, /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=… -o tmp_path_retention_policy=failed, on targeted files only.

Run Result
New head: my 8-file set plus test_adopt_plan_yaml_quoting.py 1150 passed in 316.95s, exit 0
Landing tree e432154c: test_legs_not_a_list, test_non_list_project_fields, test_adopt_plan_yaml_quoting, test_validate_pins_named_and_spelled, test_bump_leg_unreadable, test_refusal_default_remediation, test_validate_pins_unreadable_workflow, test_pin_validator, test_shape_pin_row_path 1120 passed in 371.93s, exit 0
S3 probe on the new head 7 passed (records output; table above)
CI run 39061446331 on 680e26ab tests pass (12357 passed, 145 skipped, 1 xfailed), package pass, tests-git-bash pass, SonarCloud pass

The 8-file set is test_legs_not_a_list, test_manifest_validator, test_naming_policy, test_validator_child_encoding, test_doctor_validator_verdicts, test_chain_link_fold, test_cli_reference and test_agents_md_codes. 1150 = 611 from round 0, plus 21 new rows, plus 518 adopt tests.

Lane: openreposhape-3 (openRepoShape-3)

Brett Heap and others added 2 commits October 17, 2026 21:49
…st, with `_load_legs_to_check` resolved as RV826 laid it out (#714)

#827 and this branch both changed `templates/assembly-root/scripts/
validate-pins.py`'s `_load_legs_to_check`; no other file conflicted
(`tests/test_adopt_plan_yaml_quoting.py` merged on its own).

Hunk 1, the docstring: #712's paragraph (`project.yaml` named relative to
the root), a blank line, then #714's two paragraphs.

Hunk 2, the body: #712's `raise Refusal("manifest-unreadable",
"project.yaml: not a mapping")`, then this branch's lines after its own
raise: the `declared` read with its `manifest-legs` finding, the
`not_mappings` loop with its `manifest-leg` findings, and the `legs`
line. Git merged the rest itself: `if not legs and not not_mappings:`
with #712's relative `project.yaml: no non-assembly legs declared`.

Resolved with a copy of RV826's `resolve_827_r1.py`. The resolved
function is identical, as text, to RV826's pair resolution in both merge
orders, and the whole file to its landing tree's.

Lane: openRepoShape-4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t are not mappings, `[5, openXdox]`, is said once per entry in every gate, so `validate-pins.py` saying only the first fails a test (#714)

RV826's NIT 1 on #826: the mutant MK, `for leg in not_mappings[:1]:` in
`validate-pins.py`'s `_load_legs_to_check`, passed all 109 tests of this
file, because no row held two such entries. The row `[5, openXdox]`
asserts both `manifest-leg: a leg is not a mapping` lines, each once, in
the naming, manifest and pins gates. MK: `109 passed` before the row,
`1 failed, 111 passed` with it (`[two-entries-pins]`).

Lane: openRepoShape-4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-17T23:17:44Z) W714's reply to RV826's READY (#826 issuecomment-6240291087): the merge round and NIT 1. Head 124bbd7b2a87f093715cf63750935eb5cc250f14, read back from GitHub.

The merge. #827 (#712) landed on main as 9f235da2.

NIT 1, MK: taken. test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence now takes a list of entries, and has a new row [5, openXdox] (two-entries) asserting both manifest-leg lines, each once, in all three gates.

  • MK (for leg in not_mappings[:1]: in validate-pins.py) on the merged tree, over the whole file:
    • before the row: SURVIVED, 109 passed;
    • with the row: KILLED, 1 failed, 111 passed, by [two-entries-pins].
  • The new file has 112 tests (109 + 3).

The run on the merged tree with the NIT 1 row (124bbd7b's bytes):

Merge-trees of 124bbd7b (git merge-tree --write-tree, each head fetched by refs/pull/<n>/head):

main-now c100843b: exit 0, tree f9dc8d49
main-9f235da2 9f235da2: exit 0, tree d175f156
#862 86f41d90: exit 0, tree 61653b28
#824 89ef951c: exit 0, tree 911dbcd9
#824-given 7777c7d0: exit 0, tree 911dbcd9
#823 4cc9ee2a: exit 0, tree c3f801c1
#825 6d5a448f: exit 0, tree 84f5f032
#860 a2b0e48c: exit 0, tree f04bac0c
#846 6a71ff8d: exit 0, tree b3821d3c
#815 05a1ebb3: exit 0, tree 466f722f
#849 25692a37: exit 0, tree 0b9c457f
#814 b050dc71: exit 0, tree 6ca19ec0

NIT 2 (#832's text) and NIT 3 (#827's own merge round) are not #826's. #827 has landed.

Lane: openRepoShape-4

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T23:24:29Z) RV826's delta re-check of #826's merge round (124bbd7b): READY, provided CI tests passes (run 39078382362 was pending). The merge is RV826's own resolution, byte for byte, and the NIT 1 row kills mutant MK.


RV826 delta re-check of #826 (#714) at head 124bbd7b: READY, provided CI tests passes

The merge round and the NIT 1 row both check out. The one gate not yet measured is CI tests (run 39078382362). It started at 23:16:29Z and was still running at 23:22:27Z; it took about an hour last round. Land only once it reports success.

The PR head read 124bbd7b from GitHub, and origin/main read c100843b from both a fresh fetch and ls-remote.

The six checks

  1. The merge matches my resolution.

    • I re-ran git merge --no-commit 9f235da2 on 680e26ab and resolved it with my resolve_827_r1.py (sha256 412296f9b2668205…, the same as lane 4's copy).
    • It conflicted only in templates/assembly-root/scripts/validate-pins.py.
    • My resolved tree is 65d3e20e33745d98c57cc285d2e8d2086859152d, equal to 07412007^{tree}.
    • validate-pins.py has sha256 f4247411abf12240b49986c9… in my rebuild, in 07412007, and in my round-1 RV826-land.
  2. The NIT 1 row kills MK.

    • The new row is two-entries: [5, openXdox], with both manifest-leg lines asserted once each in every gate.
    • MK (for leg in not_mappings[:1]:, validate-pins.py), run over tests/test_legs_not_a_list.py at 124bbd7b: 1 failed, 111 passed in 37.73s.
    • The only failure is test_each_gate_says_a_leg_that_is_not_a_mapping_in_one_sentence[two-entries-pins].
    • Unmutated, that row passes in check 5.
  3. Nothing else changed. git diff 07412007 124bbd7b touches only tests/test_legs_not_a_list.py (16 lines added, 11 removed). The change is NOT_MAPPINGS taking a list of entries plus the two-entries row, and the test looping over the entries. No source file changed after the merge.

  4. The merge with fresh main and the predicted squash.

  5. Targeted run on the head.

    • Files: my 8-file set (test_legs_not_a_list, test_manifest_validator, test_naming_policy, test_validator_child_encoding, test_doctor_validator_verdicts, test_chain_link_fold, test_cli_reference, test_agents_md_codes).
    • Result: 635 passed in 206.43s, exit 0. That is round 1's 632 plus the 3 new rows.
    • Environment as before: stub gh (exit 97), GH_TOKEN=bogus, empty GH_CONFIG_DIR, targeted files only.
  6. CI and SonarCloud on 124bbd7b.

Check Result
SonarCloud Code Analysis pass; gate OK on commit 124bbd7b, analysed 23:16:35Z; 0 open issues
package pass
tests-git-bash pass
tests pending, run 39078382362

Items left

None for #826. If the CI tests job fails, this verdict no longer holds and #826 needs another look. NIT 2 (the text of #832) and NIT 3 (#827's own merge round) are not #826's; #827 has landed as 9f235da2.

Lane: openreposhape-3 (openRepoShape-3)

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 02:08
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 18 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit c15a49e into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/714-legs-not-a-list branch October 18, 2026 02:12
brettheap pushed a commit that referenced this pull request Oct 18, 2026
… so the two exit checks RV862's NIT-5 names are tightened on this branch

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
…xits 1 for `legs: 5` too, and bootstrap's step (b) names each gate's exit 1 with no traceback anywhere (RV862's NIT-5, #832)

Two checks allowed for gates without #826 (#714): the doctor test took
exit 1 or 3 for `legs: 5`, because the standard's naming gate crashed on
it (CANNOT ANSWER), and bootstrap's `FAILED` line took `exit [12]` for the
naming and pins gates, which refused or crashed. Main c15a49e holds #826,
merged into this branch, and every gate now reports such a `legs:` as a
finding, exit 1. So the doctor's exit is 1 for every value, the `FAILED`
line is asserted whole, and the validators run asserts no traceback at
all, where it could assert only none of bootstrap's own.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…gs:` that is not a list, or holds an entry that is not a mapping, is said in #714's words, so the doctor's leg rows wait on the `manifest` row instead of calling the project one with no legs, bootstrap places no leg for it, and bump-leg.py and update-shape.py refuse it by name where each ended in a `TypeError` (#832) (#862)

Four more readers of a project manifest's `legs:` read it as #714 (#826) makes the three gates read it. Each iterated `legs or []`: the doctor's `Context.legs`, the assembly root's `scripts/bootstrap.py`, `bump-leg.py`'s `Project.legs` and `update-shape.py`'s `pull_request_repository`.

`legs: 5` ended each in `TypeError: 'int' object is not iterable`: the doctor's whole report (in `declared_pins`, under the `manifest kinds` row), `make bootstrap` before its first line, `bump-leg.py`, and the re-run `update-shape.py apply --pr` recognises. A scalar `openXdox`, a mapping `{a: b}` or `[5]` was read as a project with no legs:
- the doctor's `legs` row said "a one-repository project has none";
- bootstrap's step (a) said "no submodule legs declared";
- `bump-leg.py` refused `bump-leg-unknown-leg` with "it declares" and nothing after it;
- `update-shape.py` said "declares no assembly leg".

Each now says #714's sentence for what it found, the value spelled by `repr` as the gates spell it, byte for byte with the gates' own `FINDING` lines:

```
manifest-legs: legs is 5, expected a list of `role:`, `repository:` and `path:` mappings
manifest-leg: a leg is not a mapping: 5
```

- **`shape-doctor.py`:**
  - `Context.legs` reads a list only, and `Context.legs_unread` gives the sentences.
  - `legs_unread_row` makes the `legs`, `leg shape files` and `placement` rows `n/a`. Each says the sentence, says the `manifest` row's command names the fault, and names that row in `waits_on`, as `waiting_row` does for a manifest that does not load (#205).
  - `--placement-plan` refuses by name and writes nothing.
  - A list holding legs beside such an entry still has its legs checked.
- **`templates/assembly-root/scripts/bootstrap.py`:** step (a) says the sentence, places no leg for it, and places the legs beside it. Step (b)'s validators refuse, and the exit is theirs.
- **`scripts/bump-leg.py`:** it refuses `bump-leg-manifest-legs`, exit 2, before anything is fetched and in `--dry-run` as well. A list holding legs beside such an entry is refused too: its dry run had previewed a bump the real run's own `validate-manifest.py` always refused.
- **`update-shape.py`:** `update-pr-no-repository` says the sentence.

Two entries that are not mappings are said together, joined by `; `. Absent, null and `[]` are unchanged, and the tests hold each reader to its old sentence.

`templates/assembly-root/scripts/bootstrap.py` is in `shape_materialize.COPIED_VERBATIM`. No digest of it lives in this repository, so nothing is re-pinned here. In a project, `update-shape.py check` reports it `upstream-changed`, and `apply` re-copies and re-pins it.

`tests/test_legs_readers_not_a_list.py` (74 tests) covers each site over `5`, `openXdox`, `{a: b}` and `[5]`, the entry rule (`[openXdox]`, `[null]`, `[5, 6]`), a value holding ESC and the mixed list, and holds absent, null and `[]` to each reader's old sentence. It kills round 0's ten mutants and RV862's eight (each reader's `None` guard and `; ` join, and `update-shape.py`'s stray `: `). With all four files at main's bytes, 56 of its 74 tests fail; the 18 that pass are its two controls and the 16 rows that hold a reader to its old sentence.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant