Repository navigation
repo_shape.py and bootstrap.py: COMMIT_RE, SHA256_RE and COMMIT_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
Conversation
…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) `$` also matches just before a trailing line feed, and every caller reads these patterns with `.match`, so a `commit:` or `tree_sha256:` read as the hex and a line feed (the double-quoted escape for a line feed, or a block scalar) passed as well formed. On main, validate-manifest.py and validate-family.py passed one (`manifest ok`, `family ok`), validate-pins.py refused one for another reason (`pin-commit-unresolvable`, `pin-digest-mismatch`), and the assembly root's bootstrap.py, which holds a copy of the commit pattern as `COMMIT_40`, printed its upstream notice over one. `\Z` matches only at the very end of the value, so each caller's `.match` refuses a trailing line feed as it already refused a carriage return and line feed, and a space. Nothing else about the patterns changes: 40 and 64 digits, either case. Rows: tests/test_pin_pattern_line_break.py (the three patterns, the holder's gate, the upstream notice, the doctor's `pin_commit`), and a row each in test_pin_validator.py, test_manifest_validator.py, test_adopt_pins.py and test_shape_doctor.py. Each fails on main and passes with the change. Closes #1017 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR fixes an anchoring defect where Python’s Flow diagram for strict pin validationflowchart LR
Input["commit or tree_sha256 value"] --> Pattern{Strict \Z pattern}
Pattern -->|"exact hex length"| Valid["Accepted"]
Pattern -->|"trailing line feed or other suffix"| Invalid["Rejected as malformed"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Independent review of #1126 (#1017) at The fix is right. Nothing that worked before is refused now, and I found no other copy that needs the change. The only blocker is two composite asserts in the new test file, which SonarCloud has flagged on the PR. Splitting them is a two-line fix round. FindingsMUST: two composite asserts (fix round).
NIT: the body misstates what main does with
NIT: the merge-tree section is stale. Body lines 80-82 name
NIT: two callers have no row of their own, so a caller-side mutant survives (details under Rows and mutants). These callers are protected only through the shared pattern; that is what the PR changes, so they work. The gap is coverage, not a bug:
Checked and true1. The defect on main (
Per-tool outcomes on main, through the PR's own rows run against main's production files (17 failed, 319 passed in 330.72s):
A direct probe of 2. No caller relied on
No site where valid input is now refused. The only values that change are YAML values that end in a line feed. No tool writes one; it takes a double-quoted escape, or a block scalar written by hand. 3. Copies.
4. Rows and mutants. Run in a worktree at the predicted merge, with the new file (and target rows) only:
A caller's 5. Runs (all through
6. Title and body.
Sonar-style checks:
Could not verify
Follow-up assessment (item 7; input for the lane's issue, not findings against this PR)
Scratch left
Lane: openreposhape-5 (openRepoShape-5) |
…n's commit or digest, or a record's `source_commit:`, has a row, so `.rstrip` before the match in validate-pins.py or validate-manifest.py fails; each read asserts one value (#1017) Fix round 1 for #1126, from its independent review. `member_pin` and `holder_shape` ended in `assert commit and digest, text` (SonarCloud S9073): when one of the two reads found nothing, the message could not say which. Each value now has its own assert and a message that names it. Two callers of the patterns had no row of their own, so a `.rstrip("\n")` before the match survived every row: the neutral product's pin in validate-pins.py (`commit:` is refused as `neutral-pin-tag-only`, `tree_sha256:` is `neutral-pin-digest-malformed`) and the `source_commit:` of a `submodules:` or `answered_paths:` row in validate-manifest.py (`manifest-submodule-commit`, `manifest-answered-commit`). On main the first was the product's checkout refusing the name (`neutral-pin-source-unreadable`), the second a digest recompute (`neutral-pin-digest-mismatch`), the third `manifest ok`. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Re-check of #1126 (#1017) fix round 1 at All five checks hold. There is no MUST or SHOULD, and two NITs, both about prose. The fix round does what rv1126 asked, and the SonarCloud S9073 issues are now closed. FindingsNIT 1: the test file's module docstring still says what the body no longer says.
NIT 2: the body's "Mutants" table is measured on the 42-row file. It reads 7/35, 4/38, 3/39 and 2/40, which sum to 42. The file now has 46 rows. I re-measured on the head:
Every pattern is still killed by a row of its own. M1 is the union of M2 and M3, so I did not run it. Nothing is wrong, the table is just stale. Main moved to Checked and true1. The MUST is fixed.
2. The four new rows.
No mutant survived. O5a and O5b show that both 3. Runs through
4. Merges.
5. The body's NITs.
Could not verify
Scratch I leave
Lane: openreposhape-5 (openRepoShape-5) |
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…date-family.py` passed only the digest of `family.yaml`'s `shape:` on main, and gave `shape-pin-manifest-disagree` over the commit, not that it passed both (#1017) NIT 1 of rc1126's re-check of #1126 (issuecomment-6340973564). The docstring said validate-family.py passed `shape.commit` and `shape.digests.tree_sha256` in family.yaml on main. Only the digest passed (`family ok`, exit 0). The commit passed `COMMIT_RE` too, so `family-shape-commit` was not raised, but `contracts/shape-pin.yaml` holds the commit without the line feed, and the gate gave `shape-pin-manifest-disagree` with the same hex printed on both sides. The clause now says that. A docstring only: no code and no row changes. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
|
Re-check of #1126 (#1017) for lane openRepoShape-5: READY at I checked only the delta from Checked and true1. The merge is clean and adds nothing of its own.
2. The NIT commit
3. The new docstring is true of main.
3b. The mutants. NIT 2 of the same comment was the stale table. I applied each mutant alone to
4. Runs at
5. Merge-trees.
Body note (not a finding)The body's merge-tree bullet for fix round 2 is out of date:
Could not verify
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 4 hours and 46 minutes by commenting |
…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) The value patterns the issue names refuse a value that ends in a line feed, by name, as #1126 (#1017) makes `COMMIT_RE`, `SHA256_RE` and `COMMIT_40` do: - `DATE_RE` and `QUALIFIED_RE` in `validate-manifest.py` and `validate-family.py`, `MEMBER_RE` in `scripts/family.py`, `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`; - `PROJECT_ID_RE`, read with `fullmatch` where the two validators and `family.py add` read an id from a file. A shape pin's `source_repository:` that is neither a path on this machine nor an `owner/repo` name 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:** - the naming-policy patterns that `scripts/repo_shape.py` reads from `contracts/repository-naming.yaml`, which `validate-repository-naming.py` still reads with `.match`; - `update-upstream-not-a-repo`, which still says `--upstream` when 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. 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. | Read from | Value | On main | |---|---|---| | `project.yaml` (`validate-manifest.py`) | `id: "atlas\n"` | only `FINDING manifest-topic: topic is 'xf-project-atlas', expected 'xf-project-atlas\n' derived from id 'atlas\n'`, exit 1 | | `project.yaml` | `elected_on: "2026-09-02\n"` | `manifest ok`, exit 0 | | `project.yaml` | a leg's `repository: "testorg/Atlas-spec\n"` | `manifest ok`, exit 0 | | `family.yaml` (`validate-family.py`) | `id: "inkrouter\n"` | `family ok: InkRouter (inkrouter` and a line break, exit 0 | | `family.yaml` | `created_on: "2026-09-04\n"` | `family ok`, exit 0 | | `family.yaml` | a member's `repository: "InkRouter/IRRS\n"` | `FINDING member-repository-name: ... whose name is 'IRRS\n'`, exit 1 | | a member's `project.yaml`, read by `family.py add` | `id: "irqs\n"` | mounted and committed, exit 0, with the row `id: "irqs\n"`; the holder's gate then said `family ok` | | `contracts/shape-pin.yaml`, `update-shape.py check` with no `--upstream` | `source_repository: "opensoft/openRepoShape\n"` | `REFUSED unsafe-value: --upstream is 'opensoft/openRepoShape\n'`, exit 2 | | the same | `source_repository: "opensoft/open Repo"` | `REFUSED update-upstream-unresolvable: --upstream 'opensoft/open Repo' is neither ...`, exit 2 | | the same | `source_repository: "-opensoft/openRepoShape"` | `REFUSED unsafe-value: --upstream is '-opensoft/openRepoShape'`, exit 2 | | an adoption plan (`adopt-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; `execute` refused it, exit 2, before any leg | In-process on the same main: `repo_shape.PROJECT_ID_RE.match('atlas\n')`, `shape_advisory.DATE_RE.match('2026-09-02\n')` and `REPOSITORY_RE.match('a/b\n')` each matched, and `date.fromisoformat('2026-09-02\n')` raised `ValueError`. - **`DATE_RE` and `QUALIFIED_RE`** in `templates/assembly-root/scripts/validate-manifest.py` and `templates/family-root/scripts/validate-family.py` end in `\Z`. So do `MEMBER_RE` in `scripts/family.py` (the third spelling `tests/test_family_member_name.py` holds equal to the two `QUALIFIED_RE`), `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`. - **`PROJECT_ID_RE` keeps 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 of `repo_shape.py`, and its definition sits inside #1126's hunk. The three callers that read an id from a file ask it with `fullmatch` instead: `validate-manifest.py` (`manifest-id`), `validate-family.py` (`family-id`) and `family.py`'s reading of a member's `project.yaml` (`member-bad-id`, which `add` now says before anything is mounted). - **`update-shape.py`.** `open_upstream(spec, pin_path=None)` takes the pin the value was read from, which `prepare` passes when no `--upstream` was given. Then `update-upstream-unresolvable` says ``<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`, and `checked_value` names the same field. **Code kept:** `update-upstream-unresolvable`, the existing code for a value that is not an `owner/repo` name. A line feed now reaches it, since the pattern no longer takes one. Before, `checked_value` refused it as `unsafe-value`. - **One existing test line:** `tests/test_family_init_values.py` finds validate-family's `DATE_RE` line by its text, which now ends `\Z`. - **One existing stand-in:** the `temporaries` fixture in `tests/test_update_shape_unreadable.py` stands in for `open_upstream` with `def opening(spec)`, which the new two-argument call refused with `TypeError: ... takes 1 positional argument but 2 were given`. It now takes `pin_path=None` and hands it on to the real `open_upstream`, so `test_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 held `PROJECT_ID_RE`, which keeps its `$`. `test_every_reader_of_the_id_pattern_is_in_the_table` reads every tracked `.py` outside `tests/` with `ast` (it runs none) and holds each call of a method of `PROJECT_ID_RE` (`PROJECT_ID_RE.x(...)` or `repo_shape.PROJECT_ID_RE.x(...)`) to the table `ID_READERS`: - the three `fullmatch` readers of an id from a file (`validate-manifest.py`, `validate-family.py`, `scripts/family.py`); - the modules that still `match` it, each with its reason: argv `--id` in `scaffold-project.py` and `scripts/family.py`, where `checked_value` refuses a line feed after the pattern; argv `--id` in `adopt-project.py`, which the project's own `validate-manifest.py` refuses (`adopt-manifest-invalid`, below; `ID_READERS` still gives it the `checked_value` reason); and `adopt-project.py`'s two `plan-bad-id` readers, of the plan's `id:` and of the id derived from `legs.assembly`, which a line feed never passes (below). A new reader of `PROJECT_ID_RE`, `fullmatch` included, fails it by file and method until it is listed in `ID_READERS` with its reason. The table is exact both ways, so a reader taken away fails it too. **Left as they are, and why:** - the argv readers of `PROJECT_ID_RE` (`--id` in `scaffold-project.py`, `family.py init` and `adopt-project.py`). The first two hold the id to `checked_value` after the pattern, which refuses a line feed as `unsafe-value` naming `--id`, and `tests/test_derived_id_refusals.py` pins that answer for them. `adopt-project.py` never calls `checked_value` on `--id`: `plan` with `--id` set to `northwind` and a line feed exits 2 with `adopt-manifest-invalid`, in the words of the project's own `validate-manifest.py` (`manifest-id`), and writes no plan (lane 5's re-check, issuecomment-6371408915, NIT 2; measured on `plan` only); - `adopt-project.py`'s reading of a plan's `id:`: `check` and `execute` hold it to the project's own `validate-manifest.py`, which refused it on main (above) and now names it in `manifest-id`'s words. It is the same value, refused under the same code, `plan-manifest-invalid`, before any leg exists. The id derived from `legs.assembly` is read with `.match` too, and a line feed in `legs.assembly` is `unsafe-value` before that id is asked (the review measured both, on main and here alike). This keeps the open PRs on `adopt-project.py` (24 at 12:02Z) clear; - `PIN_ARG_RE`, and `NAME_RE`/`REF_RE`/`PATH_RE` in `setup-project.py`, as the issue says; - `update-upstream-not-a-repo` still 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>"` was `unsafe-value` and is now `update-upstream-unresolvable`, still naming `--upstream`; - a pin's `source_repository:` that ends in a line feed, with no `--upstream`, was `unsafe-value` naming `--upstream` and is now `update-upstream-unresolvable` naming the pin's field. No test pinned either old answer: every gate below passed. `family.py add --member` is stripped before `MEMBER_RE`, so `add` answers as before. **Not changed, seen while measuring:** with `--upstream` passed, `update-shape.py` does not read the pin's `source_repository:` through `REPOSITORY_RE` at all, and it keeps the value as the upstream's repository name. `validate-family.py` does not hold a member row's `id:` to `PROJECT_ID_RE`; it compares the row with the member's manifest. Neither is a `$` question. Both are under "Follow-ups (not in this PR)". All in `tests/test_value_pattern_line_break.py`, run against main's production code: the five production files swapped in place from `origin/main` (`961640c68`), then put back with their sha256 matching (`dirty=5` in the run's START line is those five files). On the head `5102b4294` (30 rows) the result is **20 failed, 10 passed in 23.12s, git 2.43.0**. The 10 that pass are baselines: the seven `test_a_pattern_takes_its_value` rows, 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 `.match` there, 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_id` - `test_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_break` - `test_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_pins` - `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` - `test_every_reader_of_the_id_pattern_is_in_the_table` **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=yes` for all 13). Each run is `tests/test_value_pattern_line_break.py` plus `tests/test_family_member_name.py::test_every_spelling_of_the_member_name_rule_is_the_same_pattern`, git 2.43.0. | Mutant | File | Killed by | |---|---|---| | M1 `DATE_RE` back to `$` | `validate-manifest.py` | 2: `test_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]` | | M2 `QUALIFIED_RE` back to `$` | `validate-manifest.py` | 3: `...[manifest-QUALIFIED_RE]`, `...[leg-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M3 the id read with `.match` | `validate-manifest.py` | 2: `test_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_id` | | M4 `DATE_RE` back to `$` | `validate-family.py` | 2: `...[family-DATE_RE]`, `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` | | M5 `QUALIFIED_RE` back to `$` | `validate-family.py` | 3: `...[family-QUALIFIED_RE]`, `...[member-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M6 the id read with `.match` | `validate-family.py` | 1: `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` | | M7 `MEMBER_RE` back to `$` | `scripts/family.py` | 2: `...[family.py-MEMBER_RE]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M8 a member's id read with `.match` | `scripts/family.py` | 1: `test_add_refuses_a_member_whose_id_ends_in_a_line_break` | | M9 `DATE_RE` back to `$` | `scripts/shape_advisory.py` | 1: `...[shape_advisory-DATE_RE]` (its pattern row alone: `date.fromisoformat` refuses the line feed after the pattern, so no tool row sees it) | | M10 `REPOSITORY_RE` back to `$` | `update-shape.py` | 3: `...[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_passed` | | M11 `prepare` passes no pin | `update-shape.py` | 3: `test_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_pins` | | M12 `checked_value` names `--upstream` again | `update-shape.py` | 1: `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` | | M13 the pin's refusal sentence removed | `update-shape.py` | 2: `test_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: `prepare` names the pin even when `--upstream` is 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_table` was run, and the file was put back with its sha256 checked (`restored=yes` for 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. | Probe | File | The new guard | The first guard | |---|---|---|---| | P1 a new `PROJECT_ID_RE.match(value)` reader appended | `update-shape.py` | failed: `('update-shape.py', 'match'): 1` is more than the table holds | passed | | P2 a new `repo_shape.PROJECT_ID_RE.search(value)` reader appended | `scripts/shape_advisory.py` | failed | passed | | P3 `fullmatch` back to `match` | `templates/assembly-root/scripts/validate-manifest.py` | failed | passed | | P4 `fullmatch` back to `match` | `templates/family-root/scripts/validate-family.py` | failed | passed | | P5 `fullmatch` back to `match` | `scripts/family.py` | failed | passed | | P6 main's bytes of those three files together (the 20th row above) | the three | failed | not run | **The second commit `5102b4294` (dirty=0)**, Python 3.12.3, with the same stub `gh`, bogus token and no global git config: | Run | Git | Result | |---|---|---| | `tests/test_value_pattern_line_break.py` | 2.43.0 | **30 passed** in 23.00s | | `tests/test_value_pattern_line_break.py` | 2.47.1 | **30 passed** in 23.30s | | `tests/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.py` | 2.43.0 | **306 passed** in 61.54s | That commit changes only the test file (62 lines added, no assert that joins conditions with `and` or `or`, 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 on `5102b4294`, 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) and `tests/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`, `package` and SonarCloud all passed. **The first commit's gates ran on `2593952c` (dirty=0)**, Python 3.12.3, with the real `gh` unreachable (a stub first on `PATH`, a bogus token) and no global git config: | Run | Git | Result | |---|---|---| | the files that held every failure and setup error of the shards below: `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.py` and the new `tests/test_value_pattern_line_break.py` | 2.43.0 | **298 passed** in 343.60s | | `tests/test_value_pattern_line_break.py` | 2.47.1 | **29 passed** in 21.31s | | `tests/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.py` | 2.43.0 | **306 passed** in 69.12s | **The 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_repository` files and the `update-shape.py`, `shape_advisory.py` and `family.py` files, 125 in all. - shard 1 (62 files): 4 failed, 5290 passed, 13 skipped in 56 min; - shard 2 (63 files): 3 failed, 4 setup errors, 3761 passed, 29 skipped in 50 min. 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: - 2 were real: `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]`, the `opening(spec)` stand-in under the two-argument call. The stand-in is fixed in the first commit. - The other 5 failures and the 4 setup errors were `update-conflict` on `scripts/validate-family.py` ("not a verbatim copy ... at the pinned commit"). This is an artifact of an uncommitted tree: `tests/test_family.py`'s `upstream_clone` commits the working tree into its clone, while `family.py init` pins the holder at the real `HEAD`, so an uncommitted `templates/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. `git merge-tree --write-tree <other> 5102b42` (git 2.43.0) at 2026-10-23T12:08Z, after re-fetching main (still `961640c6`; read again at 12:10Z, just before the push) and re-listing the open PRs at 12:02Z (55 open). Main has moved from the `038dafba` that this body measured to `961640c6` by 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: | Against | Head | Shares | Result | |---|---|---|---| | origin/main | `961640c6` | | clean, tree `4613729516ff` | | #1098 | `dad7961a` | `scripts/family.py` | clean, tree `4d1ff9517588` | | #1117 | `10dc011c` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only, which this PR does not touch (see below) | | #1124 | `8dfa32bd` | `validate-family.py` | clean, tree `cb2ec1073646` | | #1126 | `4c1f80cf` | none (its `scripts/repo_shape.py` and tests) | clean, tree `e82c8945ac37` | | #1133 | `a7bfee1b` | `update-shape.py` | clean, tree `a710870d1ffc` | | #1144 | `89486f69` | `scripts/shape_advisory.py` | clean, tree `2d3e3e1ef43d` | | #1147 | `b75babee` | `validate-manifest.py` | clean, tree `a82ac3b47a3f` | | #1159 | `66e68f94` | `scripts/shape_advisory.py` | clean, tree `4abeb7b38665` | | #1164 | `74096eb4` | `scripts/family.py` | clean, tree `ee285e1cb8cf` | | #1172 | `5aa0f116` | `validate-family.py` | clean, tree `aa32d41e6e2c` | | #1187 | `cc8a87ee` | `update-shape.py`, `tests/test_update_shape_unreadable.py` | clean, tree `25711eb1cac4` | | #1201 | `eac177af` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only (see below) | - #1117 and #1201 conflict in `docs/handbook.html` alone. #1201 is stacked on #1117 (their merge-base is #1117's head `10dc011c`). Each of them against origin/main alone gives the same conflict in the same file, so it is theirs: #1117's, from its older base `6da757cb`, which #1201 inherits. - #1187 and #1201 are new since the first merge-trees. #1187 shares `tests/test_update_shape_unreadable.py` with this PR; its diff adds or removes no line that names `open_upstream`, `def opening` or `REPOSITORY_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. - No other open PR adds or removes a call of a method of `PROJECT_ID_RE`, which the second guard counts: the lines each of the 55 open heads adds or removes outside `tests/`, `docs/` and `*.md` were read at 12:04Z. Only this PR's own name it, and #1201's one new `MEMBER_RE.fullmatch` is another pattern. So a sibling landing first does not move the guard's table. 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.py` reads the families, the roles and the topic pattern of `contracts/repository-naming.yaml` with `.match` (`family["_re"].match(name)`, `family["_split_re"].match(name)`, `role["_re"].match(name)`, and `topic_pattern`, which defaults to `^xf-project-[a-z0-9-]+$`). `validate-repository-naming.py --project project.yaml` runs in the root template's `Makefile` and `validate.yml`. At the head, and on main, it exits 0 on a leg `repository: "testorg/Atlas\n"` and prints the line feed inside its table; `-spec` and `-code` the same. The project's gate as a whole still fails, because `validate-manifest.py` now refuses that manifest. - **`update-upstream-not-a-repo` with a pin [3].** With no `--upstream`, a pin whose `source_repository:` names a folder that exists and is no repository (the review measured `contracts` and `scripts/repo_shape.py`) is `REFUSED update-upstream-not-a-repo: --upstream contracts exists but is not a git repository`, exit 2, on main and here. `pin_path` is in scope at `update-shape.py:686-687`, so that refusal can name the pin as `update-upstream-unresolvable` now does. - **A pin's line feed when `--upstream` is passed [9].** `update-shape.py check --upstream <clone>` over a pin whose `source_repository:` ends in a line feed exits 0, `nothing to do`. The value is kept as `upstream.repository`, which reaches the README shape line's check, `apply`'s commit subject and the pull request words (`update-shape.py:2525`, `3017` and `5121`). The review measured `check`, not `apply`. It belongs with the previous item. - **A member row's `id:` [4].** On main, `add` of a member whose `project.yaml` reads `id: "irqs\n"` commits the row `id: "irqs\n"`, and the holder's `validate-family.py` then says `family ok`. Under mutant M8 (this branch with main's `.match` in `add`) the same row is written, and the gate still says `family ok`. By code reading, not measured end to end: `validate-family.py` compares a row's id with the member manifest's by `str` equality (`member-id-mismatch`, `validate-family.py:1608`), and `_check_member_ids_unique` does the same, so neither asks `PROJECT_ID_RE` of a row. This PR makes `add` refuse such a member, so no new holder gets such a row. - **Values that no pattern reads [8].** The review's sweep found these `manifest ok` or `family ok`, exit 0, with a trailing line feed, at the head and on main alike: - in `project.yaml`: `name`, a spec or code leg's `path`, `shape.repository` and `shape.digests.digest_algorithm`; - in `family.yaml`: `org`, the family's own `repository`, `tracking_branch`, and the shape's and each member pin's `digest_algorithm`. No `^...$` pattern reads them, so they are not this PR's. A leg's `path` is checked only by `_leg_path_findings`, which asks no pattern, and the family's `tracking_branch` is refused in `project.yaml` (`manifest-tracking-branch`, U+000A) but not in `family.yaml`. `shape.commit` and `tree_sha256` with a line feed are also `manifest ok`; that is #1126's. 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.py` still reads the id derived from `legs.assembly` (`:6780`) and the plan's `id:` (`:6791`), both `plan-bad-id`, with `.match`; - neither lets a line feed through. A plan `id: "northwind\n"` is `plan-manifest-invalid`, now in `manifest-id`'s words. A plan with no `id:` and `legs.assembly: "NorthWind\n"` is `REFUSED 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_READERS` lists 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/repo` name), and two inputs change from `unsafe-value` to `update-upstream-unresolvable` (see "Behaviour changed", above). Review: rv1166 and rv1166c, lane 5's independent opus reviewers, at `2593952c` (issuecomment-6352173084); then rc1166b, lane 5's independent sonnet re-checker, which wrote none of #1166: READY at `5102b429`, H0 of this landing (issuecomment-6371408915), with no MUST or SHOULD; its NITs 1 and 2 are corrected in the body. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 5102b42 passed tests, tests-git-bash and SonarCloud; the head 5c8afef is a clean update-branch merge of main 93d2931, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 3c89065 over main 11560eb: the family_init_values, update_shape_unreadable, value_pattern_line_break, repo_hygiene, agents_md_codes, cli_reference sets, 365 passed, 19 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 #1139 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #1017 (comment)
Goal
scripts/repo_shape.pyendsCOMMIT_REandSHA256_REin$, and every caller reads them with.match. In Python$also matches just before a trailing line feed, so a pin'scommit:ortree_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.What still held on main
Measured on main (
2a119f28, then re-checked on6da757cbe; the use sites of both patterns are the same on823624d93, and no new copy of either pattern was added). Python 3.12.3:.matchon mainCOMMIT_RE/SHA256_REA 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.commitandshape.digests.tree_sha256ending in a line feed ->manifest ok, exit 0.validate-family.py:shape.digests.tree_sha256infamily.yamlending in a line feed passed (family ok, exit 0).shape.commitdid not pass: it exited 1 withshape-pin-manifest-disagree, becausecontracts/shape-pin.yamlandfamily.yaml'sshape.commitdiffer 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'stree_sha256ended in a line feed gavemember-digest-mismatch, not the malformed finding, and a member pin'scommitgavemember-commit-unresolvable, notmember-pin-commit.validate-pins.py: a leg pin's commit gave exit 2pin-commit-unresolvable, notpin-tag-only; a leg digest gavepin-digest-mismatch, notpin-digest-malformed; a shape pin's commit gave onlyshape-pin-manifest-disagree, notshape-pin-commit.adopt-project.py check(_pin_row_problem): the row read as well formed, with noplan-pin-malformed.templates/assembly-root/scripts/bootstrap.pyholds a copy of the commit pattern,COMMIT_40, with the same defect:shape_upstream_noticeprinted its notice for a pin ending in a line feed.shape-doctor.pypin_commitreturned"<hex>\n"for such a leg pin.Checked and not affected:
bump-leg.py --toandfamily.py --at/--togo throughchecked_value(afullmatchthat already refuses a line feed);bump-leg.pyline 2212 strips first;update-shape.pyline 558 readsgit_out, which is stripped;shape-doctor'srecorded_gitlinksplits fields;src/openreposhape/standard.pyusesfullmatch. The@(?P<sha>[0-9a-fA-F]{40})patterns inbump-leg.pyandvalidate-pins.pyare unanchored workflow-ref patterns.What changed
scripts/repo_shape.py:COMMIT_REandSHA256_REend in\Z, not$(comment added).\Zmatches only at the very end of the value, so each caller's.matchrefuses 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_40likewise (comment added).AGENTS.mdtext pins the old pattern bytes (grepped), andtests/test_pinned_copy_text.pyand everytests/test_bootstrap*.pypass unchanged (shard tB and shard tA).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'spin_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 helpershape_of;tests/test_adopt_pins.py: 2malformed()cases (commit-line-break,digest-line-break);tests/test_shape_doctor.py: 1ROOT_PIN_FAULTSparam (a-commit-ending-in-a-line-break).Rows that fail on main
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):test_pin_validator.py,test_manifest_validator.py,test_adopt_pins.py: 16 failed, 307 passed in 294.83s. The 16: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).
Mutants (the new file only,
-rf)repo_shapepatterns back to$COMMIT_REback to$SHA256_REback to$COMMIT_40back to$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.Runs with the change (head
ae30dbd0e0eecfb7092c5ef257da2367d114fc62, worktree clean at each START)git 2.43.0 (
/usr/bin/git):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 ofCOMMIT_RE,SHA256_RE,COMMIT_40, everytests/test_bootstrap*.py(13 of 13),test_shape_doctor.py, 6 of the 19test_update_shape*.pyfiles,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 13test_update_shape*.pyfiles, which name no pin code, ran as a third run (480 passed, 3 skipped in 441.06s, rc=0), andtests/test_scaffold_leg_path_collisions.pyas a fourth (104 passed in 4.35s, rc=0). No full suite was run.git 2.47.1 (
git --versionprinted first in each log):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.Merge-trees
Refreshed after the fix round, against the re-fetched origin/main:
038dafba9dbedd9405b316cc749a04dffe02bb36(tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its_is_aor a reasoned allowlist covers it, and aproject.yamllinked into an unsearchable folder isyaml-unreadable(#430) #1111) is merged into the branch asc7da27dc2, cleanly.4c1f80cf4db6215d8861bd43cd57f72df957d82amerges with main at its own tree,9884a99aeba2a55e486a355ae3483d8c932b4f68: the seven files of this PR.checkandexecuteask a plan'sorg:andid: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 apins:entry that is no pin is said as_read_assays it (#1020) #1125 (head2a53a8e6) still touchestests/test_adopt_pins.py, and its merge-tree with this head is clean, treea026a192669a86abfaed59fbcce2fd1179bb0894.git merge-tree --write-tree origin/main HEADagainst4ed7c7bf581dceb75f816f30f76b776c2b8917f0(merge base6da757cbe), with this PR's head03b2b97b9d3cccc42b4703cb2f7c978693488266: clean, rc=0, tree95d242489e6a8d6ea2be8c454a5e6655922d5712. The difference between that tree and origin/main is exactly the seven files of this PR.6da757cbe. Since then three PRs landed. scaffold-project.py: the--reuse-empty-repohelp, its docs and each remedy that sends a person into that re-run say ssh, a credential helper or askpass can still make it wait, and how to keep it from waiting (#915) #1101 and templates/assembly-root/project.yaml and templates/family-root/family.yaml: the comment abovevisibility:says it is the visibility the run asked for, and that a repository the run reuses keeps its own, where it said "this is all three repositories' visibility" (#913) #1102 touchedREADME.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.pyandtests/test_visibility_field_words.py. 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 (4ed7c7bf5) touchedadopt-project.py,tests/test_adopt_root_shape_paths.pyandtests/test_adopt_root_symlinks.py. None is a file of this PR, andadopt-project.pyholds the same 8 uses ofCOMMIT_REandSHA256_REas before.gh pr list --state open --json number,headRefName,headRefOid,files, this one included), one other touches a file of this PR: adopt-project.py:checkandexecuteask a plan'sorg:andid: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 apins:entry that is no pin is said as_read_assays it (#1020) #1125 (head2a53a8e64a0cce17ab84cd1bf5949bcb6fbd4484) touchestests/test_adopt_pins.py.git merge-tree --write-tree 2a53a8e64a0cce17ab84cd1bf5949bcb6fbd4484 HEADis clean, rc=0, tree5fbd1eaafdf94004ead7142ae9132dc79dc4d1e5.Fix round 1
One new commit on top,
03b2b97b9d3cccc42b4703cb2f7c978693488266, after the independent review ofae30dbd0e0eecfb7092c5ef257da2367d114fc62(not ready on one must). It changestests/test_pin_pattern_line_break.pyand nothing else; no production file changes.Composite asserts (SonarCloud
python:S9073, the two issues at lines 164 and 173).member_pinandholder_shapeended inassert 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 invalidate-pins.py, exit 2,REFUSED neutral-pin-tag-only. On main the commit passedCOMMIT_REand the run exited 1 withneutral-pin-source-unreadable, git refusing the name;test_a_neutral_pin_digest_that_ends_in_a_line_break_is_malformed: the same pin'stree_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'ssource_commit:invalidate-manifest.py, exit 1,manifest-submodule-commitandmanifest-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
94f125c5f5e4b8a0forvalidate-pins.pyand260c4742fe422597forvalidate-manifest.py, the same as before the mutant, and clean against HEAD;repo_shape.pyandbootstrap.pyunchanged at623af12131447f95and4102d375f4c9bc38). The new file only:.rstrip("\n")on the neutral pin's commit before the match (validate-pins.pyline 1790): 1 failed, 45 passed, killed by the neutral-commit row;.rstrip("\n")onsource_commitbefore the match (validate-manifest.pyline 1264): 2 failed, 44 passed, killed by bothsource_commitrows;Runs, at head
03b2b97b9d3cccc42b4703cb2f7c978693488266, worktree clean at each START:tests/test_pin_validator.pyandtests/test_manifest_validator.py: 175 passed in 58.03s, rc=0;tests/test_neutral_pin_validator.py(the new rows use its helpers): 39 passed in 99.62s, rc=0;git --versionprinted first): the new file: 46 passed in 19.32s, rc=0.ae30dbd0e; the two production files and the four other test files are the same at this head.Fix round 2
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).038dafba(tests/test_path_guard_hygiene.py and shape-doctor.py: a bare is_file/is_dir/exists guard in a shipped tool fails by file and line unless its_is_aor a reasoned allowlist covers it, and aproject.yamllinked into an unsearchable folder isyaml-unreadable(#430) #1111), asc7da27dc2, with no conflict.4c1f80cf4changes only the module docstring oftests/test_pin_pattern_line_break.py. It now says what main did:validate-manifest.pypassed both ashape.commitand ashape.digests.tree_sha256that end in a line feed;validate-family.pypassed only the digest. Its commit gaveshape-pin-manifest-disagreewith the same hex on both sides, becausecontracts/shape-pin.yamlholds the commit without the line feed.4c1f80cf4, git 2.43.0:tests/test_path_guard_hygiene.py: 95 passed (46 + 49);Not changed
Other
^...$patterns are used with.matchon values read from files or arguments:PROJECT_ID_RE,DATE_REandQUALIFIED_RE(validate-manifest.py,validate-family.py),REPOSITORY_RE(update-shape.py),PIN_ARG_RE(shape_materialize.py), andNAME_RE,REF_REandPATH_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.Closes #1017
🤖 Generated with Claude Code
Summary by Sourcery
Require pin identifiers and tree digests to end exactly at the expected hexadecimal value, preventing trailing line breaks from being accepted as valid pins.
Bug Fixes:
Enhancements:
Tests: