Skip to content

adopt-project.py: check and execute ask a plan's org: and id: right after its three repository names, so a bad one beside an unanswered entry, a moved source, a refused name or another repository is that one refusal in both, and a pins: entry that is no pin is said as _read_as says it (#1020) - #1125

Draft
brettheap wants to merge 2 commits into
mainfrom
fix/1020-bad-id-first-problem

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

#901 made check and execute name the same first problem among a plan's options. Outside them, the two still named different problems first (#1020, found by lane 1's writer on #901).

  1. One rule, the refusal before what check lists. check refuses a bad org: or id: outright, exit 2, and lists none of its findings. execute refuses at the first of them, but it asked the two only after it had refused four things that check lists as findings: a plan for another repository (plan-not-the-source), a name the naming policy refuses (naming-unclassified), an unanswered entry (plan-unresolved) and a source that has moved on (plan-stale). And check asked the two only where the policy took the plan's names (_manifest_values), so an unsafe org: beside a refused name was a finding about the name, or about another repository. Both commands now ask the two first of the plan's values, right after the three repository names (plan.names(), then _checked_org_and_id): execute in cmd_execute, ahead of _refuse_a_plan_for_another_repository, and check at its head, after the triad advisory. Review round 1, below, moves both to right after the plan's keys, before _work_root. _checked_plan_values no longer asks them, since cmd_execute has.
  2. adopt-project.py check: a plan in a name the policy refuses gets no rename-first remedy, and a source that names no repository is never compared with the plan's recorded source.repository #716's L4-48, decided. An org: that checked_value refuses, held to a repository, was unsafe-value to check and plan-not-the-source to execute. Such an org: names no repository, on GitHub or in the manifest, so it is not compared with the source's. It is unsafe-value in both commands now, held to a repository or not. Once it is mended, a plan for another repository is still plan-not-the-source in both. This reverses three tests in tests/test_adopt_check_plan_name.py, which pinned the old answers on purpose:
    • test_an_unsafe_org_is_unsafe_in_check_and_compared_by_execute_if_held expected plan-not-the-source from execute in its six held rows. As test_an_unsafe_org_is_unsafe_in_check_and_in_execute_held_or_not, all twelve expect unsafe-value from both, in the same words.
    • test_check_compares_an_unsafe_org_where_no_manifest_is_built expected check to compare org: acme labs beside the refused name bad_name: plan-not-the-source and naming-unclassified, exit 1. As test_an_unsafe_org_is_asked_before_the_plans_refused_names, it expects unsafe-value, exit 2, from check and from execute.
    • test_check_asks_an_unsafe_org_first_where_a_kept_submodule_url_is_relative expected plan-not-the-source from execute. It now expects unsafe-value, in check's words.
  3. A pins: entry that is no pin was spelled with repr (`pins:` entry 1 is 7, True, None). adopt-project.py check and execute: one bad plan value is named in different orders, or by two or three findings, and two pin sentences say more than execute does #901 made pins: itself read as _read_as reads it, and an entry reads the same way now: "the number 7", "the boolean true", "null", "a list of 2 item(s)".

Docstrings and comments now say this: _checked_org_and_id, _checked_plan_values, _manifest_values, _held_findings, _not_the_source, _refuse_a_plan_for_another_repository, _read_pins and the #499 block comment. No finding is added or removed: only which problem is named first changes, and the spelling of an entry.

Measured on main at 6da757cb and on this branch, with a stub gh, a bogus token, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false and --local-remote-dir. No run made a leg.

The plan has check on main execute on main both, here
a bad id beside an unanswered entry plan-bad-id plan-unresolved plan-bad-id
a bad id beside a source that has moved on plan-bad-id plan-stale plan-bad-id
a bad id beside a name the policy refuses plan-bad-id naming-unclassified plan-bad-id
a bad id beside another organisation, held to a repository plan-bad-id plan-not-the-source plan-bad-id
no id:, and legs.assembly: north_wind (a derived id the policy refuses too) plan-bad-id naming-unclassified plan-bad-id
an unsafe org: held to a repository (L4-48) unsafe-value plan-not-the-source unsafe-value
an unsafe org: beside an unanswered entry unsafe-value plan-unresolved unsafe-value
an unsafe org: beside a source that has moved on unsafe-value plan-stale unsafe-value
an unsafe org: beside a refused name finding naming-unclassified, exit 1 naming-unclassified unsafe-value
the same, held to a repository finding plan-not-the-source, exit 1 plan-not-the-source unsafe-value

Each "here" row is exit 2 from both, word for word. The last two rows agreed on main. They change because check now asks the two where the policy refuses the names, as execute now does: without that, moving execute alone would have made them disagree. Plans with only one of these problems (an unanswered entry, a moved source, a refused name, another repository) are named as on main by both.

Tests.

  • tests/test_adopt_plan_value_order.py gains 16 rows:
    • the 10 above, through both commands, comparing the first line;
    • four pins: entries that are no pin: number, boolean, null and list;
    • two guard rows, id and org: a leg name checked_value refuses (legs.spec: "-spec") beside a bad id: or org: is that name's unsafe-value in both, so the three names are still asked first.
  • The 14 non-guard rows all fail against 6da757cb's adopt-project.py and pass here. The two guard rows pass on both, by design.
  • Existing rows changed for the new behaviour:
    • test_adopt_check_plan_name.py: the three tests above, and its docstring's L4-48 line.
    • test_adopt_pins.py: a-number.
    • test_adopt_execute_refusals.py: the three entry rows of PIN_ENTRIES.

Mutants, each applied to adopt-project.py, run against the 16 new rows and the L4-48 tests (30 tests), and restored with its sha256 checked (9178964d, the same after each one):

Mutant Killed by
M1: cmd_execute does not ask _checked_org_and_id early 18: the 10 new rows, the 6 held L4-48 rows, and the two other reversed tests
M2: cmd_execute asks it after _refuse_a_plan_for_another_repository 11: the 3 new held rows, the 6 held L4-48 rows, and the two other reversed tests
M3: cmd_check asks plan.names() but not _checked_org_and_id at its head 3: org-and-refused-name, org-held-and-refused-name, test_an_unsafe_org_is_asked_before_the_plans_refused_names
M4: cmd_check asks _checked_org_and_id but not plan.names() at its head 2: both guard rows
M5: a pins: entry that is no pin spelled with repr again 4: the four new entry rows

