Repository navigation
validate-manifest.py, validate-family.py, family.py and update-shape.py: an id:, a date or a repository: ending in a line feed is refused by name, and a pin's source_repository: that is no path or owner/repo name is named as the pin's (#1139) - #1166
Conversation
…py: an `id:`, a date or a `repository:` read from a file that ends in a line feed is refused by name, and a shape pin's `source_repository:` is refused in the pin's name (#1139) `$` also matches just before a trailing line feed, and every caller read these patterns with `.match`, so a value a YAML file held as the value and a line feed (a double-quoted `\n`, or a block scalar) passed as well formed. Measured on main 038dafb: `elected_on:` and a leg's `repository:` were `manifest ok`; `family.yaml`'s `id:` and `created_on:` were `family ok`; `project.yaml`'s `id:` was only a topic finding and a member's `repository:` a `member-repository-name` of a project "'IRRS\n'"; `family.py add` mounted and committed a member whose `project.yaml` id ended in one; and `update-shape.py` refused a pin's `source_repository:` so written as `unsafe-value` of `--upstream`, a flag nobody passed. `DATE_RE` and `QUALIFIED_RE` in both validators, `family.py`'s `MEMBER_RE` (held to the validators' spelling by `test_family_member_name.py`), `shape_advisory.py`'s `DATE_RE` and `update-shape.py`'s `REPOSITORY_RE` end in `\Z`, as #1017 ends `COMMIT_RE` and `SHA256_RE`. `PROJECT_ID_RE` keeps its `$`, since every id refusal prints it and the validators run against each project's own `repo_shape.py`: the three readers of an id from a file ask it with `fullmatch` (`manifest-id`, `family-id`, `member-bad-id`). `open_upstream` takes the pin it read the value from, and `update-upstream-unresolvable` and `checked_value`'s `unsafe-value` name the pin's `source_repository:`. No code is added or renamed. `test_update_shape_unreadable.py`'s stand-in for `open_upstream` takes the pin as well and hands it to the real one. Closes #1139 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideData structuresAnchor the end-sensitive date and repository-name patterns at the actual end of values, and retain the public ID-pattern spelling while requiring whole-value matches for IDs read from files.
APIExtend upstream resolution to accept the source pin's path context, so invalid pin values and unsafe-value refusals identify the pin field while explicit --upstream errors retain their existing naming.
Uses of the APIPass the pin path only when no --upstream argument was supplied, preserving the distinction between a pin-derived source and an explicitly supplied upstream.
TestsUpdate the upstream-opening stand-in for the added optional pin-path argument.
Keep the family validator's source-text assertion aligned with its end-anchored date pattern.
Add regression coverage for line-feed-terminated values, pin-specific refusal messages, and the inventory of end-sensitive patterns and ID-pattern readers.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Independent review of #1166 (#1139) for lane openRepoShape-5 Reviewers: rv1166, then rv1166c (Opus 5.5), read-only, at head Verdict: READY. There are no MUST or SHOULD findings.
There are 9 NITs or follow-ups, below. Findings 1-7 are the partial review's, now measured where they were not. Findings 8 and 9 are new. What held (measured)
Findings
Logs
Lane: openreposhape-5 (openRepoShape-5) |
…of `PROJECT_ID_RE` it is not listed for fails by file and method, so a new `match` reader of an id read from a file, which takes a trailing line feed, is caught when it is written The first guard in this file held the four pattern names this change ends in `\Z`, and nothing held `PROJECT_ID_RE`, which keeps its `$` and is read with `fullmatch` where an id comes from a file. A new `PROJECT_ID_RE.match` reader of a file would have passed every gate (#1166's review, finding 7). `ID_READERS` lists the six (module, method) pairs that call it at this head, with the calls each holds and the reason a line feed cannot reach it: the three `fullmatch` readers of an id from a file, and the three `match` readers of argv (`checked_value` refuses a line feed after them) or of the plan's id (refused by the project's own validate-manifest.py, or as `unsafe-value` for the id derived from `legs.assembly`). The guard reads every tracked `.py` outside `tests/` with `ast`, so it runs nothing. No production file changes. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
id:, a date or a repository: read from a file that ends in a line feed is refused by name, and a shape pin's source_repository: is refused in the pin's name (#1139)id:, a date or a repository: ending in a line feed is refused by name, and a pin's source_repository: that is no path or owner/repo name is named as the pin's (#1139)
|
Re-check of #1166 (#1139) NIT round for lane openRepoShape-5 READY at Re-checker rc1166b, read-only: no commit, push or GitHub write. I checked only the delta since the review's head There are no MUST and no SHOULD findings. The guard does what the body says, and it fails when it should. Every review item the body says it fixed is fixed, except where noted below. There are four NITs. NIT 2 is a false justification that the body, the test file and the commit message all repeat. FindingsMUST: none. SHOULD: none. NIT 1: a new
NIT 2: for
NIT 3: the body's "Merge-trees" section is a snapshot that main has since passed.
NIT 4: the guard reads spellings, not meaning (optional).
Review items the body says it fixedThe numbers are the review's, in issuecomment-6352173084.
Checked and true
Not checked
(Posted by lane openRepoShape-5's coordinator. rc1166b is lane 5's independent sonnet re-checker; it wrote none of #1166. NIT 1 and NIT 2 are fixed in the body before #1166 lands, so the squash message carries neither; NIT 3 and NIT 4 need no change.) Lane: openreposhape-5 (openRepoShape-5) |
…w pins path-twice's wording of whose file it puts back, the index's or HEAD's as the index holds none, a row pins the value check's reading of HEAD's where the deletion of `.gitmodules` is staged, and `_listed_as_git_reads`'s docstring says it returns nothing where the work tree's file is on disk (#1181) (#1208) It follows #1124 (for #1077), which landed as `dea964a`; W1181r restacked it onto main with only its own change (#1208 issuecomment-6371837537). It is the follow-up of RV1124b's review of #1124 (see #1150 for the neighbouring follow-ups from the first review). ## What changed - `tests/test_validate_family_index_gitmodules.py` - **PT7 (NIT 1).** In `test_1077_path_twice_records_its_put_back_for_the_next_finding`, after the existing `` `{put_back}` `` assertion, a second assertion pins the whole put-back wording: `back first from {GONE[how][0]}, `{put_back}`, so that every other section stays`. It rides on the two existing rows, `[deleted]` (the index's) and `[deletion-staged]` (HEAD's, as the index holds none). The row's docstring says path-twice names whose file it puts back. - **S1c (NIT 2).** A new row, `test_1077_a_value_head_gives_where_the_deletion_is_staged_puts_head_s_back`. `update = !true` is committed in `members/IRRS`'s section and the deletion of `.gitmodules` is staged, so the index holds no file and git reads HEAD's. The row checks the premise (`git submodule status` exits 128), then that `found_alike(holder, *MODES)` returns the one finding as the issue gives it, whole. It types the two lines in order, runs `git add -- .gitmodules` and a plain `git commit`, and checks that `!true` is not in HEAD, `head_paths` is `{ROW, OTHER_ROW}`, `git status --porcelain` is empty, and `ends_well` passes (the gate in every mode, and a plain clone). - `templates/family-root/scripts/validate-family.py` (NIT 3): the docstring of `_listed_as_git_reads` only. It opens "Where the work tree's .gitmodules is gone from disk, ..." and says it returns `(b'', '')` where the work tree's file is on disk. The script has no pinned copy and no checked-in digest. Its AST is equal to `8dfa32bd7`'s once that one docstring is dropped (sha256 `63f4b9d5...` there, `30abb4ea...` here). ## Counts - The rows file has 16 rows: 15 old and 1 new. PT7's assertion adds no row. - On this head: 16 passed. - Gate, 14 files, one pytest run on this head: **777 passed** (exit 0). The files are the rows file, `tests/test_repo_hygiene.py`, the eight other test files #1124 changed (`test_family_add_refusals`, `test_family_gitmodules_values`, `test_family_hand_mount_git_rm_lines`, `test_family_holder_bootstrap_texts`, `test_family_holder_remedies_compose`, `test_family_unnamed_gitlink`, `test_validate_family_remedies_typed`, `test_validate_family_unnamed_mounts`), the three more that name the functions and codes it touched (`test_family_add_spelling_leftovers`, `test_family_bootstrap_gate_order`, `test_validate_family_typed_states`), and `test_pinned_copy_text.py`. - The same 14 files on a scratch merge commit of `origin/main` (`961640c68`) and this head: **778 passed** (exit 0). The one extra id is a new row in `test_repo_hygiene.py` on `main`. ## Mutants Each mutant was applied to `validate-family.py` in a scratch worktree of this head, run against the new rows file (16 rows) and a copy of #1124's original 15-row file (31 rows together), and restored with its sha256 checked (`30abb4ea...` before and after). | Mutant | Edit | New file (16 rows) | #1124's 15 old rows | |---|---|---|---| | PT7 | path-twice's `f"from {whose}, ` written `f"from the index's, ` | 1 failed: `[deletion-staged]`; `[deleted]` passes | 15 passed (survives) | | S1c | `_listed_as_git_reads` reads `spec = f":{GITMODULES}"` alone, with no fallback to HEAD's | 1 failed: the new S1c row | 15 passed (survives) | ## Merge-trees `git merge-tree --write-tree` of each ref against this head was clean: - `origin/main` (`961640c68`) - #1124's head (`8dfa32bd7`) - #1166's head (`5102b4294`) and #1172's head (`5aa0f116d`), the two other open PRs that touch `validate-family.py`; and the head of #1172's base branch (`b75babee3`) No other open PR touches `tests/test_validate_family_index_gitmodules.py`. Closes #1181 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-23T20:36:20Z) Follow-up filed: #1229, rc1166b's NITs 1, 2 and 4 in tests/test_value_pattern_line_break.py, the ID_READERS reason for adopt-project.py's --id and the guard's wording (issuecomment-6371408915); it applies once #1166 has landed. Nothing in it blocks #1166. Lane: openreposhape-5 (openRepoShape-5) |
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 6 days and 22 hours by commenting |
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rlier apply kept, read `unchanged`, as a module a written copy imports from and as an importer of a written one, so a plain apply that stops either on its import is refused (#1082) (#1133) **#1082: `update-shape.py apply` refuses, before any write, every run that would leave a copy importing a name from a copy that does not define it, wherever the kept copy came from: this run's `--accept-local`, or an earlier run's.** Fix round 1 (`53791e45`, on a merge of main `038dafba`) answers rv1133's review: its SHOULD 1 (the known limit was inside this Goal) is fixed here rather than filed, and its SHOULD 2 and NIT are taken. All probes are offline. `gh` is a stub that exits 97, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, and git is 2.43.0. The tool is main's `update-shape.py` (sha256 `ca47f90e`, blob `a7ae7505`, the same at `2a119f28`, `6da757cb`, `4ed7c7bf` and `038dafba`). Scripts and logs are in lane 5's scratch `v118/w1082/` (`tools/probe1023.sh`, `tools/probe1075.sh`, `probe-*`) and, for fix round 1, `v120/f1133/` (`tools/knownlimit.sh`, `tools/probe1075.sh`, `limit-*`, `probe-*1075`). **#1075's `untraced`, on main's own history.** #1075 has now landed, so main itself holds the case. - A holder cut at `6476c170` has a `bootstrap.py` with no `untraced`. Its own edit to that file is committed. - `apply --at 2a119f2 --accept-local scripts/bootstrap.py --branch shape/first` exits 0. That was main before #1075, and its `siblings.py` takes no `untraced`. - `check --at 6da757c` reads `unchanged scripts/bootstrap.py` and `upstream-changed scripts/siblings.py`. #1075 changed `siblings.py` alone, and `bootstrap.py` is blob `3c1060d3` at both commits. - `apply --at 6da757c --branch shape/second` exits 0, prints `copied scripts/siblings.py`, and commits. - Then `scripts/siblings.py --help` exits 1 with `ImportError: cannot import name 'untraced' from 'bootstrap'`. - The one-step run, `apply --at 6da757c --accept-local scripts/bootstrap.py`, exits 2 with `scripts/bootstrap.py (kept) lacks untraced, which scripts/siblings.py imports from it`. The holder is unchanged. **#1023's `member_make_environment`.** A holder is cut at `f1946d04`, the parent of #1023's `9e154d87`. Its `bootstrap.py` has no `member_make_environment`, and its `siblings.py` does not name it. Its own edit to `bootstrap.py` is committed. - **Main's history alone does not reach the gap for this name.** #1023 defined the name in `bootstrap.py` and added it to `siblings.py`'s import in one commit. `git log -S member_make_environment` over the two files names `9e154d87` alone. - So `check --at <main>` reads `both scripts/bootstrap.py`. - `apply --at <main>` is `update-conflict`. - `apply --at <main> --accept-local scripts/bootstrap.py` is refused already: `scripts/bootstrap.py (kept) lacks member_make_environment, untraced, which scripts/siblings.py imports from it` at `6da757cb`, and `lacks member_make_environment` at `2a119f28`. The holder is unchanged. - **With #1023 landed in two halves, as #1045 and #1075 landed `untraced`, it does reach it.** T1 is a commit on main with `siblings.py` as it was at `f1946d04`. That is `7c695d71` on `2a119f28`, and `3d5a7eda` on `6da757cb`. - `apply --at T1 --accept-local scripts/bootstrap.py --branch shape/first` exits 0. - `check --at <main>` reads `unchanged scripts/bootstrap.py` and `upstream-changed scripts/siblings.py`. - `apply --at <main> --branch shape/second` exits 0, copies `siblings.py`, and commits. - `scripts/siblings.py --help` then exits 1 with `ImportError: cannot import name 'member_make_environment' from 'bootstrap'`. - Measured against both `2a119f28` and `6da757cb`. **The reverse direction: a copy an earlier run kept, as the importer (rv1133's SHOULD 1).** rv1133 measured it with main's tool and with `aa7ddf59`'s; fix round 1 measured it again with main's (`limit-m/`). - A family holder is cut at `4ed7c7bf`. Its own edit to `siblings.py` adds `from bootstrap import told_past`, which the upstream's `siblings.py` does not import, and is committed. `siblings.py --help` still exits 0. - `apply --at U1 --accept-local scripts/siblings.py --branch shape/first` exits 0. U1 is main plus an unrelated change to `validate-family.py`. - U2 then renames `told_past` in `bootstrap.py` alone. `check --at U2` reads `upstream-changed scripts/bootstrap.py` and `unchanged scripts/siblings.py`. - `apply --at U2 --branch shape/second`, with no `--accept-local`, exits 0, prints `copied scripts/bootstrap.py`, and commits. `siblings.py --help` then exits 1 with `ImportError: cannot import name 'told_past' from 'bootstrap'`. - This run's own write breaks a copy that worked before it, so it is the Goal's case. The one-step run, `--accept-local scripts/siblings.py`, was already refused by #604 with `scripts/bootstrap.py (written) no longer defines told_past, which scripts/siblings.py (kept) imports from it`. `update-shape.py`, `_refuse_kept_copies_missing_names` (#536, #604): - **Each written `.py` copy is asked against its module as this run leaves it, whoever kept it.** The reading is `_copies_as_left`, `_written_copies_lacking` and `_names_as_left`. The module may be: - **kept by this run's `--accept-local`:** as before, the line says `(kept)`; - **an `unchanged` row whose pin records the project's own bytes**, which is a copy an earlier run kept that the upstream has not changed since. The line says `(kept by an earlier run)`; - **the upstream's own bytes at the target:** written by this run (an `upstream-changed` row or an `--add` file), `already-at-target`, or `unchanged` with the target's bytes. The line says `(the upstream's)`. No shipped copy meets this today: the reader rows of `tests/test_update_shape_kept_helper.py` find every name each shipped importer takes defined beside it. So it refuses only a target whose own copies disagree. - **A copy an earlier run kept is asked as an importer too (fix round 1).** #604 asked only a copy this run's `--accept-local` keeps against a module this run writes (`_kept_copies_importing_dropped`). The rows `_copies_as_left` reads as `kept by an earlier run` are now passed beside the kept ones, under a new side, `WRITTEN_UNDER_EARLIER`. - Its line says who kept the importer: `scripts/bootstrap.py (written) no longer defines told_past, which scripts/siblings.py (kept by an earlier run) imports from it`. `KEPT_IMPORTERS` maps each `(written)` side to its importer's word, so a `(kept)` importer's line is unchanged. - `_names_dropped` still limits the refusal to names this run takes away: a name the module's bytes on disk define and its written bytes do not. A copy an earlier run kept whose names all stay, or that takes a name the module lacked before too, still applies. - An `unchanged` copy holding the upstream's own bytes is still not asked as an importer: that pair is two of the target's own copies. - **The early `if not kept: return` is gone.** A run with no `--accept-local` is asked too. The guard still stands after `update-conflict`, `update-local-drift` and `update-rewrite-symlink`, and before `confirm` and any write. - **The refusal's code is kept, and so are its words where every line is one this run's `--accept-local` makes.** - A `(kept)` or `(written)`-under-`(kept)` refusal prints byte for byte what main prints. In fix round 1, with the head `53791e45`: `cmp` of the masked one-step logs gives 1312 bytes for #1075's `(kept)` refusal and 1331 bytes for the `told_past` `(written)` refusal, both identical to main's tool. A #604 row now pins that opening (SHOULD 2). - Where a line is of another kind, the refusal opens with words true of every kind of line, now including a `(written)` line under a copy an earlier run kept: `a copy imports a name from a copy beside it whose bytes, as this run leaves them, do not define it, and this run writes one of the two`. - **The remediation gives each kind its exit.** - **For a copy an earlier run kept,** it says why `check` reads it `unchanged`, then gives two exits. As a module that lacks a name: merge the upstream's version by hand so it defines every name its line names. As an importer of a name the written module drops (fix round 1): change it so it no longer takes any name its `(written) no longer defines` line names. Then commit, and run again with `--accept-local on each copy an earlier run kept that you changed, whose row then reads locally-modified`. Or write the upstream's bytes over it (the remedy's own `git show` line names that copy), commit, and run again. All four exits are tested end to end, and each checks the state `check` then reads: `locally-modified` after the first, `already-at-target` after the second. - **For two of the upstream's own copies,** it says `run this again with an --at whose copies agree`. - **The known limit is gone.** The docstring paragraph that named it, and the row that pinned it, are replaced by the rows below that hold the refusal. `KEPT_LEAD`, the code and every `(kept)` line are as main has them. `tests/test_update_shape_kept_helper.py`: - **A fixture, `earlier_standard`,** models #1023 in two halves in the suite's own clone, from A's bytes, with no historical commit: - `before-1023`: the holder's `bootstrap.py` spells `member_make_environment` by #950's `member_make_env`, and its `siblings.py` defines its own; - a holder cut there; - `bootstrap-1023`: A's `bootstrap.py` back; - `siblings-no-name`: the control; - A is the second half. - **Fix round 1 adds `kept-no-name` to `dropping_standard`,** a branch off `kept-base` that changes `validate-family.py` and adds or takes away no name. A holder cut at `kept-base` keeps its own `bootstrap.py`, which imports `DROPPED_NAME` from `repo_shape.py`, through an earlier apply there (`kept_by_an_earlier_apply`, shared with `kept_earlier`); `dropped` is the next apply's target. This is rv1133's `told_past` scenario in the file's own fixtures. - **Rows, in the file's style and reusing its helpers.** - `refusal()` gains `left=` and `at_target=`, and `assert_stops_on_the_import` gains `script=`. - Fix round 1: `assert_the_kept_copy_starts_as_before` and `assert_the_exit_applies` are shared by the `--accept-local` rows and the earlier-kept rows. - No assert joins conditions with `and` or `or` (205 asserts, checked on the syntax tree). - #1075's docstring line (`six names`) is kept as main has it. - **Main merged.** `git merge origin/main` (`038dafba`) was clean: merge commit `7aee5efa`, tree `27a2785e`, the same as `git merge-tree --write-tree aa7ddf5 038dafb`. The fix is one new commit on it, `53791e45`. Nothing was amended, rebased or forced. - **SHOULD 1 (the known limit).** Fixed as above, rather than filed. New rows: - `test_an_earlier_kept_copy_is_asked_when_the_next_apply_drops_its_name`: end to end, `check` reads the kept `bootstrap.py` `unchanged` and `repo_shape.py` `upstream-changed`; the plain apply to `dropped` is refused with nothing written, one detail line, the general opening, and that copy's exits, and none of the other kinds' exits. Its premise is measured: the kept `bootstrap.py` beside `dropped`'s `repo_shape.py` stops on the import. - `test_an_earlier_kept_copy_importing_what_this_run_keeps_applies` (2 controls): a name `repo_shape.py` lacked before too, and one the written `repo_shape.py` still defines. Both apply with no `--accept-local`, and the kept copy starts after the run exactly where it started before. - `test_either_exit_for_an_earlier_kept_importer_applies` (2): each exit the remedy gives that copy, with the state `check` reads after it; the apply then writes `repo_shape.py`, and `bootstrap.py` starts. - `test_an_importer_an_earlier_run_kept_is_asked_against_a_written_module` (2, in process): the line, the opening, the exits, and the `again` wording alone and beside a copy this run keeps. - `test_an_importer_holding_the_targets_bytes_is_not_asked` replaces `test_an_importer_this_run_leaves_alone_is_not_asked`, whose second param pinned the limit. - **The scenario itself, with the head's tool** (`limit-h/`): the second apply exits 2 with `scripts/bootstrap.py (written) no longer defines told_past, which scripts/siblings.py (kept by an earlier run) imports from it`, the holder is unchanged, and `siblings.py --help` exits 0. The one-step, first-apply and `check` logs match main's tool once paths and shas are masked. #1075's probe (`probe-h1075/`) is refused at its second apply too, with `(kept by an earlier run) lacks untraced`. - **SHOULD 2.** `test_keeping_a_copy_that_imports_a_name_the_run_drops_is_refused`, a #604 end-to-end `(written)` row, now asserts `startswith(KEPT_LEAD)`. rv1133's R2 is killed (below). - **NIT.** The exits rows (`test_either_exit_the_remedy_names_applies` and the new one) check that `check` reads `locally-modified` after the change exit and `already-at-target` after the drop exit. The first exit's id now says the edit was changed by hand, as the remedy's `changed` does. The new test file (185 rows) was run against main's `update-shape.py` (`038dafba`, sha256 `ca47f90e`, in a scratch clone): **17 failed, 168 passed** (`base-main.log`). Against `aa7ddf59`'s: **13 failed, 172 passed** (`base-aa7.log`). With the change: **185 passed**. | row | on main | on `aa7ddf59` | |---|---|---| | `test_an_earlier_kept_copy_is_asked_when_the_next_apply_writes_its_importer` | `assert 0 == 2`: the second apply copies `siblings.py` | fails: the old opening | | `test_with_branch_an_earlier_kept_copy_is_refused_before_any_branch` | `assert 0 == 2` | passes | | `test_either_exit_the_remedy_names_applies` (2 params) | `assert 0 == 2`: the apply the remedy follows is not refused | passes | | `test_a_written_copy_is_asked_against_its_module_as_this_run_leaves_it` (4 params) | no refusal | fails: the old opening | | `test_each_kind_of_module_is_given_its_own_exit` (2 params) | no refusal | 1 fails: its remedy says `merged`, not `changed` | | `test_every_kind_of_line_in_one_run_is_one_refusal` (2 params) | only the `(kept)` line | fails: the old opening | | **new:** `test_an_earlier_kept_copy_is_asked_when_the_next_apply_drops_its_name` | `assert 0 == 2`: the plain apply copies `repo_shape.py` | the same | | **new:** `test_either_exit_for_an_earlier_kept_importer_applies` (2 params) | `assert 0 == 2`: the apply the remedy follows is not refused | the same | | **new:** `test_an_importer_an_earlier_run_kept_is_asked_against_a_written_module` (2 params) | no refusal; beside a kept copy, only its `(kept)` line | the same | These pass on main and after: the one-step row (`test_the_one_step_run_is_refused_as_it_was`, which pins the old opening), the #604 row that now pins `KEPT_LEAD`, the controls (`test_an_earlier_kept_copy_that_defines_every_name_applies`, the two new earlier-kept importer controls, the four module-defines-every-name controls), and `test_an_importer_holding_the_targets_bytes_is_not_asked`. Each was applied alone in a scratch clone, run on the test file, and restored by checkout with its sha256 checked. **Fix round 1, at `53791e45` (185 rows):** | mutant | result | |---|---| | N1, the earlier-kept rows not passed to `_kept_copies_importing_dropped` (`{}`), as `aa7ddf59` had it | killed, 5 failed: the 5 new rows that fail on main | | N2, an earlier-kept importer called `(kept)` in its line | killed, 3 failed: the end-to-end refusal row and both in-process rows | | N3, an earlier-kept importer given the `(written)`-under-`(kept)` side | killed, 3 failed: the same three | | N4, the `KEPT_LEAD` opening kept for a `(written)` line under a copy an earlier run kept | killed, 3 failed: the same three | | N5, the change exit for an earlier-kept importer dropped | killed, 3 failed: the same three | | N6, the `unchanged` why and the `--accept-local on each copy an earlier run kept` exit given to an earlier-kept module only | killed, 3 failed: the same three | | N7, the module, not the earlier-kept importer, named for `git show` | killed, 3 failed: the same three | | N8, the "what the upstream's own copy of that file imports instead" clause for a `(written)`-under-`(kept)` line only | killed, 1 failed: the end-to-end refusal row | | N9, every `unchanged` row asked as an importer, the target's own bytes too | killed, 1 failed: `test_an_importer_holding_the_targets_bytes_is_not_asked` | | N10, the old general opening (`a copy this run writes or keeps imports ...`) | killed, 10 failed | | rv1133's R2, a `(written)`-only refusal given the general opening (`sides <= {KEPT_MODULE}`) | killed, 1 failed: `test_keeping_a_copy_that_imports_a_name_the_run_drops_is_refused` (SHOULD 2); it survived at `aa7ddf59` | | the first round's M1, the old `kept` set restored | killed, 17 failed: the 17 rows that fail on main | | the first round's M3, the guard called after `_write_copies` | killed, 15 failed: every end-to-end refusal row, the 3 new ones among them | | the first round's M7, earlier-kept and upstream's own swapped | killed, 12 failed | No survivor. Every mutant was restored by checkout with its sha256 checked (`5b386f8b`), and each clone's status was clean at the end (`mutants1.out`, `mutants2.out`, `mut-*.log`). **The first round, at `7b447602` (179 rows):** | mutant | result | |---|---| | M1, the old `kept` set restored (`left = dict.fromkeys(kept, KEPT_MODULE)`) | killed, 12 failed: the 12 rows that fail on main | | M2, `unchanged` rows asked as importers (against the modules this run writes) but the written importers asked against the old `kept` set only | killed, 14 failed | | M3, the guard called after `_write_copies` (refused after a write) | killed, 12 failed: every end-to-end refusal row sees `copied` | | M4, a copy an earlier run kept called `(kept)` | killed, 5 failed | | M5, the upstream's own modules not asked | killed, 5 failed | | M6, the new opening always used | killed, 1 failed: the one-step row's old opening | | M7, earlier-kept and upstream's own swapped | killed, 6 failed | | M8, the `--accept-local on each copy an earlier run kept` exit dropped | killed, 3 failed | | M9, a written module read from its bytes on disk | killed, 4 failed | | M10, the `unchanged` sentence dropped | killed, 2 failed | All runs went through lane 5's `run9.sh`, with the stub `gh`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null` and `EMAIL` exported. No more than two ran at once. The full suite was not run. **Fix round 1, at `53791e45`** (each `START` line records the head and `dirty=0`; logs in `v120/f1133/`): | run | git | result | log | |---|---|---|---| | the new file, `tests/test_update_shape_kept_helper.py` | 2.43.0 | **185 passed** in 100.27s | `own243.log` | | the same | 2.47.1 | **185 passed** in 95.67s | `own247.log` | | `tests/test_update_shape.py`, `tests/test_update_shape_messages.py`, `tests/test_update_shape_paste_lines.py` | 2.43.0 | **144 passed** in 181.71s | `runC.log` | | `tests/test_update_shape_apply_lines_and_headers.py`, `tests/test_update_shape_merged_away.py`, `tests/test_update_shape_rewrite_symlink.py`, `tests/test_update_shape_landing.py` | 2.43.0 | **163 passed** in 223.09s | `runD.log` | | `tests/test_update_shape_add_not_a_directory.py` | 2.43.0 | **13 passed** in 33.11s | `runF.log` | | `tests/test_path_guard_hygiene.py` and the trio (`tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`) | 2.43.0 | **219 passed** in 54.58s | `runE.log` | No failure and no skip in any run at this head. The two runs of the new file went through `run9.sh` on 2026-10-22 at 13:31Z, the next three on 2026-10-22 from 15:16Z to 15:21Z, and `runF.log` at 15:23Z; each `START` line reads `head=53791e45...` and `dirty=0`. Of the 19 `tests/test_update_shape*.py` files, these 9 ran at this head. The other 10 did not run at this head: all 10 ran at the first round's head `7b447602` in shard A or B (below), over the first round's `update-shape.py`, which this round's commit changed. The full suite was not run. **The first round, at `7b447602`** (tree `e0e2d19e`, the same tree as `aa7ddf59`): the new file 179 passed on git 2.43.0 and on git 2.47.1; shard A (29 files) 2031 passed, 5 skipped; shard B (29 files) 1814 passed, 5 skipped; the trio 170 passed. No failure in any run. Read at `53791e45`, after a re-fetch of main (`origin/main` is still `038dafba`, the second parent of the branch's merge commit `7aee5efa`): - **`git merge-tree --write-tree origin/main HEAD`** exits 0 and writes tree `bfa8c1d4`, which is `HEAD`'s own tree: the branch holds `038dafba` as an ancestor, so the merge adds nothing. - **The merge commit `7aee5efa`** (parents `aa7ddf59` and `038dafba`) has tree `27a2785e`, the same tree as `git merge-tree --write-tree aa7ddf5 038dafb`, which exits 0. It was clean. - **Open pull requests:** 37 were listed with `gh pr list --state open --limit 100 --json number,headRefName,headRefOid,files`. None but this one touches `update-shape.py` or `tests/test_update_shape_kept_helper.py`, so there is no other head to merge against. The list is `open-prs-d1.json` in `v120/f1133/`. - `git diff --name-only origin/main HEAD` names those two files and no other. rc1133 re-checked `53791e45` (#1133 issuecomment-6347273216): READY, with three NITs. This round takes all three, in one new commit, `a7bfee1b`, on `53791e45`. It was pushed as a fast-forward of the branch; nothing was amended, rebased or forced. The commit changes `tests/test_update_shape_kept_helper.py` alone (+7, -4). `update-shape.py` is `53791e45`'s, sha256 `5b386f8b`, so the fix and every measurement in the sections above are unchanged. - **NIT 1 (taken): a refusal in both directions at once now has its opening pinned.** `_missing_name_refusal` opens with `KEPT_LEAD` where every line is one this run's `--accept-local` makes. A run with a `(kept) lacks` line and a `(written) no longer defines` line is such a run, but `test_both_directions_in_one_run_are_one_refusal` pinned its lines, its clause and its remedy, not its opening. rc1133's mutant O1 (`lead = (KEPT_LEAD if len(sides) == 1 and sides <= {KEPT_MODULE, WRITTEN_MODULE} else LEFT_LEAD)`) therefore survived the 185 rows. The row now asserts `result.stderr.startswith(KEPT_LEAD), result.stderr`, right after `assert_refused_writing_nothing`, and its docstring says why the opening is that one. - **NIT 2 (taken): the Merges section lacked #1166.** Its row is in "Merges at `a7bfee1b`" below, and it replaces the Merges section's "Open pull requests" line. - **NIT 3 (taken): the title** named only the first round's direction, a written copy asked against its module. It now names the copy an earlier apply kept in both roles: as the module a written copy imports from, and as an importer of a written module. It is 249 characters and ASCII. rc1133's 242-character rewrite was close, but its "a plain apply that would break it" is true only where the kept copy is the importer: where it is the module, the apply breaks the written importer. The new title says "either". ### Mutant O1, before and after Applied alone in this worktree and restored by checkout, sha256 `5b386f8b` checked, status clean afterwards. The mutant is rc1133's (`mutate_rc.py`, copied unchanged). | test file | run | result | |---|---|---| | `53791e45`'s, without the new assert | the row alone, with O1 applied | **survives**: 1 passed, 184 deselected (`o1-before-row.log`). rc1133 measured the whole file: 185 passed | | `a7bfee1b`'s, with the new assert | the whole file, with O1 applied | **killed**: 1 failed, 184 passed (`o1-after-file.log`). The failure is `test_both_directions_in_one_run_are_one_refusal` at the new assert: the opening read `a copy imports a name from a copy beside it whose bytes, as this run leaves them, do not define it, ...` | No other mutant was needed: the round adds no behaviour and no new row, and the earlier rounds' 14 + 10 mutants stand at an unchanged `update-shape.py`. ### Rows that fail on main None, and that is measured, not assumed. The new assert pins an opening the head keeps exactly as main prints it, so it cannot fail on main. rc1133 had that only by reading, so it is measured here: the row, new assert included, run with main's `update-shape.py` (`origin/main` `961640c6`, sha256 `ca47f90e`) swapped into the worktree, gives **1 passed** (`main-row.log`); the file was restored to `5b386f8b` afterwards. The assert guards that opening against a later regression like O1, as SHOULD 2's assert guards the one-step opening against R2. ### Runs at `a7bfee1b` All through lane 5's `run9.sh`, each `START` line at `head=a7bfee1b...` and `dirty=0`, with the stub `gh` and `EMAIL` exported; two ran at once, and the full suite was not run. No failure and no skip. Logs are in lane 5's scratch `v124/n1133/`. | run | git | result | log | |---|---|---|---| | `tests/test_update_shape_kept_helper.py` and `tests/test_path_guard_hygiene.py` | 2.43.0 | **234 passed** (185 + 49) in 61.57s | `own243.log` | | `tests/test_update_shape_kept_helper.py` | 2.47.1 (first on PATH, printed in the log) | **185 passed** in 48.73s | `own247.log` | The commit changes no function, no path guard and no refusal text, so no other test file names anything it changed. The test file still has no assert that joins conditions with `and` or `or` (206 asserts, checked on the syntax tree), no non-ASCII byte, and its widest line is 79 characters. ### Merges at `a7bfee1b` Read after a re-fetch of main: `origin/main` is `961640c6` (#1155, which adds `docs/release-notes/v1.2.0.md` and nothing else). It touches neither of this PR's files. - **`git merge-tree --write-tree origin/main HEAD`** exits 0 and writes tree `98765a8b`. The branch does not hold `961640c6`, and the predicted merge differs from `HEAD` by `docs/release-notes/v1.2.0.md` alone. - **#1166** (`fix/1139-anchored-patterns`, head `2593952c95`, opened 2026-10-22T17:31:57Z) touches `update-shape.py`, at lines well apart from `_missing_name_refusal` (rc1133's reading), and does not touch the test file. `git merge-tree --write-tree a7bfee1 2593952` exits 0 and writes tree `67858a0b`. Against `53791e45` the same command writes `37498506`, rc1133's tree, which is reproduced. rc1133's tests on the predicted merge of `53791e45` and #1166 (`c5a98365`: this file, `test_value_pattern_line_break.py`, `test_update_shape_unreadable.py`, `test_path_guard_hygiene.py`) were 311 passed; they were not re-run at `a7bfee1b`, which changes one assert and a docstring in a test file #1166 does not touch. - **Open pull requests:** 41 were listed with `gh pr list --state open --limit 100 --json number,headRefName,headRefOid,files`. Besides this one, only #1166 touches `update-shape.py`, and none but this one touches `tests/test_update_shape_kept_helper.py`, so no other head needs a merge-tree. The list is `open-prs.json`. Review: lane 5's independent re-check, by a reviewer that wrote none of #1133: READY at `a7bfee1b` (issuecomment-6352252707), with no MUST, SHOULD or NIT left. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 a7bfee1 passed tests, tests-git-bash and SonarCloud; the head 08339c9 is a clean update-branch merge of main 105b5da, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree b7073af over main 105b5da: the update_shape_kept_helper, repo_hygiene, agents_md_codes, cli_reference sets, 356 passed. 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 #1082 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ject.py`'s argv `--id` the reason that holds (`match`, then validate-manifest.py refuses a line feed), a row proves it, and the guard says a new reader needs a row, `fullmatch` too (#1243) `tests/test_value_pattern_line_break.py` says only what is true of the readers of `PROJECT_ID_RE`: no row of `ID_READERS` gives a reason that does not hold for the tool it names, and the guard `test_every_reader_of_the_id_pattern_is_in_the_table` tells the author of a new reader, in its docstrings and in its failure message, that the one way to pass is a row in `ID_READERS`, whichever method the reader calls. This is #1229, the follow-up of #1166 (#1139): rc1166b's NIT 1, NIT 2 and NIT 4 (#1166 issuecomment-6371408915). Wording, one assert message and one row, in the test file only. No production file changes. **Restacked onto main after #1166 landed** as `105b5da` (2026-11-08T14:49Z). This branch was built on #1166's head `5102b429` (branch `fix/1139-anchored-patterns`), with one commit, `f92338ef6`. The restack merges, never rebases: 1. #1166's final head, `5c8afefd` (its update-branch refresh). 2. Its squash `105b5da` on main, keeping THIS branch's side (`ours`) for `tests/test_value_pattern_line_break.py`. That file is the one add/add conflict, and the squash's side would drop every #1229 edit without a marker. 3. Main, which had not moved past the squash, so this merge was a no-op. - **The head:** `b80214b7f`. Its diff against main is the one file, 56 lines added and 16 removed. - **Checked by lane 5's `restack3.sh`:** its changed lines equal `f92338ef6`'s against that commit's base, and `git merge-tree --write-tree` with main is clean. 1. **A false reason (rc1166b NIT 2).** The module docstring's "LEFT AS THEY ARE" bullet and the `ID_READERS` rows said every argv `--id` of `scaffold-project.py`, `family.py init` and `adopt-project.py` "holds the id to `checked_value` after the pattern". That holds for the first two, and `tests/test_derived_id_refusals.py`'s `TOOLS` pins it for those two only. `adopt-project.py` never calls `checked_value` on `--id`: `_refuse_bad_id` asks `PROJECT_ID_RE.match(project_id)` of the id `plan` takes, passed or derived, and `args.id` reaches no `checked_value` call (`git grep -n 'args\.id\b' -- adopt-project.py`; its calls are for `--project`, the tracking branch, `--pin-owner`, the leg paths and `--org`). A line feed is still refused, by another door, the project's own `validate-manifest.py`. 2. **"`fullmatch` is enough" (rc1166b NIT 1).** Both docstrings said a new reader "asks with `fullmatch`, or it is listed in `ID_READERS`". The guard is `assert id_pattern_calls() == expected`, a dict equality over every (module, method), exact both ways, so a new `fullmatch` reader fails it too, and so does a listed reader taken away. The assert had no message, so the author of a sibling PR saw only a dict diff. 3. **The guard reads spellings (rc1166b NIT 4, optional).** `_ID = PROJECT_ID_RE` then `_ID.match(value)` passes it, and so does `from repo_shape import PROJECT_ID_RE as X`. Nothing in #1166 says that is false, and nothing says it. All in `tests/test_value_pattern_line_break.py`. - **Item 1.** The module docstring's bullet names `scaffold-project.py` and `family.py init` only, with `TOOLS` as the proof, and a new bullet says `adopt-project.py`'s argv `--id` is NOT held to `checked_value`: `plan` asks it with `match` and calls `checked_value` on `--project` and never on the id, so `northwind\n` passes the pattern, and the project's own `validate-manifest.py` refuses it as `adopt-manifest-invalid` naming `--id`, in `manifest-id`'s words, exit 2, no plan written (the row below, "measured for `plan` only"). The `adopt-project.py` row of `ID_READERS` begins with that reason in place of "argv `--id`, as above". The `scripts/family.py` and `scaffold-project.py` rows are as they were. - **Item 1's optional row.** `test_adopt_plan_names_a_passed_id_that_ends_in_a_line_break_as_an_id` runs `adopt-project.py plan --id` with a trailing line feed (conftest's `write_plan` over its `source_repo`, one `plan` run of about a second) and asserts exit 2, the first stderr line `REFUSED adopt-manifest-invalid: --id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-id: id is 'northwind\n'; it must match ^[a-z0-9][a-z0-9-]*$.`, and that no plan file was written. It also imports `write_plan` from `conftest`. `--id` and `--id=...` gave the same lines when measured. - **Item 2.** Both docstrings drop "it asks with `fullmatch`, or": a new reader, `fullmatch` included, is listed in `ID_READERS` with the reason a line feed cannot reach it, or the test fails; the table is exact both ways, so a reader taken away loses its row. The comment above `ID_READERS` says a reader added later needs its row whichever method it calls. - **Item 3.** The assert carries a message: "`ID_READERS` lists every call of a method of `PROJECT_ID_RE` outside `tests/`, `fullmatch` included, by module and method. Add, change or drop the row of the module and method the diff below shows, and give a `match` or `search` reader the reason a line feed cannot reach it." Pytest prints it above the dict diff. - **Item 4 (aliases).** Left as it is, and said: both docstrings say the guard reads spellings (a call whose receiver is the name `PROJECT_ID_RE` or an attribute of that name) and stops there, with the two aliases named as outside it. S1 to S3 below measure exactly that. `git grep -n 'checked_value' -- tests/test_value_pattern_line_break.py` now shows the `checked_value` reason only against `scaffold-project.py` and `scripts/family.py` (lines 43, 171, 173); the module docstring's `adopt-project.py` bullet and the `adopt-project.py` row name `validate-manifest.py` and `adopt-manifest-invalid` for it, and say `adopt-project.py` is not held to `checked_value`. **The new row fails on main (`50869f384`).** The test file is #1166's, so it was copied from this head onto a detached worktree at main, and only the new row was run (git 2.43.0): it fails, because on main the manifest validator's `id` pattern still takes `northwind` and a line feed (`$` matches before the line feed), and the line feed is refused only through the topic: ``` E AssertionError: REFUSED adopt-manifest-invalid: --id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-topic: topic is 'xf-project-northwind', expected 'xf-project-northwind\n' derived from id 'northwind\n'. E - manifest-id: id is 'northwind\n'; it must match ^[a-z0-9][a-z0-9-]*$. E + manifest-topic: topic is 'xf-project-northwind', expected 'xf-project-northwind\n' derived from id 'northwind\n'. ``` So the row holds the claim that `manifest-id` is the refusal, which is #1166's own change to `validate-manifest.py`, and it passes at #1166's head plus this commit (31 passed below). That is why this PR was stacked on #1166 until #1166 landed. Each applied alone to a scratch worktree (detached at `f92338ef6`), run, restored by writing the original bytes back, and the sha256 compared (`restored=True` each time, `git status` clean after each). Production files only; the test file is not touched by a plant. Node ids: the guard is `test_every_reader_of_the_id_pattern_is_in_the_table`, the row is the new one above. Baseline before any plant: both pass (2 passed). | plant | file | what | result | | --- | --- | --- | --- | | G1 | `update-shape.py` | `def _rc_probe(value): return PROJECT_ID_RE.fullmatch(value)` appended | guard fails, its message names `ID_READERS`; `Left contains 1 more item: {('update-shape.py', 'fullmatch'): 1}` | | G2 | `update-shape.py` | the same with `.match` | guard fails, message names `ID_READERS` | | G3 | `update-shape.py` | the same with `.search` | guard fails, message names `ID_READERS` | | G4 | `adopt-project.py` | `if False:` in place of the plan-`id:` test at line 6791 | guard fails, message names `ID_READERS`; `{('adopt-project.py', 'match'): 2} != {('adopt-project.py', 'match'): 3}` | | R1 | `adopt-project.py` | `fullmatch` in place of `match` at line 5140 (`_refuse_bad_id`) | guard fails (names `ID_READERS`) and the row fails: `REFUSED adopt-bad-id: --id 'northwind\n' must match ^[a-z0-9][a-z0-9-]*$` | | R2 | `adopt-project.py` | `checked_value('--id', project_id)` added before line 5140 | row fails: `REFUSED unsafe-value: --id is 'northwind\n', which is not a value this tool will put on a git or gh command line` | | R3 | `adopt-project.py` | `if False:` in place of `if values is not None:` in `_refuse_unholdable_values`, so no manifest check | row fails: the plan is written, exit 0 | | S1 | `update-shape.py` | `_ID = PROJECT_ID_RE` then `_ID.match(value)` | guard PASSES (1 passed): an alias is outside it, as the docstrings now say | | S2 | `update-shape.py` | `from repo_shape import PROJECT_ID_RE as X` then `X.match(value)` | guard PASSES (1 passed) | | S3 | `update-shape.py` | `PROJECT_ID_RE.match(value)` in a comment and in a string | guard PASSES (1 passed): not counted, as intended | No mutant survives: G1 to G4 and R1 to R3 each fail where they should. R2 shows the new row also fails if `adopt-project.py` ever does hold the id to `checked_value`, which makes the module docstring's sentence false, so whoever adds that call is sent to the wording. All through the lane's runner `run9.sh` (stub `gh` first on PATH, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, `EMAIL` exported), on the committed head `f92338ef6` with `dirty=0`, Python 3.12.3, `git --version` printed into each log. - `tests/test_value_pattern_line_break.py`: 31 passed on git 2.43.0 (25.78 s) and 31 passed on git 2.47.1 (31.21 s), the 30 of #1166's head plus the new row. - The file plus the hygiene five (`test_pinned_copy_text.py`, `test_path_guard_hygiene.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`): 337 passed on git 2.43.0 (111.39 s) and 337 passed on git 2.47.1 (264.87 s), that is 31 and the five's 306. - `git diff --check` is clean. The diff touches only the test file, adds no line over 79 columns (the file's own width) and no non-ASCII line, and the file has no `assert` that joins conditions with `and` or `or` (an `ast` scan for a `BoolOp` in any assert: none). **At the restack (2026-11-08):** main is `105b5daf9`, #1166's squash, and the head is `b80214b7f`. `git merge-tree --write-tree origin/main HEAD` is clean, and the diff against main is the test file alone. The rest of this section is from the first push. Main `50869f384` (re-fetched just before the push), this head `f92338ef6`: - `git merge-tree --write-tree origin/main HEAD`: clean (exit 0). - `git merge-tree --write-tree 5102b42 HEAD` (#1166's head): clean (exit 0). - For the stack as a whole, `git merge-tree --write-tree origin/main 5102b42` (#1166 alone with main): clean (exit 0). The merge base of main and #1166's head is `038dafba9`. Of the 51 open PRs (`gh pr list --json number,headRefName,headRefOid,files`), only #1166 touches `tests/test_value_pattern_line_break.py`. Not checked: whether a PR that touches `adopt-project.py` (26 do) adds or removes a call of a method of `PROJECT_ID_RE`; that is the guard's own job once #1166 lands, and #1166's body found none among the open PRs. A PR that makes `plan` hold `--id` to `checked_value` would fail the new row (R2), which is the row doing its work. Review: rv1243, lane 5's independent sonnet reviewer, which wrote none of #1243: READY at `f92338ef`, H0 of this landing before its restack (issuecomment-6385593825), with no MUST; its one SHOULD (the body's restack sentence) is fixed in the body, and its four NITs are optional. The restack's changed lines equal the reviewed ones, and lane 3 placed it on v1.2.1 right after #1131 (#473 issuecomment-6723002632). CI (Brett Heap's ruling 27): H0 f92338e passed tests, tests-git-bash and SonarCloud; the head b80214b merges #1166's final head, then #1166's squash 105b5da (the branch's own side for #1166's files), then main, with tests, tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 372f767 over main 5926ccc: the value_pattern_line_break, repo_hygiene, agents_md_codes, cli_reference sets, 202 passed. 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 #1229 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Goal
The value patterns the issue names refuse a value that ends in a line feed, by name, as #1126 (#1017) makes
COMMIT_RE,SHA256_REandCOMMIT_40do:DATE_REandQUALIFIED_REinvalidate-manifest.pyandvalidate-family.py,MEMBER_REinscripts/family.py,DATE_REinscripts/shape_advisory.pyandREPOSITORY_REinupdate-shape.py;PROJECT_ID_RE, read withfullmatchwhere the two validators andfamily.py addread an id from a file.A shape pin's
source_repository:that is neither a path on this machine nor anowner/reponame is refused in the pin's name, not--upstream's. Values without a line feed keep their outcome, and no refusal code is added or renamed. Two inputs do move from one existing code to another (see "Behaviour changed", below).What this PR does not cover, and the title does not claim:
scripts/repo_shape.pyreads fromcontracts/repository-naming.yaml, whichvalidate-repository-naming.pystill reads with.match;update-upstream-not-a-repo, which still says--upstreamwhen a pin names a folder that exists and is not a repository.Both are follow-ups, with the other findings of the independent review, under "Follow-ups (not in this PR)".
This does not need #1126 and does not touch its files or its test file.
What held on main
Measured on main
038dafba(Python 3.12.3, git 2.43.0) by a probe of each tool, before any change. Each value is written in the file as a YAML double-quoted"...\n", which the reader takes as the value and a line feed.project.yaml(validate-manifest.py)id: "atlas\n"FINDING manifest-topic: topic is 'xf-project-atlas', expected 'xf-project-atlas\n' derived from id 'atlas\n', exit 1project.yamlelected_on: "2026-09-02\n"manifest ok, exit 0project.yamlrepository: "testorg/Atlas-spec\n"manifest ok, exit 0family.yaml(validate-family.py)id: "inkrouter\n"family ok: InkRouter (inkrouterand a line break, exit 0family.yamlcreated_on: "2026-09-04\n"family ok, exit 0family.yamlrepository: "InkRouter/IRRS\n"FINDING member-repository-name: ... whose name is 'IRRS\n', exit 1project.yaml, read byfamily.py addid: "irqs\n"id: "irqs\n"; the holder's gate then saidfamily okcontracts/shape-pin.yaml,update-shape.py checkwith no--upstreamsource_repository: "opensoft/openRepoShape\n"REFUSED unsafe-value: --upstream is 'opensoft/openRepoShape\n', exit 2source_repository: "opensoft/open Repo"REFUSED update-upstream-unresolvable: --upstream 'opensoft/open Repo' is neither ..., exit 2source_repository: "-opensoft/openRepoShape"REFUSED unsafe-value: --upstream is '-opensoft/openRepoShape', exit 2adopt-project.py check)id: "northwind\n"FINDING plan-manifest-invalid: id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-topic: ..., exit 1;executerefused it, exit 2, before any legIn-process on the same main:
repo_shape.PROJECT_ID_RE.match('atlas\n'),shape_advisory.DATE_RE.match('2026-09-02\n')andREPOSITORY_RE.match('a/b\n')each matched, anddate.fromisoformat('2026-09-02\n')raisedValueError.What changed
DATE_REandQUALIFIED_REintemplates/assembly-root/scripts/validate-manifest.pyandtemplates/family-root/scripts/validate-family.pyend in\Z. So doMEMBER_REinscripts/family.py(the third spellingtests/test_family_member_name.pyholds equal to the twoQUALIFIED_RE),DATE_REinscripts/shape_advisory.pyandREPOSITORY_REinupdate-shape.py.PROJECT_ID_REkeeps its$. Every id refusal prints its text (must match ^[a-z0-9][a-z0-9-]*$, pinned in six test files), the validators run against each project's own copy ofrepo_shape.py, and its definition sits inside repo_shape.py and bootstrap.py:COMMIT_RE,SHA256_REandCOMMIT_40end in\Z, not$, so a pin'scommit:ortree_sha256:that ends in a line break is refused as not 40 or 64 hex by every validator andadopt-project.py(#1017) #1126's hunk. The three callers that read an id from a file ask it withfullmatchinstead:validate-manifest.py(manifest-id),validate-family.py(family-id) andfamily.py's reading of a member'sproject.yaml(member-bad-id, whichaddnow says before anything is mounted).update-shape.py.open_upstream(spec, pin_path=None)takes the pin the value was read from, whichpreparepasses when no--upstreamwas given. Thenupdate-upstream-unresolvablesays<root>/contracts/shape-pin.yaml: `source_repository:` is '...', which is neither a path on this machine nor an `owner/repo` name, with a remedy to write the pin's field or pass--upstream, andchecked_valuenames the same field. Code kept:update-upstream-unresolvable, the existing code for a value that is not anowner/reponame. A line feed now reaches it, since the pattern no longer takes one. Before,checked_valuerefused it asunsafe-value.One existing test line:
tests/test_family_init_values.pyfinds validate-family'sDATE_REline by its text, which now ends\Z.One existing stand-in: the
temporariesfixture intests/test_update_shape_unreadable.pystands in foropen_upstreamwithdef opening(spec), which the new two-argument call refused withTypeError: ... takes 1 positional argument but 2 were given. It now takespin_path=Noneand hands it on to the realopen_upstream, sotest_a_read_refused_in_prepare_closes_the_upstream[check|apply]asks what it asked before.The rows are in a new file of their own,
tests/test_value_pattern_line_break.py.A second guard, in a second commit (
5102b4294, a test only, no production file). The first guard holds the four pattern names this PR ends in\Z, and nothing heldPROJECT_ID_RE, which keeps its$.test_every_reader_of_the_id_pattern_is_in_the_tablereads every tracked.pyoutsidetests/withast(it runs none) and holds each call of a method ofPROJECT_ID_RE(PROJECT_ID_RE.x(...)orrepo_shape.PROJECT_ID_RE.x(...)) to the tableID_READERS:fullmatchreaders of an id from a file (validate-manifest.py,validate-family.py,scripts/family.py);matchit, each with its reason: argv--idinscaffold-project.pyandscripts/family.py, wherechecked_valuerefuses a line feed after the pattern; argv--idinadopt-project.py, which the project's ownvalidate-manifest.pyrefuses (adopt-manifest-invalid, below;ID_READERSstill gives it thechecked_valuereason); andadopt-project.py's twoplan-bad-idreaders, of the plan'sid:and of the id derived fromlegs.assembly, which a line feed never passes (below).A new reader of
PROJECT_ID_RE,fullmatchincluded, fails it by file and method until it is listed inID_READERSwith its reason. The table is exact both ways, so a reader taken away fails it too.Left as they are, and why:
PROJECT_ID_RE(--idinscaffold-project.py,family.py initandadopt-project.py). The first two hold the id tochecked_valueafter the pattern, which refuses a line feed asunsafe-valuenaming--id, andtests/test_derived_id_refusals.pypins that answer for them.adopt-project.pynever callschecked_valueon--id:planwith--idset tonorthwindand a line feed exits 2 withadopt-manifest-invalid, in the words of the project's ownvalidate-manifest.py(manifest-id), and writes no plan (lane 5's re-check, issuecomment-6371408915, NIT 2; measured onplanonly);adopt-project.py's reading of a plan'sid::checkandexecutehold it to the project's ownvalidate-manifest.py, which refused it on main (above) and now names it inmanifest-id's words. It is the same value, refused under the same code,plan-manifest-invalid, before any leg exists. The id derived fromlegs.assemblyis read with.matchtoo, and a line feed inlegs.assemblyisunsafe-valuebefore that id is asked (the review measured both, on main and here alike). This keeps the open PRs onadopt-project.py(24 at 12:02Z) clear;PIN_ARG_RE, andNAME_RE/REF_RE/PATH_REinsetup-project.py, as the issue says;update-upstream-not-a-repostill says--upstream. A pin's value reaches it only when it names a path that exists, not through the pattern.Behaviour changed, stated plainly, and what the issue's Verify means by "No refusal code changes": the issue's Verify line reads as no code added or renamed, and none is. Two inputs do move from one existing code to another, exit 2 both before and after:
update-shape.py --upstream "opensoft/openRepoShape<LF>"wasunsafe-valueand is nowupdate-upstream-unresolvable, still naming--upstream;source_repository:that ends in a line feed, with no--upstream, wasunsafe-valuenaming--upstreamand is nowupdate-upstream-unresolvablenaming the pin's field.No test pinned either old answer: every gate below passed.
family.py add --memberis stripped beforeMEMBER_RE, soaddanswers as before.Not changed, seen while measuring: with
--upstreampassed,update-shape.pydoes not read the pin'ssource_repository:throughREPOSITORY_REat all, and it keeps the value as the upstream's repository name.validate-family.pydoes not hold a member row'sid:toPROJECT_ID_RE; it compares the row with the member's manifest. Neither is a$question. Both are under "Follow-ups (not in this PR)".Rows failing on main
All in
tests/test_value_pattern_line_break.py, run against main's production code: the five production files swapped in place fromorigin/main(961640c68), then put back with their sha256 matching (dirty=5in the run's START line is those five files). On the head5102b4294(30 rows) the result is 20 failed, 10 passed in 23.12s, git 2.43.0. The 10 that pass are baselines: the seventest_a_pattern_takes_its_valuerows, the spelling-table guard and the two double-quoted-without-a-line-feed rows.The first 19 are the first commit's rows (19 failed, 10 passed in 22.12s on its 29 rows at
038dafba). The independent review reproduced them from main's bytes of the same five files (19 failed, 10 passed in 26.78s, the same 19 ids). The 20th,test_every_reader_of_the_id_pattern_is_in_the_table, is the second commit's: it fails on main because the three readers of an id from a file are.matchthere, not.fullmatch.test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]test_a_pattern_refuses_its_value_and_a_line_feed[manifest-QUALIFIED_RE]test_a_pattern_refuses_its_value_and_a_line_feed[family-DATE_RE]test_a_pattern_refuses_its_value_and_a_line_feed[family-QUALIFIED_RE]test_a_pattern_refuses_its_value_and_a_line_feed[family.py-MEMBER_RE]test_a_pattern_refuses_its_value_and_a_line_feed[shape_advisory-DATE_RE]test_a_pattern_refuses_its_value_and_a_line_feed[update-shape-REPOSITORY_RE]test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[leg-repository]test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_idtest_a_family_value_that_ends_in_a_line_break_is_a_finding[id]test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]test_a_family_value_that_ends_in_a_line_break_is_a_finding[member-repository]test_add_refuses_a_member_whose_id_ends_in_a_line_breaktest_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[space]test_a_pins_source_checked_value_refuses_is_named_as_the_pinstest_an_upstream_passed_with_a_line_feed_is_named_as_passedtest_every_reader_of_the_id_pattern_is_in_the_tableMutants
13 of 13 killed. Each mutant was applied alone to one file of the branch's production bytes (the same bytes as the first commit and as the head, which adds a test only), the rows were run, and the file was put back with its sha256 checked (
restored=yesfor all 13). Each run istests/test_value_pattern_line_break.pyplustests/test_family_member_name.py::test_every_spelling_of_the_member_name_rule_is_the_same_pattern, git 2.43.0.DATE_REback to$validate-manifest.pytest_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE],test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]QUALIFIED_REback to$validate-manifest.py...[manifest-QUALIFIED_RE],...[leg-repository],test_every_spelling_of_the_member_name_rule_is_the_same_pattern.matchvalidate-manifest.pytest_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id],test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_idDATE_REback to$validate-family.py...[family-DATE_RE],test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]QUALIFIED_REback to$validate-family.py...[family-QUALIFIED_RE],...[member-repository],test_every_spelling_of_the_member_name_rule_is_the_same_pattern.matchvalidate-family.pytest_a_family_value_that_ends_in_a_line_break_is_a_finding[id]MEMBER_REback to$scripts/family.py...[family.py-MEMBER_RE],test_every_spelling_of_the_member_name_rule_is_the_same_pattern.matchscripts/family.pytest_add_refuses_a_member_whose_id_ends_in_a_line_breakDATE_REback to$scripts/shape_advisory.py...[shape_advisory-DATE_RE](its pattern row alone:date.fromisoformatrefuses the line feed after the pattern, so no tool row sees it)REPOSITORY_REback to$update-shape.py...[update-shape-REPOSITORY_RE],test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed],test_an_upstream_passed_with_a_line_feed_is_named_as_passedpreparepasses no pinupdate-shape.pytest_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]and[space],test_a_pins_source_checked_value_refuses_is_named_as_the_pinschecked_valuenames--upstreamagainupdate-shape.pytest_a_pins_source_checked_value_refuses_is_named_as_the_pinsupdate-shape.pytest_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]and[space]No mutant is added for the stand-in in
tests/test_update_shape_unreadable.py: it is a test change.The independent review re-ran M3, M8, M11 and M12, and one mutant of its own (X1:
preparenames the pin even when--upstreamis passed). All five were killed, and each file was put back with its sha256 checked. The other nine of the 13 are this writer's runs alone.The second commit's guard: 6 of 6 probes caught. Each was applied alone to one tracked file at
5102b4294,test_every_reader_of_the_id_pattern_is_in_the_tablewas run, and the file was put back with its sha256 checked (restored=yesfor all six). The first commit's guard,test_every_spelling_of_a_value_pattern_is_in_the_table, was run with each plant too: it passes every one of them, which is the gap the second guard closes.PROJECT_ID_RE.match(value)reader appendedupdate-shape.py('update-shape.py', 'match'): 1is more than the table holdsrepo_shape.PROJECT_ID_RE.search(value)reader appendedscripts/shape_advisory.pyfullmatchback tomatchtemplates/assembly-root/scripts/validate-manifest.pyfullmatchback tomatchtemplates/family-root/scripts/validate-family.pyfullmatchback tomatchscripts/family.pyRuns
The second commit
5102b4294(dirty=0), Python 3.12.3, with the same stubgh, bogus token and no global git config:tests/test_value_pattern_line_break.pytests/test_value_pattern_line_break.pytests/test_pinned_copy_text.py,tests/test_path_guard_hygiene.py,tests/test_repo_hygiene.py,tests/test_agents_md_codes.py,tests/test_cli_reference.pyThat commit changes only the test file (62 lines added, no assert that joins conditions with
andoror, ASCII), so the first commit's runs below are not repeated: they hold the production files and every other test file, and those are unchanged. CI on5102b4294, and an independent look at the second commit, were not done when this was written.The independent review of the first commit (rv1166c, READY, read-only) reports these gates on
2593952c9, all passing: the stand-in gate's 6 files (298 passed), the new file on 2.47.1 (29 passed), the five hygiene files (306 passed), 15 more test files that name a changed function, pattern or code (1611 passed, 19 skipped) andtests/test_shape_doctor.py(445 passed, 3 skipped). It also read CI on that head at about 20:04Z:tests(the full suite on 3.14),tests-git-bash,packageand SonarCloud all passed.The first commit's gates ran on
2593952c(dirty=0), Python 3.12.3, with the realghunreachable (a stub first onPATH, a bogus token) and no global git config:tests/test_family.py,tests/test_update_shape_pr_base.py,tests/test_update_shape_unreadable.py,tests/test_update_shape_header_comments.py,tests/test_update_shape_messages.pyand the newtests/test_value_pattern_line_break.pytests/test_value_pattern_line_break.pytests/test_pinned_copy_text.py,tests/test_path_guard_hygiene.py,tests/test_repo_hygiene.py,tests/test_agents_md_codes.py,tests/test_cli_reference.pyThe two shards, on the uncommitted tree holding the same production bytes as the first commit (the 7 files' sha256 checked before and after), git 2.43.0: the validators' test files, the
REPOSITORY_RE/source_repositoryfiles and theupdate-shape.py,shape_advisory.pyandfamily.pyfiles, 125 in all.Every failure and setup error is in the five existing files of the first row of the table above, which all pass on the first commit:
test_a_read_refused_in_prepare_closes_the_upstream[check|apply], theopening(spec)stand-in under the two-argument call. The stand-in is fixed in the first commit.update-conflictonscripts/validate-family.py("not a verbatim copy ... at the pinned commit"). This is an artifact of an uncommitted tree:tests/test_family.py'supstream_clonecommits the working tree into its clone, whilefamily.py initpins the holder at the realHEAD, so an uncommittedtemplates/family-root/file is never verbatim at its pin. On the first commit they pass.Every other file of the two shards passed on these production bytes, so the shards were not re-run.
Merge-trees
git merge-tree --write-tree <other> 5102b4294(git 2.43.0) at 2026-10-23T12:08Z, after re-fetching main (still961640c6; read again at 12:10Z, just before the push) and re-listing the open PRs at 12:02Z (55 open). Main has moved from the038dafbathat this body measured to961640c6by one file,docs/release-notes/v1.2.0.md. Each open PR that touches one of this PR's 8 paths, at its current head, plus #1126:961640c64613729516ffdad7961ascripts/family.py4d1ff951758810dc011cscripts/family.pyscripts/family.pymerges clean; conflict indocs/handbook.htmlonly, which this PR does not touch (see below)8dfa32bdvalidate-family.pycb2ec10736464c1f80cfscripts/repo_shape.pyand tests)e82c8945ac37a7bfee1bupdate-shape.pya710870d1ffc89486f69scripts/shape_advisory.py2d3e3e1ef43db75babeevalidate-manifest.pya82ac3b47a3f66e68f94scripts/shape_advisory.py4abeb7b3866574096eb4scripts/family.pyee285e1cb8cf5aa0f116validate-family.pyaa32d41e6e2ccc8a87eeupdate-shape.py,tests/test_update_shape_unreadable.py25711eb1cac4eac177afscripts/family.pyscripts/family.pymerges clean; conflict indocs/handbook.htmlonly (see below)removethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117 and family.py: theadd,bumpandremovelines its remedies name begin with the interpreter and the script's path, quote the row's name, fill in the family's org, say the branch they run on, andmember-already-presentprints itsremove(#1127) #1201 conflict indocs/handbook.htmlalone. family.py: theadd,bumpandremovelines its remedies name begin with the interpreter and the script's path, quote the row's name, fill in the family's org, say the branch they run on, andmember-already-presentprints itsremove(#1127) #1201 is stacked on family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117 (their merge-base is family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's head10dc011c). Each of them against origin/main alone gives the same conflict in the same file, so it is theirs: family.py: theremovethat bothmember-duplicate-idremedies name and theaddthatbump'smember-unknownnames carry--family-root <holder>, quoted for the host, so each runs as printed (#1005) #1117's, from its older base6da757cb, which family.py: theadd,bumpandremovelines its remedies name begin with the interpreter and the script's path, quote the row's name, fill in the family's org, say the branch they run on, andmember-already-presentprints itsremove(#1127) #1201 inherits.applyputs every byte back under anyOSErrorafter its first write, says what it put back on stderr, and raises the error as it was, so a file gone between itsstatand its read, a broken pipe or anEIOno longer leaves the tree half-written behind a traceback (#1173) #1187 and family.py: theadd,bumpandremovelines its remedies name begin with the interpreter and the script's path, quote the row's name, fill in the family's org, say the branch they run on, andmember-already-presentprints itsremove(#1127) #1201 are new since the first merge-trees. update-shape.py:applyputs every byte back under anyOSErrorafter its first write, says what it put back on stderr, and raises the error as it was, so a file gone between itsstatand its read, a broken pipe or anEIOno longer leaves the tree half-written behind a traceback (#1173) #1187 sharestests/test_update_shape_unreadable.pywith this PR; its diff adds or removes no line that namesopen_upstream,def openingorREPOSITORY_RE, so it brings no one-argument stand-in for the two-argument call. That was read for all 55 open heads at 12:12Z; only this PR's own lines name them.PROJECT_ID_RE, which the second guard counts: the lines each of the 55 open heads adds or removes outsidetests/,docs/and*.mdwere read at 12:04Z. Only this PR's own name it, and family.py: theadd,bumpandremovelines its remedies name begin with the interpreter and the script's path, quote the row's name, fill in the family's org, say the branch they run on, andmember-already-presentprints itsremove(#1127) #1201's one newMEMBER_RE.fullmatchis another pattern. So a sibling landing first does not move the guard's table.Follow-ups (not in this PR)
Each is a candidate for its own issue. They come from the independent review (rv1166c, READY at
2593952c9, issuecomment-6352173084), which measured each of them on main and at the head unless the item says it did not. The review's finding numbers are in brackets.The naming-policy patterns [1].
scripts/repo_shape.pyreads the families, the roles and the topic pattern ofcontracts/repository-naming.yamlwith.match(family["_re"].match(name),family["_split_re"].match(name),role["_re"].match(name), andtopic_pattern, which defaults to^xf-project-[a-z0-9-]+$).validate-repository-naming.py --project project.yamlruns in the root template'sMakefileandvalidate.yml. At the head, and on main, it exits 0 on a legrepository: "testorg/Atlas\n"and prints the line feed inside its table;-specand-codethe same. The project's gate as a whole still fails, becausevalidate-manifest.pynow refuses that manifest.update-upstream-not-a-repowith a pin [3]. With no--upstream, a pin whosesource_repository:names a folder that exists and is no repository (the review measuredcontractsandscripts/repo_shape.py) isREFUSED update-upstream-not-a-repo: --upstream contracts exists but is not a git repository, exit 2, on main and here.pin_pathis in scope atupdate-shape.py:686-687, so that refusal can name the pin asupdate-upstream-unresolvablenow does.A pin's line feed when
--upstreamis passed [9].update-shape.py check --upstream <clone>over a pin whosesource_repository:ends in a line feed exits 0,nothing to do. The value is kept asupstream.repository, which reaches the README shape line's check,apply's commit subject and the pull request words (update-shape.py:2525,3017and5121). The review measuredcheck, notapply. It belongs with the previous item.A member row's
id:[4]. On main,addof a member whoseproject.yamlreadsid: "irqs\n"commits the rowid: "irqs\n", and the holder'svalidate-family.pythen saysfamily ok. Under mutant M8 (this branch with main's.matchinadd) the same row is written, and the gate still saysfamily ok. By code reading, not measured end to end:validate-family.pycompares a row's id with the member manifest's bystrequality (member-id-mismatch,validate-family.py:1608), and_check_member_ids_uniquedoes the same, so neither asksPROJECT_ID_REof a row. This PR makesaddrefuse such a member, so no new holder gets such a row.Values that no pattern reads [8]. The review's sweep found these
manifest okorfamily ok, exit 0, with a trailing line feed, at the head and on main alike:project.yaml:name, a spec or code leg'spath,shape.repositoryandshape.digests.digest_algorithm;family.yaml:org, the family's ownrepository,tracking_branch, and the shape's and each member pin'sdigest_algorithm.No
^...$pattern reads them, so they are not this PR's. A leg'spathis checked only by_leg_path_findings, which asks no pattern, and the family'stracking_branchis refused inproject.yaml(manifest-tracking-branch, U+000A) but not infamily.yaml.shape.commitandtree_sha256with a line feed are alsomanifest ok; that is repo_shape.py and bootstrap.py:COMMIT_RE,SHA256_REandCOMMIT_40end in\Z, not$, so a pin'scommit:ortree_sha256:that ends in a line break is refused as not 40 or 64 hex by every validator andadopt-project.py(#1017) #1126's.The first commit's message, and what the review measured
The first commit's message says "the three readers of an id from a file ask it with
fullmatch(manifest-id,family-id,member-bad-id)". That is true of those three, but it reads as if there were no others [2]. What the review measured instead:adopt-project.pystill reads the id derived fromlegs.assembly(:6780) and the plan'sid:(:6791), bothplan-bad-id, with.match;id: "northwind\n"isplan-manifest-invalid, now inmanifest-id's words. A plan with noid:andlegs.assembly: "NorthWind\n"isREFUSED unsafe-value: legs.assembly is 'NorthWind\n', exit 2, before the derived id is asked, on main and here alike.So it is the wording alone that overstates. The second commit's
ID_READERSlists both readers with that reason.The title's pin clause is the narrower one (the pin's name only where the value is no path and no
owner/reponame), and two inputs change fromunsafe-valuetoupdate-upstream-unresolvable(see "Behaviour changed", above).Closes #1139
Lane: openreposhape-5 (openRepoShape-5)
🤖 Generated with Claude Code
Summary by Sourcery
Reject trailing line feeds in validated identifiers and repository-related values, and attribute invalid shape-pin sources to the pin that supplied them.
Bug Fixes:
source_repository:values against the pin field rather than the absent--upstreamoption.Enhancements:
Tests: