Repository navigation
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
brettheap wants to merge 2 commits into
Conversation
… 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>
Reviewer's GuideThe PR makes Sequence diagram for aligned plan validation in check and executesequenceDiagram
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
Flow diagram for unified plan value precedenceflowchart 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]
Flow diagram for human-readable malformed pin diagnosticsflowchart 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]
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
RV1125: independent review of #1125 at One SHOULD: a bad What I measuredAll of it was on a scratch merge commit of main
No run made a leg. The table gives the code each command names first, with
The single-problem plans name the same thing on the merge as on main:
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. FindingsSHOULD-1: a bad
NIT-1: NIT-2: no test pins
L4-48: decided right (no finding)
|
| 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>
|
|
RV1125b: independent review of #1125 at No MUST and no SHOULD. RV1125's SHOULD-1 is fixed, NIT-1 and NIT-2 are done, and no plan that agreed at What I measuredScratch merge A probe of 56 hand-edited plans (RV1125's 33 and 23 more) ran both commands on main, on main +
The two side effects, and one more
FindingsNIT-A: no row pins the head ask against NIT-B: the body could name the shared MutantsEach applied to the merge's
RV1125's K3 and K9 die now, as W1020b said. #1111's rule
Gate on the merge
|
…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>



#901 made
checkandexecutename 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).checklists.checkrefuses a badorg:orid:outright, exit 2, and lists none of its findings.executerefuses at the first of them, but it asked the two only after it had refused four things thatchecklists 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). Andcheckasked the two only where the policy took the plan's names (_manifest_values), so an unsafeorg: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):executeincmd_execute, ahead of_refuse_a_plan_for_another_repository, andcheckat its head, after the triad advisory. Review round 1, below, moves both to right after the plan's keys, before_work_root._checked_plan_valuesno longer asks them, sincecmd_executehas.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 recordedsource.repository#716's L4-48, decided. Anorg:thatchecked_valuerefuses, held to a repository, wasunsafe-valuetocheckandplan-not-the-sourcetoexecute. Such anorg:names no repository, on GitHub or in the manifest, so it is not compared with the source's. It isunsafe-valuein both commands now, held to a repository or not. Once it is mended, a plan for another repository is stillplan-not-the-sourcein both. This reverses three tests intests/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_heldexpectedplan-not-the-sourcefromexecutein its six held rows. Astest_an_unsafe_org_is_unsafe_in_check_and_in_execute_held_or_not, all twelve expectunsafe-valuefrom both, in the same words.test_check_compares_an_unsafe_org_where_no_manifest_is_builtexpectedcheckto compareorg: acme labsbeside the refused namebad_name:plan-not-the-sourceandnaming-unclassified, exit 1. Astest_an_unsafe_org_is_asked_before_the_plans_refused_names, it expectsunsafe-value, exit 2, fromcheckand fromexecute.test_check_asks_an_unsafe_org_first_where_a_kept_submodule_url_is_relativeexpectedplan-not-the-sourcefromexecute. It now expectsunsafe-value, incheck's words.pins:entry that is no pin was spelled withrepr(`pins:` entry 1 is 7,True,None). adopt-project.pycheckandexecute: one bad plan value is named in different orders, or by two or three findings, and two pin sentences say more thanexecutedoes #901 madepins:itself read as_read_asreads 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_pinsand 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
6da757cband on this branch, with a stubgh, a bogus token, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=falseand--local-remote-dir. No run made a leg.checkon mainexecuteon mainplan-bad-idplan-unresolvedplan-bad-idplan-bad-idplan-staleplan-bad-idplan-bad-idnaming-unclassifiedplan-bad-idplan-bad-idplan-not-the-sourceplan-bad-idid:, andlegs.assembly: north_wind(a derived id the policy refuses too)plan-bad-idnaming-unclassifiedplan-bad-idorg:held to a repository (L4-48)unsafe-valueplan-not-the-sourceunsafe-valueorg:beside an unanswered entryunsafe-valueplan-unresolvedunsafe-valueorg:beside a source that has moved onunsafe-valueplan-staleunsafe-valueorg:beside a refused namenaming-unclassified, exit 1naming-unclassifiedunsafe-valueplan-not-the-source, exit 1plan-not-the-sourceunsafe-valueEach "here" row is exit 2 from both, word for word. The last two rows agreed on main. They change because
checknow asks the two where the policy refuses the names, asexecutenow does: without that, movingexecutealone 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.pygains 16 rows:pins:entries that are no pin: number, boolean, null and list;idandorg: a leg namechecked_valuerefuses (legs.spec: "-spec") beside a badid:ororg:is that name'sunsafe-valuein both, so the three names are still asked first.6da757cb'sadopt-project.pyand pass here. The two guard rows pass on both, by design.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 ofPIN_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):cmd_executedoes not ask_checked_org_and_idearlycmd_executeasks it after_refuse_a_plan_for_another_repositorycmd_checkasksplan.names()but not_checked_org_and_idat its headorg-and-refused-name,org-held-and-refused-name,test_an_unsafe_org_is_asked_before_the_plans_refused_namescmd_checkasks_checked_org_and_idbut notplan.names()at its headpins:entry that is no pin spelled withrepragainGate, on this branch's tree
66845bb3, under a stubgh,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBALan empty file,GIT_CONFIG_NOSYSTEM=1,GIT_ALLOW_PROTOCOLandGIT_SSH_COMMANDunset, andEMAILset. 125 files in six shards. The branch is based onmainat6da757cb.mainhas since moved to823624d9(#1101, #1102), and its diff against6da757cbis empty in the five files this branch changes.tests/test_adopt_*.py(68 files, 34 in each of s1 and s2):ADOPT,adopt-project.pyoradopter, among themtest_repo_hygiene.pyandtest_agents_md_codes.py(28 files in s3, 27 in s4):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 againstmainat823624d9and against each open PR's head that touchesadopt-project.py. Every one exits 0 and is clean:mainat823624d9: tree728fb9e4. At review round 1's head9549006f, againstmainat038dafba(it took adopt-project.py:checknames what the clone's.githolds as git keeps refs, and asks a pin's file through the links the root keeps, so a link into.gitor onto a pin's file is refused before any leg or adopted with the copy beside it (#904) #1091): clean, tree2cf183ed.makefile/is tested, README saysmakeat the root reads a keptGNUmakefileormakefileinstead, and a docstring says the shape's Makefile alone holdsCONTRACTS_DIR(#1061) #1123 (5c3908f8), adopt-project.py: anorg/repo--sourcetoo long for the disk is refused as a path is, naming no GitHub repository and offering no url, so--source <300 a's>/Thingis no longer offeredhttps://github.com/<300 a's>/Thing, which can never clone (#1003) #1122 (e75587e7), adopt-project.py:execute's NEXT printspython3 scripts/bootstrap.pywhere GNU make at the root reads a keptGNUmakefileormakefile, and its NOTE names that file and what it takes formake bootstrapto run there (#1047) #1121 (c04bb710), adopt-project.py:plan'ssourceline names the source's branch as_shownnames a path, so a bidi control, a line boundary or a byte that is not UTF-8 in it is spelled and no longer reaches the terminal raw (#1035) #1120 (c55e55e3), adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119 (bdcf9952), adopt-project.py, shape_materialize.py and tests: a named bad link left out of the copy and pin findings takes no other path's finding with it, and the docstrings and the materializer's refusal name the #979 exception and what helps it (#1089) #1118 (c60066cb), adopt-project.py: under a one-byte locale such as Latin-1,checkreads a plan written in UTF-8 whose source is there, asking the plan's UTF-8 bytes where the spelling as written names nothing, rather than refusingplan-source-gone(#1078) #1106 (504a1dd6), shape_materialize.py and the pinned AGENTS-shape.md: where the root keeps aGNUmakefileor amakefile, which GNU make reads first, the Makefile collision follow-up no longer says the shape's make targets arrive with the merge, and the pinned file saysmakereads the first of the three and the block reaches it only once that file holds it (#1048) #1105 (5a4de7f7) and adopt-project.py:checknames what the clone's.githolds as git keeps refs, and asks a pin's file through the links the root keeps, so a link into.gitor onto a pin's file is refused before any leg or adopted with the copy beside it (#904) #1091 (160de118).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), head9549006f, one commit on top of2a53a8e6:id:ororg:beside a--work-dirthis user cannot write in wasplan-bad-id(orunsafe-value) tocheckandadopt-work-dir-unwritabletoexecute:checklists the directory as a finding (adopt-project.pycheck: refuse a kept file at the shape's pin path, and an unwritable--work-dir, before any leg exists #524), andexecuterefuses it before it opens the source, ahead of the two asks. Both commands now askplan.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 incmd_execute,cmd_check,_checked_org_and_idand_checked_plan_valuessay so, and the "A REFUSAL OFcheck'S IS ASKED BEFORE WHAT IT LISTS" paragraph names the work directory among what is asked after. A--work-dirthat is a file or cannot be made agrees as well: both ask_work_rootafter the two now.checkprinted the Triad advisory, on stdout, once it had opened the source and before it refused a badorg:orid:; the refusal now comes before the source is opened, so it prints nothing on stdout. (2)executeasks the two before_require_filter_repo(), so a badid:is the refusal even wheregit filter-repois missing;checkasks no such question.--work-dirthat is a file, a badid:ororg:is now asked, incheckand inexecute, before_work_rootrefuses a--work-direvery 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 at6da757cbboth commands saidadopt-work-dir-sharedorplan-source-goneof such a plan; here both sayplan-bad-idorunsafe-value, so only which refusal comes first changes: the two commands agreed before and agree now. The same plan with no badid:ororg:is refused as before, in both. Measured at6da757cband at9549006f, for a badid:and for a badorg: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 badid:ororg:beside a--work-dirthat is a file or a shared one, or beside a gone source, is pinned through neitherchecknorexecute#1179.tests/test_adopt_plan_value_order.py:both_ontakes a--work-dirfor both commands;test_the_org_or_the_id_is_named_before_an_unwritable_work_dirhas a row for a badid:and one for a badorg:, each beside a--work-dirthat ischmod 500, asserting exit 2 from both, the same first problem, and noadopt-work-dir-unwritable. It skips wherechmodcannot keep this user from writing (root, Windows), bytest_adopt_root_symlinks.unwritable_work.test_check_asks_an_unsafe_org_first_where_a_kept_submodule_url_is_relativesays it is a regression row: since adopt-project.py: after #901,checkandexecutestill name different first problems for a bad id beside an unanswered entry, a stale source or a refused name, and apins:entry that is no pin is spelled with repr #1020checkasksorg:at its head, so the relative url decides nothing.checkprints 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_rootand the source are opened.test_check_asks_the_org_and_the_id_before_it_says_anything_else(a row for a badid:and one for a badorg:) asserts thatcheckprints nothing on stdout and refuses first on stderr, and its control, the unedited plan, says the advisory first.2a53a8e6'sadopt-project.py: the 4 new rows fail (the work-dir rows:executesaysadopt-work-dir-unwritablewherechecksays the id or the org; the NIT-2 rows: stdout holds the advisory), and the 2 guard rows pass.adopt-project.py, run againsttest_adopt_plan_value_order.pyandtest_adopt_check_plan_name.py(85 tests), restored with sha256 checked (412edbda, the same after each one):checkasks the two after the advisory (this PR's place before the round)checkasks them only before_print_what_will_happen(RV1125's K3)checkasks them only before_not_the_source(K9)executeasks them after the work-dir refusal, before the source is openedexecuteasks them where it did before the round, after_require_filter_repo()8bd1e05fofmainat038dafbaand9549006f(tree2cf183ed, clean, not pushed), in the landing gate's environment, six shards: the PR's four test files, 416 passed;test_adopt_work_dir_refusal.pyandtest_adopt_shape_pin_and_work_dir.py, 73 passed;test_cli_reference.py,test_agents_md_codes.py,test_repo_hygiene.py,test_path_guard_hygiene.pyandtest_scaffold_leg_path_collisions.py, 323 passed;test_shape_doctor.pyin three shards of 150, 150 and 148 ids (448 in all): 150 passed, 150 passed, 145 passed and 3 skipped. 0 failures.test_path_guard_hygiene.pypasses on the merge: its three guards from adopt-project.py:checknames what the clone's.githolds as git keeps refs, and asks a pin's file through the links the root keeps, so a link into.gitor onto a pin's file is refused before any leg or adopted with the copy beside it (#904) #1091 are onmainnow, and the test is green there.checkprinted the advisory before a refusal would fail, and none was found in this repository's tests.Left for follow-up issues, not taken here (see #1113 and #1114):
adopt-project.py:
checkandexecutestill name different first problems for four pairs with no bad id: stale and unanswered, another repository and unanswered, another repository and stale, a refused name and unanswered #1113:checkandexecutestill name different first problems for four pairs that have no bad id ororg::Each is
check's finding order againstexecute's refusal order, and is the same onmainand here.adopt-project.py:
_pin_row_problemspells a pin field that is not text with Python's repr (commit: 7,True), where_read_asgives the number 7 and the boolean true #1114:_pin_row_problemspells a pin field that is not text with Python'srepr(`commit: 7`,True).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)