Gate, on this branch's tree 66845bb3, under a stub gh, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL an empty file, GIT_CONFIG_NOSYSTEM=1, GIT_ALLOW_PROTOCOL and GIT_SSH_COMMAND unset, and EMAIL set. 125 files in six shards. The branch is based on main at 6da757cb. main has since moved to 823624d9 (#1101, #1102), and its diff against 6da757cb is empty in the five files this branch changes.

  • every tests/test_adopt_*.py (68 files, 34 in each of s1 and s2):
    • s1: 2013 passed, 2 skipped (local run on this head).
    • s2: 2560 passed, 1 skipped (local run on this head).
  • the 55 other files that name ADOPT, adopt-project.py or adopter, among them test_repo_hygiene.py and test_agents_md_codes.py (28 files in s3, 27 in s4):
    • s3: 1992 passed, 8 skipped (local run on this head).
    • s4: 1860 passed, 1 skipped (local run on this head).
  • tests/test_shape_doctor.py, s5: 445 passed, 3 skipped (local run on this head).
  • tests/test_scaffold_leg_path_collisions.py, s6: 104 passed (local run on this head).

Merge-trees, git merge-tree --write-tree, run on 2026-10-21 against main at 823624d9 and against each open PR's head that touches adopt-project.py. Every one exits 0 and is clean:

The merged trees were not run through the tests: the merges are clean as text only.

Review round 1 (RV1125's review of 2a53a8e6: one SHOULD and two NITs), head 9549006f, one commit on top of 2a53a8e6:

  • SHOULD-1. A bad id: or org: beside a --work-dir this user cannot write in was plan-bad-id (or unsafe-value) to check and adopt-work-dir-unwritable to execute: check lists the directory as a finding (adopt-project.py check: refuse a kept file at the shape's pin path, and an unwritable --work-dir, before any leg exists #524), and execute refuses it before it opens the source, ahead of the two asks. Both commands now ask plan.names() and _checked_org_and_id(plan) right after the mistyped-plan question, before _work_root, as RV1125 sketched. They read only the plan. The comments in cmd_execute, cmd_check, _checked_org_and_id and _checked_plan_values say so, and the "A REFUSAL OF check'S IS ASKED BEFORE WHAT IT LISTS" paragraph names the work directory among what is asked after. A --work-dir that is a file or cannot be made agrees as well: both ask _work_root after the two now.
  • Two side effects, said here. (1) check printed the Triad advisory, on stdout, once it had opened the source and before it refused a bad org: or id:; the refusal now comes before the source is opened, so it prints nothing on stdout. (2) execute asks the two before _require_filter_repo(), so a bad id: is the refusal even where git filter-repo is missing; check asks no such question.
  • Two more reorders, said here (RV1125b's NIT-B). Besides the --work-dir that is a file, a bad id: or org: is now asked, in check and in execute, before _work_root refuses a --work-dir every user can write in, without the sticky bit (adopt-work-dir-shared), and before a source that has moved on is refused (plan-source-gone). On main at 6da757cb both commands said adopt-work-dir-shared or plan-source-gone of such a plan; here both say plan-bad-id or unsafe-value, so only which refusal comes first changes: the two commands agreed before and agree now. The same plan with no bad id: or org: is refused as before, in both. Measured at 6da757cb and at 9549006f, for a bad id: and for a bad org: beside each. No row pins either yet: see tests/test_adopt_plan_value_order.py: follow-up of PR #1125 (#1020) from its review: four mutants of the head ask (L1, L2, L4, L11) that none of its 85 rows kills, so a bad id: or org: beside a --work-dir that is a file or a shared one, or beside a gone source, is pinned through neither check nor execute #1179.
  • Rows in tests/test_adopt_plan_value_order.py: both_on takes a --work-dir for both commands; test_the_org_or_the_id_is_named_before_an_unwritable_work_dir has a row for a bad id: and one for a bad org:, each beside a --work-dir that is chmod 500, asserting exit 2 from both, the same first problem, and no adopt-work-dir-unwritable. It skips where chmod cannot keep this user from writing (root, Windows), by test_adopt_root_symlinks.unwritable_work.
  • NIT-1. The docstring of test_check_asks_an_unsafe_org_first_where_a_kept_submodule_url_is_relative says it is a regression row: since adopt-project.py: after #901, check and execute still name different first problems for a bad id beside an unanswered entry, a stale source or a refused name, and a pins: entry that is no pin is spelled with repr #1020 check asks org: at its head, so the relative url decides nothing.
  • NIT-2. check prints its findings only at its end, so no finding tells a late ask from one at its head. Its stdout does: the Triad advisory comes after _work_root and the source are opened. test_check_asks_the_org_and_the_id_before_it_says_anything_else (a row for a bad id: and one for a bad org:) asserts that check prints nothing on stdout and refuses first on stderr, and its control, the unedited plan, says the advisory first.
  • Rows against 2a53a8e6's adopt-project.py: the 4 new rows fail (the work-dir rows: execute says adopt-work-dir-unwritable where check says the id or the org; the NIT-2 rows: stdout holds the advisory), and the 2 guard rows pass.
  • Mutants of this round, each applied to the new adopt-project.py, run against test_adopt_plan_value_order.py and test_adopt_check_plan_name.py (85 tests), restored with sha256 checked (412edbda, the same after each one):
Mutant Killed by
N1: check asks the two after the advisory (this PR's place before the round) 2: both NIT-2 rows
N2: check asks them only before _print_what_will_happen (RV1125's K3) 2: both NIT-2 rows
N3: check asks them only before _not_the_source (K9) 2: both NIT-2 rows
N4: execute asks them after the work-dir refusal, before the source is opened 2: both work-dir rows
N5: execute asks them where it did before the round, after _require_filter_repo() 2: both work-dir rows

Left for follow-up issues, not taken here (see #1113 and #1114):

Lane 4 lands nothing outside v1.2.0 until after the v1.2.0 tag, so this stays a draft until then.

Closes #1020

🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)

… right after its three repository names, so a bad one beside an unanswered entry, a moved source, a refused name or another repository is that one refusal in both, and a `pins:` entry that is no pin is said as `_read_as` says it (#1020)

#901 made `check` and `execute` name the same first problem among a plan's options. Outside them, the two still named different problems first (#1020, found by lane 1's writer on #901).

1. **One rule, the refusal before what `check` lists.** `check` refuses a bad `org:` or `id:` outright, exit 2, and lists none of its findings. `execute` refuses at the first of them, but it asked the two only after it had refused four things that `check` lists as findings: a plan for another repository (`plan-not-the-source`), a name the naming policy refuses (`naming-unclassified`), an unanswered entry (`plan-unresolved`) and a source that has moved on (`plan-stale`). And `check` asked the two only where the policy took the plan's names (`_manifest_values`), so an unsafe `org:` beside a refused name was a finding about the name, or about another repository. Both commands now ask the two first of the plan's values, right after the three repository names (`plan.names()`, then `_checked_org_and_id`): `execute` in `cmd_execute`, ahead of `_refuse_a_plan_for_another_repository`, and `check` at its head, after the triad advisory. `_checked_plan_values` no longer asks them, since `cmd_execute` has.
2. **#716's L4-48, decided.** An `org:` that `checked_value` refuses, held to a repository, was `unsafe-value` to `check` and `plan-not-the-source` to `execute`. Such an `org:` names no repository, on GitHub or in the manifest, so it is not compared with the source's. It is `unsafe-value` in both commands now, held to a repository or not. Once it is mended, a plan for another repository is still `plan-not-the-source` in both. This reverses three tests in `tests/test_adopt_check_plan_name.py`, which pinned the old answers on purpose:
   - `test_an_unsafe_org_is_unsafe_in_check_and_compared_by_execute_if_held` expected `plan-not-the-source` from `execute` in its six held rows. As `test_an_unsafe_org_is_unsafe_in_check_and_in_execute_held_or_not`, all twelve expect `unsafe-value` from both, in the same words.
   - `test_check_compares_an_unsafe_org_where_no_manifest_is_built` expected `check` to compare `org: acme labs` beside the refused name `bad_name`: `plan-not-the-source` and `naming-unclassified`, exit 1. As `test_an_unsafe_org_is_asked_before_the_plans_refused_names`, it expects `unsafe-value`, exit 2, from `check` and from `execute`.
   - `test_check_asks_an_unsafe_org_first_where_a_kept_submodule_url_is_relative` expected `plan-not-the-source` from `execute`. It now expects `unsafe-value`, in `check`'s words.
3. **A `pins:` entry that is no pin** was spelled with `repr` (`` `pins:` entry 1 is 7 ``, `True`, `None`). #901 made `pins:` itself read as `_read_as` reads it, and an entry reads the same way now: "the number 7", "the boolean true", "null", "a list of 2 item(s)".

Docstrings and comments now say this: `_checked_org_and_id`, `_checked_plan_values`, `_manifest_values`, `_held_findings`, `_not_the_source`, `_refuse_a_plan_for_another_repository`, `_read_pins` and the #499 block comment. No finding is added or removed: only which problem is named first changes, and the spelling of an entry.

Measured on main at `6da757cb` and on this branch, with a stub `gh`, a bogus token, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `--local-remote-dir`. No run made a leg.

| The plan has | `check` on main | `execute` on main | both, here |
|---|---|---|---|
| a bad id beside an unanswered entry | `plan-bad-id` | `plan-unresolved` | `plan-bad-id` |
| a bad id beside a source that has moved on | `plan-bad-id` | `plan-stale` | `plan-bad-id` |
| a bad id beside a name the policy refuses | `plan-bad-id` | `naming-unclassified` | `plan-bad-id` |
| a bad id beside another organisation, held to a repository | `plan-bad-id` | `plan-not-the-source` | `plan-bad-id` |
| no `id:`, and `legs.assembly: north_wind` (a derived id the policy refuses too) | `plan-bad-id` | `naming-unclassified` | `plan-bad-id` |
| an unsafe `org:` held to a repository (L4-48) | `unsafe-value` | `plan-not-the-source` | `unsafe-value` |
| an unsafe `org:` beside an unanswered entry | `unsafe-value` | `plan-unresolved` | `unsafe-value` |
| an unsafe `org:` beside a source that has moved on | `unsafe-value` | `plan-stale` | `unsafe-value` |
| an unsafe `org:` beside a refused name | finding `naming-unclassified`, exit 1 | `naming-unclassified` | `unsafe-value` |
| the same, held to a repository | finding `plan-not-the-source`, exit 1 | `plan-not-the-source` | `unsafe-value` |

Each "here" row is exit 2 from both, word for word. The last two rows agreed on main. They change because `check` now asks the two where the policy refuses the names, as `execute` now does: without that, moving `execute` alone would have made them disagree. Plans with only one of these problems (an unanswered entry, a moved source, a refused name, another repository) are named as on main by both.

**Tests.**
- `tests/test_adopt_plan_value_order.py` gains 16 rows:
  - the 10 above, through both commands, comparing the first line;
  - four `pins:` entries that are no pin: number, boolean, null and list;
  - two guard rows, `id` and `org`: a leg name `checked_value` refuses (`legs.spec: "-spec"`) beside a bad `id:` or `org:` is that name's `unsafe-value` in both, so the three names are still asked first.
- The 14 non-guard rows all fail against `6da757cb`'s `adopt-project.py` and pass here. The two guard rows pass on both, by design.
- Existing rows changed for the new behaviour:
  - `test_adopt_check_plan_name.py`: the three tests above, and its docstring's L4-48 line.
  - `test_adopt_pins.py`: `a-number`.
  - `test_adopt_execute_refusals.py`: the three entry rows of `PIN_ENTRIES`.

Closes #1020

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

sourcery-ai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes check and execute validate plan repository names, org:, and id: in the same precedence order and with the same refusal behavior, while formatting malformed pins: entries through _read_as instead of Python repr; extensive regression tests cover ordering, unsafe organizations, and diagnostic wording.

Sequence diagram for aligned plan validation in check and execute

sequenceDiagram
    participant User
    participant Command
    participant Plan
    participant Validation

    User->>Command: invoke check or execute
    Command->>Plan: names()
    Command->>Validation: _checked_org_and_id(plan)
    alt org or id refused
        Validation-->>Command: Refusal: unsafe-value or plan-bad-id
        Command-->>User: exit 2
    else values accepted
        alt check
            Command->>Validation: list findings
            Validation-->>Command: findings or success
        else execute
            Command->>Validation: _refuse_a_plan_for_another_repository()
            Command->>Validation: _check_names()
            Validation-->>Command: refusal or execution
        end
        Command-->>User: result
    end
Loading

Flow diagram for unified plan value precedence

flowchart TD
    A[Plan] --> B["plan.names()"]
    B --> C[_checked_org_and_id]
    C -->|refused| D[unsafe-value or plan-bad-id]
    C -->|accepted| E{Command}
    E -->|check| F[List findings]
    E -->|execute| G[_refuse_a_plan_for_another_repository]
    G --> H[_check_names]
    H --> I[Continue execution]
Loading

Flow diagram for human-readable malformed pin diagnostics

flowchart TD
    A[pins entry] --> B{Entry is a mapping?}
    B -->|yes| C[Process as pin]
    B -->|no| D[_read_as entry]
    D --> E[Report plan-pin-malformed with readable value]
Loading

File-Level Changes

Change Details Files
Unified validation order for plan organization and ID values across check and execute.
  • Ask the three repository names first, then validate org: and id: before repository matching, naming-policy checks, unresolved entries, stale sources, and other findings.
  • Move execute validation ahead of repository refusal and remove the duplicate validation from _checked_plan_values.
  • Add head-of-command validation in check, causing either command to refuse invalid values with matching code, wording, and exit status.
  • Preserve the guard ordering so repository names still precede organization and ID validation.
  • Update unsafe-organization behavior so invalid values are never compared as repositories and consistently produce unsafe-value.
  • Revise related tests and add matrix coverage for competing org:/id: problems and refusal ordering.
adopt-project.py
tests/test_adopt_check_plan_name.py
tests/test_adopt_plan_value_order.py
Changed malformed pins: entry diagnostics to use the reader-facing value formatter.
  • Replace Python repr output with _read_as formatting for scalar, null, and list entries that are not pins.
  • Update documentation and existing expectations for number, boolean, null, and list values.
  • Add cross-command tests verifying matching diagnostic wording.
adopt-project.py
tests/test_adopt_execute_refusals.py
tests/test_adopt_pins.py
tests/test_adopt_plan_value_order.py
Updated implementation documentation and regression coverage to describe the new refusal semantics.
  • Refresh docstrings and comments covering manifest extraction, held findings, repository comparisons, and refusal ordering.
  • Add 16 focused value-order and pin-format cases, including guard cases and competing-problem scenarios.
  • Update L4-48 expectations so unsafe organizations consistently win over repository comparison.
adopt-project.py
tests/test_adopt_check_plan_name.py
tests/test_adopt_plan_value_order.py
tests/test_adopt_execute_refusals.py
tests/test_adopt_pins.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1020 Make check and execute name the same first problem when a bad org: or id: appears alongside an unanswered entry, stale source, refused name, or another repository, while deciding the held unsafe-org: case. ✅
#1020 Spell a pins: entry that is not a pin using the same reader-oriented wording as _read_as, rather than Python repr. ✅

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

This was referenced Oct 21, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

RV1125: independent review of #1125 at 2a53a8e6: NOT READY

One SHOULD: a bad id: or org: beside an unwritable --work-dir is still two different first problems, and the new comments say it cannot be. Everything else holds. The 10 rows agree, L4-48 is decided the right way, _read_pins reads as _read_as, and the merge with main 4ed7c7bf is clean and green.

What I measured

All of it was on a scratch merge commit of main 4ed7c7bf and the head 2a53a8e6: commit e394d426, tree 6bb897ee, adopt-project.py sha256 3da1ea61. I compared it with main itself. Both commands ran on each hand-edited plan, with the same --local-remote-dir. The probe environment:

  • a stub gh first on PATH, GH_TOKEN=bogus and an empty GH_CONFIG_DIR;
  • GIT_SSH_COMMAND=false and an https_proxy that goes nowhere;
  • GIT_CONFIG_GLOBAL an empty file and GIT_CONFIG_NOSYSTEM=1.

No run made a leg. The table gives the code each command names first, with check's exit code. On the merge, every bad-id row is exit 2 from both commands, with stderr identical byte for byte.

The plan has main: check / execute merge: both Issue
a bad id beside an unanswered entry plan-bad-id / plan-unresolved plan-bad-id #1020
a bad id beside a source that moved on plan-bad-id / plan-stale plan-bad-id #1020
a bad id beside a refused name plan-bad-id / naming-unclassified plan-bad-id #1020
a bad id beside another org, held plan-bad-id / plan-not-the-source plan-bad-id #1020
no id:, legs.assembly: north_wind plan-bad-id / naming-unclassified plan-bad-id #1020
org: "-evil", held (L4-48) unsafe-value / plan-not-the-source unsafe-value #1020
org: "-evil" beside an unanswered entry unsafe-value / plan-unresolved unsafe-value #1020
org: "-evil" beside a moved source unsafe-value / plan-stale unsafe-value #1020
org: "-evil" beside a refused name finding naming-unclassified (1) / same unsafe-value #1020
the same, held finding plan-not-the-source (1) / same unsafe-value #1020
id: 007 beside an unanswered entry plan-bad-id / plan-unresolved plan-bad-id #1020
org: [a, b], held unsafe-value / plan-not-the-source unsafe-value #1020
a bad id beside a bad date and an unanswered entry plan-bad-id / plan-unresolved plan-bad-id #1020
a bad id, another org (held), moved, unanswered plan-bad-id / plan-not-the-source plan-bad-id #1020
a bad id, another org, held only by source: repository: plan-bad-id / plan-not-the-source plan-bad-id #1020
org: "-evil", held only by source: repository: unsafe-value / plan-not-the-source unsafe-value #1020
a bad id beside pins: [7], follow_ups: 7, a taken split branch, or legs.spec: "-spec" agree on main agree (unchanged) -
a bad id beside paths: 7 finding plan-bad-paths (1) / refused plan-bad-paths unchanged, agree -
a bad id: beside an unwritable --work-dir plan-bad-id / adopt-work-dir-unwritable still differ SHOULD-1
org: "-evil" beside an unwritable --work-dir unsafe-value / adopt-work-dir-unwritable still differ SHOULD-1
a moved source and an unanswered entry finding plan-stale / plan-unresolved unchanged, still differ #1113
another repository and an unanswered entry finding plan-unresolved / plan-not-the-source unchanged, still differ #1113
another repository and a moved source finding plan-stale / plan-not-the-source unchanged, still differ #1113
a refused name and an unanswered entry finding plan-unresolved / naming-unclassified unchanged, still differ #1113

The single-problem plans name the same thing on the merge as on main:

  • an unanswered entry;
  • a moved source;
  • a refused name;
  • another repository;
  • an unheld org: "-evil".

In each, stderr is identical on main and on the merge, apart from the commit hashes of the moved source. So are #1113's four pairs. The PR changes nothing it does not claim to change.

Findings

SHOULD-1: a bad id: or org: beside an unwritable --work-dir still gives two first problems, and the new comments say otherwise.

NIT-1: test_check_asks_an_unsafe_org_first_where_a_kept_submodule_url_is_relative's docstring still opens on RV825's premise. The premise was that _submodule_findings asks org: earlier than _manifest_values. After this PR, check asks it at its head, before either, so the relative url no longer decides anything, and the test now matches the one above it. Say that it is kept as a regression row: since #1020 check asks org: at its head, whatever the submodule urls.

NIT-2: no test pins check's head position against a later one.

  • The survivors: mutants K3 and K9, which ask _checked_org_and_id late in check (before _print_what_will_happen, or before _not_the_source), pass every row.
  • Why: check prints its findings only at the end, so any refusal before that point wins. The position shows only against a refusal raised between the head and the late spot. _check_split_branch and _entry_findings raise none directly.
  • Not a defect; the head placement is the right hygiene. Note it in the docstring, or add a row if such a refusal exists.

L4-48: decided right (no finding)

unsafe-value is the right first problem. Reversing the three rows is justified.

_read_pins: right (no finding)

An entry that is neither a name nor a mapping is now _read_as(row): "the number 7", "the boolean true", "null", "a list of 2 item(s)". That is how pins: itself is said (_plan_key_findings), and how the items of follow_ups: and allow_empty_legs: are said. No doc quotes the old repr text; the only hits outside adopt-project.py are the changed tests. A name entry keeps repr, which is right: it is text, quoted as the other names are. _pin_row_problem's fields are #1114's.

Merge-trees

Tests on the scratch merge e394d426

All runs used the landing gate's environment: a stub gh, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL an empty file, GIT_CONFIG_NOSYSTEM=1, GIT_ALLOW_PROTOCOL and GIT_SSH_COMMAND unset, EMAIL set, and -p no:cacheprovider -o tmp_path_retention_policy=failed.

  • The PR's four files (test_adopt_plan_value_order.py, test_adopt_check_plan_name.py, test_adopt_execute_refusals.py, test_adopt_pins.py): 412 passed.

  • test_cli_reference.py, test_agents_md_codes.py, test_repo_hygiene.py and test_scaffold_leg_path_collisions.py: 274 passed, exit 0.

  • test_shape_doctor.py, in three shards of 154, 153 and 141: 154 passed; 153 passed; 138 passed and 3 skipped (PowerShell, Windows only). In all, 445 passed and 3 skipped, exit 0 in each shard.

  • The PR's new rows and its 8 reversed L4-48 rows, against main 4ed7c7bf's adopt-project.py:

    • all 14 new non-guard rows fail;
    • all 8 reversed rows fail;
    • the 2 guards and the 6 held-to-none rows pass.

    That is the body's claim, against today's main.

Mutants

Each mutant was applied to the merge's adopt-project.py and run against 47 tests:

  • the 16 new rows;
  • the 14 L4-48 tests;
  • 17 probe rows of mine that assert the two commands name the same code.

The file was restored after each one, sha256 3da1ea61...b255 each time.

Mutant Result
K1: execute asks org/id after _check_names killed, 24 (14 PR tests)
K2: execute asks org/id after _refuse_an_unrunnable_plan killed, 34 (18 PR tests)
K3: check asks org/id only before _print_what_will_happen survived, equivalent on stderr (NIT-2)
K4: check asks org/id before plan.names() killed, 2 (the guards)
K5: execute asks org/id before plan.names() killed, 2 (the guards)
K6: _checked_org_and_id asks the id before the org survived: #901's order, shared by both, so it cannot split them; outside this PR
K7: an entry that is no pin spelled with str() killed, 4
K8: execute asks org/id after plan-not-the-source, before _check_names killed, 18 (11 PR tests)
K9: check asks org/id only before _not_the_source survived, equivalent on stderr (NIT-2)

Lane: openreposhape-4 (openRepoShape-4)

… right after the plan's keys and before `_work_root`, so a bad one beside an unwritable `--work-dir` is that one refusal in both (#1020)

RV1125's SHOULD-1 on #1125. A bad `id:` or `org:` beside a `--work-dir`
this user cannot write in was `plan-bad-id` (or `unsafe-value`) to `check`
and `adopt-work-dir-unwritable` to `execute`: `check` lists the directory
as a finding (#524), and `execute` refuses it before it opens the source,
ahead of the two asks, which #1125 had placed after `_work_root`. The
comments in `cmd_execute` and `_checked_org_and_id` said that cannot
happen.

- `cmd_check` asks `plan.names()` and `_checked_org_and_id(plan)` right
  after its mistyped-plan question, before `_work_root`, and `cmd_execute`
  asks them right after `_refuse_a_mistyped_plan`, before `_work_root`.
  Both read only the plan. `execute` now asks them before
  `_require_filter_repo()` too.
- A side effect, said in the body: `check` printed the Triad advisory,
  once it had opened the source, before it refused a bad `org:` or `id:`;
  it prints nothing now, since the refusal comes before the source is
  opened.
- The comments and docstrings that placed the asks "at its head, after the
  triad advisory" or "right after the names" say where they are now.
- tests/test_adopt_plan_value_order.py: `both_on` takes a `--work-dir` for
  both commands; one row per bad `id:` and bad `org:` beside an unwritable
  `--work-dir` (skipped where `chmod` cannot keep this user from writing);
  and, for NIT-2, one row per value that asks `check` alone and holds
  stdout empty, the position being told by the Triad advisory the unedited
  plan prints first.
- tests/test_adopt_check_plan_name.py: the docstring of the kept-submodule
  row says it is a regression row since `check` asks `org:` at its head
  (NIT-1).

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

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

RV1125b: independent review of #1125 at 9549006f: READY

No MUST and no SHOULD. RV1125's SHOULD-1 is fixed, NIT-1 and NIT-2 are done, and no plan that agreed at 2a53a8e6 disagrees now. One NIT below: the head position is pinned against the advisory and the unwritable directory, but not against _work_root's own refusals or a gone source.

What I measured

Scratch merge 68f8fa79 of origin/main 038dafba and 9549006f (tree 2cf183ed, adopt-project.py sha256 333e5b89). git merge-tree exits 0; the tree differs from main only in the PR's five files, and its changed lines equal the PR's diff from 6da757cb. For the round's own change I also merged main with 2a53a8e6 (3606b4b7).

A probe of 56 hand-edited plans (RV1125's 33 and 23 more) ran both commands on main, on main + 2a53a8e6 and on the merge: stub gh, GH_TOKEN=bogus, empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, a dead proxy, empty global git config, --local-remote-dir.

The plan has main and 2a53a8e6: check / execute merge: both
a bad id: beside an unwritable --work-dir (SHOULD-1) plan-bad-id / adopt-work-dir-unwritable plan-bad-id
a bad org: beside an unwritable --work-dir (SHOULD-1) unsafe-value / adopt-work-dir-unwritable unsafe-value
a derived bad id, or a bad id beside a moved source and an unanswered entry, beside an unwritable --work-dir plan-bad-id / adopt-work-dir-unwritable plan-bad-id
legs.spec: "-spec" beside an unwritable --work-dir unsafe-value / adopt-work-dir-unwritable unsafe-value
a bad id:, org: or leg name with git filter-repo off PATH value / filter-repo-missing the value's code
a bad id: or org: beside a --work-dir that is a file, or one every user can write in adopt-work-dir-unwritable or adopt-work-dir-shared, both the value's code
a bad id: or org: beside a gone source (source.local_path moved; with and without a recorded repository to clone again) plan-source-gone, both the value's code

The two side effects, and one more

  • (1) is true and acceptable: for every bad-value plan check's stdout is empty on the merge, where it held the Triad advisory. An unheld org: "-evil" differs from main only there.
  • (2) is true and acceptable: with git filter-repo missing, execute said filter-repo-missing where check named the bad value; both name the value now.
  • Not named in the body, and acceptable: a bad value is now asked before _work_root refuses a shared --work-dir (adopt-work-dir-shared) and before a gone source is refused (plan-source-gone), in both commands. Both agreed before and agree now, and a refused run now makes no work directory and clones nothing again. The body names only the --work-dir that is a file.

Findings

NIT-A: no row pins the head ask against _work_root's own refusals or a gone source. The PR's 85 tests in the two order files pass under L1, L4 and L11 (and L2), which put the asks of one command after _work_root (or after the source is opened). Each makes the two commands disagree on a bad id: or org: beside a --work-dir that is a file or a shared one (and, for L2, beside a gone source): my probe kills them. Fix: add --work-dir-is-a-file and gone-source rows to test_the_org_or_the_id_is_named_before_an_unwritable_work_dir, through both commands. A follow-up is fine.

NIT-B: the body could name the shared --work-dir and the gone source beside "a --work-dir that is a file".

Mutants

Each applied to the merge's adopt-project.py in its own worktree, run against test_adopt_plan_value_order.py and test_adopt_check_plan_name.py (85 tests) and against my probe's 40 bad-value plans asserting that both commands name the same code; restored after each, sha256 333e5b89 every time.

Mutant PR tests (85) probe (40)
L1: check asks after _work_root survive killed, 5
L2: check asks after the source is opened survive killed, 9
L3: check asks after the advisory (the round's old place) killed, 2 (NIT-2 rows) killed, 9
K3: check asks org/id only before _print_what_will_happen killed, 2 (NIT-2 rows) killed, 8
K9: check asks org/id only before _not_the_source killed, 2 (NIT-2 rows) killed, 8
L4: execute asks after _work_root survive killed, 5
L5: execute asks after the unwritable refusal (N4) killed, 2 (work-dir rows) killed, 11
L6: execute asks after the source is opened killed, 2 (work-dir rows) killed, 14
L7: execute asks after _require_filter_repo (N5) killed, 2 (work-dir rows) killed, 17
L8: both ask after _work_root survive survive: both name the work dir, so they still agree
L9: check asks org/id before the three names killed, 2 (guards) killed, 1
L10: execute asks org/id before the three names killed, 2 (guards) killed, 1
L11: check names at the head, org/id after _work_root survive killed, 4
L12: execute org/id at the head, names after _require_filter_repo killed, 2 (guards) killed, 4
L13: check org/id at the head, names after _work_root killed, 2 (guards) killed, 2

RV1125's K3 and K9 die now, as W1020b said.

#1111's rule

test_path_guard_hygiene.py passes on the merge (below). The PR adds no is_file, is_dir, exists, is_symlink or os.path guard (grep of the added lines: 0).

Gate on the merge 68f8fa79

Landing gate's environment: stub gh, GH_TOKEN=bogus, empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL an empty file, GIT_CONFIG_NOSYSTEM=1, GIT_ALLOW_PROTOCOL and GIT_SSH_COMMAND unset, EMAIL set, -p no:cacheprovider -o tmp_path_retention_policy=failed.

  • The PR's four files: test_adopt_plan_value_order.py and test_adopt_check_plan_name.py, 85 passed; test_adopt_execute_refusals.py and test_adopt_pins.py, 331 passed (416 in all).
  • test_adopt_work_dir_refusal.py and test_adopt_shape_pin_and_work_dir.py: 73 passed.
  • test_cli_reference.py, test_agents_md_codes.py and test_repo_hygiene.py: 170 passed.
  • test_path_guard_hygiene.py and test_scaffold_leg_path_collisions.py: 153 passed.
  • test_shape_doctor.py in 8 shards of 56 ids (bash mapfile lists, each non-empty; together all 448 collected ids once): 56; 56; 55 + 1 skipped; 55 + 1 skipped; 55 + 1 skipped; 56; 56; 56. In all 445 passed, 3 skipped, exit 0 in each.

Merge-trees

Main + this PR against each of the 36 other open PRs: 35 clean (exit 0). #1117 conflicts in docs/handbook.html, as it does with main alone, and is clean against this head. For the 12 that touch adopt-project.py (#1105, #1106, #1118 to #1123, #1141, #1143, #1153, #1154), cmd_check and cmd_execute are unchanged on the merged tree from their head down to NamingPolicy.load. No open PR's tests assume check prints the advisory before a refusal (grep of their added test lines). #1119 and #1140 edit tests/test_adopt_root_symlinks.py but leave the unwritable_work fixture this PR imports as it is.

Lane: openreposhape-4 (openRepoShape-4)

This was referenced Oct 22, 2026
@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026
brettheap added a commit that referenced this pull request Oct 23, 2026
…40` end in `\Z`, not `$`, so a pin's `commit:` or `tree_sha256:` that ends in a line break is refused as not 40 or 64 hex by every validator and `adopt-project.py` (#1017) (#1126)

`scripts/repo_shape.py` ends `COMMIT_RE` and `SHA256_RE` in `$`, and every caller reads them with `.match`. In Python `$` also matches just before a trailing line feed, so a pin's `commit:` or `tree_sha256:` that ends in a line break passed as well formed. This makes both patterns refuse it, wherever a value is decided to be valid, and changes nothing else about them.

Measured on main (`2a119f28`, then re-checked on `6da757cbe`; the use sites of both patterns are the same on `823624d93`, and no new copy of either pattern was added). Python 3.12.3:

| pattern | value | `.match` on main |
|---|---|---|
| `COMMIT_RE` / `SHA256_RE` | 40 / 64 hex | a match |
| same | hex and a line feed | a match (the defect) |
| same | hex and a carriage return and line feed | None |
| same | hex and a space | None |

A line feed reaches the patterns through YAML: the double-quoted escape `"<hex>\n"` and a block scalar both read as the hex and a line feed. On main:

- `validate-manifest.py`: `shape.commit` and `shape.digests.tree_sha256` ending in a line feed -> `manifest ok`, exit 0.
- `validate-family.py`: `shape.digests.tree_sha256` in `family.yaml` ending in a line feed passed (`family ok`, exit 0). `shape.commit` did not pass: it exited 1 with `shape-pin-manifest-disagree`, because `contracts/shape-pin.yaml` and `family.yaml`'s `shape.commit` differ only by the line feed, so the same 40 hex is printed on both sides; the finding that names the commit as not 40 hex, `family-shape-commit`, was not raised. A member pin's `tree_sha256` ended in a line feed gave `member-digest-mismatch`, not the malformed finding, and a member pin's `commit` gave `member-commit-unresolvable`, not `member-pin-commit`.
- `validate-pins.py`: a leg pin's commit gave exit 2 `pin-commit-unresolvable`, not `pin-tag-only`; a leg digest gave `pin-digest-mismatch`, not `pin-digest-malformed`; a shape pin's commit gave only `shape-pin-manifest-disagree`, not `shape-pin-commit`.
- `adopt-project.py check` (`_pin_row_problem`): the row read as well formed, with no `plan-pin-malformed`.
- `templates/assembly-root/scripts/bootstrap.py` holds a copy of the commit pattern, `COMMIT_40`, with the same defect: `shape_upstream_notice` printed its notice for a pin ending in a line feed.
- `shape-doctor.py` `pin_commit` returned `"<hex>\n"` for such a leg pin.

Checked and not affected: `bump-leg.py --to` and `family.py --at/--to` go through `checked_value` (a `fullmatch` that already refuses a line feed); `bump-leg.py` line 2212 strips first; `update-shape.py` line 558 reads `git_out`, which is stripped; `shape-doctor`'s `recorded_gitlink` splits fields; `src/openreposhape/standard.py` uses `fullmatch`. The `@(?P<sha>[0-9a-fA-F]{40})` patterns in `bump-leg.py` and `validate-pins.py` are unanchored workflow-ref patterns.

- `scripts/repo_shape.py`: `COMMIT_RE` and `SHA256_RE` end in `\Z`, not `$` (comment added). `\Z` matches only at the very end of the value, so each caller's `.match` refuses a line feed as it already refused a carriage return and line feed, and a space. 40 and 64 digits, either case, are unchanged.
- `templates/assembly-root/scripts/bootstrap.py`: `COMMIT_40` likewise (comment added).
- Nothing else in production changes. No README, doc, handbook or `AGENTS.md` text pins the old pattern bytes (grepped), and `tests/test_pinned_copy_text.py` and every `tests/test_bootstrap*.py` pass unchanged (shard tB and shard tA).
- Rows:
  - new `tests/test_pin_pattern_line_break.py` (46 rows: 42 at first, and 4 added in fix round 1, below): the three patterns x case, length, after-suffix (line feed, carriage return and line feed, space, two line feeds, carriage return, tab) and before-prefix; the holder's gate (four rows and a baseline); the bootstrap upstream notice (two rows); the doctor's `pin_commit` (two rows);
  - `tests/test_pin_validator.py`: 3 rows (leg commit, leg digest, shape-pin commit);
  - `tests/test_manifest_validator.py`: 2 rows (`shape.commit`, shape digest) and a helper `shape_of`;
  - `tests/test_adopt_pins.py`: 2 `malformed()` cases (`commit-line-break`, `digest-line-break`);
  - `tests/test_shape_doctor.py`: 1 `ROOT_PIN_FAULTS` param (`a-commit-ending-in-a-line-break`).

Run with the two production files of main checked out, on a head with the same tree as this one (git 2.43.0, `EMAIL=service@opensoft.dev`):

- the new file, `test_pin_validator.py`, `test_manifest_validator.py`, `test_adopt_pins.py`: **16 failed, 307 passed** in 294.83s. The 16:
  - new file (9): `line-feed-COMMIT_RE`, `line-feed-SHA256_RE`, `line-feed-bootstrap-COMMIT_40`, `test_a_member_pin_commit_that_ends_in_a_line_break_is_refused`, `test_a_member_pin_digest_that_ends_in_a_line_break_is_malformed`, `test_a_family_shape_commit_that_ends_in_a_line_break_is_a_finding`, `test_a_family_shape_digest_that_ends_in_a_line_break_is_a_finding`, `test_the_upstream_notice_is_silent_over_a_pin_that_ends_in_a_line_break`, `test_the_doctor_reads_a_pins_commit_as_the_validators_do[40-hex-and-a-line-feed]`;
  - `test_pin_validator.py` (3): a commit, a digest and a shape-pin commit that end in a line break;
  - `test_manifest_validator.py` (2): a shape commit and a shape digest;
  - `test_adopt_pins.py` (2): `plan-pin-malformed-commit-line-break`, `plan-pin-malformed-digest-line-break`;
- `test_shape_doctor.py -k a_root_pin_that_does_not_read`: **1 failed, 12 passed, 436 deselected** in 57.84s (`a-commit-ending-in-a-line-break`).

With the change the same files pass (see the runs).

| mutant | failed | passed |
|---|---|---|
| M1: both `repo_shape` patterns back to `$` | 11 | 35 |
| M2: only `COMMIT_RE` back to `$` | 7 | 39 |
| M3: only `SHA256_RE` back to `$` | 4 | 42 |
| M4: only the bootstrap `COMMIT_40` back to `$` | 2 | 44 |

Re-measured in fix round 2, at `4c1f80cf`, on the 46-row file. The first round measured its 42 rows: 7/35, 4/38, 3/39 and 2/40. M1 is M2 and M3 together.

Each is killed by at least one row, and each pattern is killed by a row of its own. Every mutant was undone: the production files are byte for byte (`cmp`) their backups of this change.

git 2.43.0 (`/usr/bin/git`):

- shard tA, 35 files: **2782 passed, 8 skipped** in 4138.49s, rc=0;
- shard tB, 39 files: **2630 passed, 6 skipped** in 4532.76s, rc=0;
- trio (`tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`): **170 passed** in 37.48s, rc=0.

The two shards are 74 targeted files: every file that names a pin code (`pin-tag-only`, `pin-digest-*`, `shape-pin-*`, `manifest-shape-*`, `family-shape-*`, `member-pin-*`, `update-pin-*`, `neutral-pin-*`, `plan-pin-*`, `not 40/64 hex`) or one of `COMMIT_RE`, `SHA256_RE`, `COMMIT_40`, every `tests/test_bootstrap*.py` (13 of 13), `test_shape_doctor.py`, 6 of the 19 `test_update_shape*.py` files, `test_family.py`, `test_scaffold_e2e.py`, and the pin tests. They replaced a wide run (226 importer files, about 27k tests) that was killed by PID under load (load average 72 on 32 cores, 2% in 11 minutes); that run is not evidence. The new file and the four edited files are all inside the shards. The other 13 `test_update_shape*.py` files, which name no pin code, ran as a third run (480 passed, 3 skipped in 441.06s, rc=0), and `tests/test_scaffold_leg_path_collisions.py` as a fourth (104 passed in 4.35s, rc=0). No full suite was run.

git 2.47.1 (`git --version` printed first in each log):

- the new file, `test_pin_validator.py`, `test_manifest_validator.py`, `test_adopt_pins.py`: **323 passed** in 208.37s, rc=0.
- `test_shape_doctor.py -k a_root_pin_that_does_not_read`: **13 passed, 436 deselected** in 52.85s, rc=0.

Refreshed after the fix round, against the re-fetched origin/main:

- **Fix round 2, 2026-10-22:**
  - origin/main `038dafba9dbedd9405b316cc749a04dffe02bb36` (#1111) is merged into the branch as `c7da27dc2`, cleanly.
  - The head `4c1f80cf4db6215d8861bd43cd57f72df957d82a` merges with main at its own tree, `9884a99aeba2a55e486a355ae3483d8c932b4f68`: the seven files of this PR.
  - Of the 28 open pull requests, #1125 (head `2a53a8e6`) still touches `tests/test_adopt_pins.py`, and its merge-tree with this head is clean, tree `a026a192669a86abfaed59fbcce2fd1179bb0894`.
  - The bullets below are fix round 1's.
- `git merge-tree --write-tree origin/main HEAD` against `4ed7c7bf581dceb75f816f30f76b776c2b8917f0` (merge base `6da757cbe`), with this PR's head `03b2b97b9d3cccc42b4703cb2f7c978693488266`: clean, rc=0, tree `95d242489e6a8d6ea2be8c454a5e6655922d5712`. The difference between that tree and origin/main is exactly the seven files of this PR.
- The branch stays on `6da757cbe`. Since then three PRs landed. #1101 and #1102 touched `README.md`, `docs/cli.md`, `docs/handbook.html`, `scaffold-project.py`, `templates/assembly-root/project.yaml`, `templates/family-root/family.yaml`, `tests/readme-cap.d/915.txt`, `tests/test_scaffold_reuse_seeded_legs.py` and `tests/test_visibility_field_words.py`. #1091 (`4ed7c7bf5`) touched `adopt-project.py`, `tests/test_adopt_root_shape_paths.py` and `tests/test_adopt_root_symlinks.py`. None is a file of this PR, and `adopt-project.py` holds the same 8 uses of `COMMIT_RE` and `SHA256_RE` as before.
- Of the 23 open pull requests (listed with `gh pr list --state open --json number,headRefName,headRefOid,files`, this one included), one other touches a file of this PR: #1125 (head `2a53a8e64a0cce17ab84cd1bf5949bcb6fbd4484`) touches `tests/test_adopt_pins.py`. `git merge-tree --write-tree 2a53a8e HEAD` is clean, rc=0, tree `5fbd1eaafdf94004ead7142ae9132dc79dc4d1e5`.

One new commit on top, `03b2b97b9d3cccc42b4703cb2f7c978693488266`, after the independent review of `ae30dbd0e0eecfb7092c5ef257da2367d114fc62` (not ready on one must). It changes `tests/test_pin_pattern_line_break.py` and nothing else; no production file changes.

- **Composite asserts (SonarCloud `python:S9073`, the two issues at lines 164 and 173).** `member_pin` and `holder_shape` ended in `assert commit and digest, text`. Each value now has its own assert, with a message that names it (`fixture drift: no member pin commit`, `no member pin tree_sha256`, `no shape commit`, `no shape tree_sha256`).
- **Two callers had no row of their own,** so a `.rstrip("\n")` before the match in either survived every row. The new rows (the file now has 46):
  - `test_a_neutral_pin_commit_that_ends_in_a_line_break_is_refused`: the neutral product's pin in `validate-pins.py`, exit 2, `REFUSED neutral-pin-tag-only`. On main the commit passed `COMMIT_RE` and the run exited 1 with `neutral-pin-source-unreadable`, git refusing the name;
  - `test_a_neutral_pin_digest_that_ends_in_a_line_break_is_malformed`: the same pin's `tree_sha256`, exit 1, `neutral-pin-digest-malformed`. On main it was recomputed and compared (`neutral-pin-digest-mismatch`);
  - `test_a_recorded_source_commit_that_ends_in_a_line_break_is_a_finding`, two rows (`submodules`, `answered-paths`): a record row's `source_commit:` in `validate-manifest.py`, exit 1, `manifest-submodule-commit` and `manifest-answered-commit`. On main: `manifest ok`.
- **The four rows fail on main.** The new file with main's two production files checked out: **13 failed, 33 passed** (the nine of the first round, and these four).
- **Mutants,** each applied alone to the committed head, the file restored afterwards (sha256 `94f125c5f5e4b8a0` for `validate-pins.py` and `260c4742fe422597` for `validate-manifest.py`, the same as before the mutant, and clean against HEAD; `repo_shape.py` and `bootstrap.py` unchanged at `623af12131447f95` and `4102d375f4c9bc38`). The new file only:

  - S1, `.rstrip("\n")` on the neutral pin's commit before the match (`validate-pins.py` line 1790): **1 failed, 45 passed**, killed by the neutral-commit row;
  - S1b, the same on the neutral pin's recorded digest before the match (line 1800): **1 failed, 45 passed**, killed by the neutral-digest row;
  - S2, `.rstrip("\n")` on `source_commit` before the match (`validate-manifest.py` line 1264): **2 failed, 44 passed**, killed by both `source_commit` rows;
  - before this round S1 and S2 survived.
- **Runs, at head `03b2b97b9d3cccc42b4703cb2f7c978693488266`, worktree clean at each START:**
  - git 2.43.0: the new file, `tests/test_pin_validator.py` and `tests/test_manifest_validator.py`: **175 passed** in 58.03s, rc=0;
  - git 2.43.0: the trio: **170 passed** in 39.29s, rc=0;
  - git 2.43.0, extra: `tests/test_neutral_pin_validator.py` (the new rows use its helpers): **39 passed** in 99.62s, rc=0;
  - git 2.47.1 (`git --version` printed first): the new file: **46 passed** in 19.32s, rc=0.
  - The shards, the other git 2.47.1 runs and the extras above ran at `ae30dbd0e`; the two production files and the four other test files are the same at this head.

This round is one merge of main and one new commit, after rc1126's re-check: READY at `03b2b97b`, with two prose NITs (#1126 issuecomment-6340973564).

- **Merge of main:** `038dafba` (#1111), as `c7da27dc2`, with no conflict.
- **NIT 1:** `4c1f80cf4` changes only the module docstring of `tests/test_pin_pattern_line_break.py`. It now says what main did:
  - `validate-manifest.py` passed both a `shape.commit` and a `shape.digests.tree_sha256` that end in a line feed;
  - `validate-family.py` passed only the digest. Its commit gave `shape-pin-manifest-disagree` with the same hex on both sides, because `contracts/shape-pin.yaml` holds the commit without the line feed.
  - No code or row changes.
- **NIT 2:** the Mutants table above is re-measured on the 46-row file.
- **Runs at `4c1f80cf4`, git 2.43.0:**
  - the file and `tests/test_path_guard_hygiene.py`: **95 passed** (46 + 49);
  - with main's two production files checked out: **13 failed, 33 passed**, as before.

Other `^...$` patterns are used with `.match` on values read from files or arguments: `PROJECT_ID_RE`, `DATE_RE` and `QUALIFIED_RE` (`validate-manifest.py`, `validate-family.py`), `REPOSITORY_RE` (`update-shape.py`), `PIN_ARG_RE` (`shape_materialize.py`), and `NAME_RE`, `REF_RE` and `PATH_RE` (`setup-project.py`). They have the same `$` property and are outside the two patterns this issue names, so they are left for #1139, filed from this PR's review.

Review: lane 5's independent re-check, by a reviewer that wrote none of #1126: READY at `4c1f80cf`, the head that lands (issuecomment-6344730185), with no MUST, SHOULD or NIT. It checked the delta from `03b2b97b`, which rc1126 passed (issuecomment-6340973564). CI (Brett Heap's ruling 27): H0 is the head 4c1f80c itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 038dafb; and the local gate passed on its predicted squash tree df676e7 over main 8679c80: the adopt_pins, manifest_validator, pin_pattern_line_break, pin_validator, shape_doctor, repo_hygiene, agents_md_codes, cli_reference sets, 944 passed, 3 skipped. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green".

Closes #1017

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

This branch has not been deployed

No deployments
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