Skip to content

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

Merged
brettheap merged 3 commits into
mainfrom
fix/1139-anchored-patterns
Nov 8, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

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_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.

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.

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.

What changed

  • 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 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'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)".

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

Mutants

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

Runs

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.

Merge-trees

git merge-tree --write-tree <other> 5102b4294 (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)

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.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 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'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.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).

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:

  • Reject trailing line feeds in manifest, family, member, advisory-date, and repository values instead of accepting them as valid input.
  • Report invalid shape-pin source_repository: values against the pin field rather than the absent --upstream option.
  • Reject member project IDs ending in a line feed before mounting or recording the member.

Enhancements:

  • Use complete-value matching for IDs read from project and family files while preserving the existing ID pattern and refusal codes.
  • Add coverage and guards for all relevant value-pattern readers, including protection against untracked ID-pattern call sites.

Tests:

  • Add regression tests covering trailing-line-feed handling across validators, family operations, adoption checks, and shape updates.
  • Update affected test fixtures for the expanded upstream-opening interface and revised pattern text.

…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>
@sourcery-ai

sourcery-ai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Data structures

Anchor 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.

  • scripts/family.py L310-313, L1133-1137
  • scripts/shape_advisory.py L169-172
  • templates/assembly-root/scripts/validate-manifest.py L116-121, L798-804
  • templates/family-root/scripts/validate-family.py L193-198, L588-593
  • update-shape.py L208-211

API

Extend 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.

  • update-shape.py L671-678, L692-700 (+2 small edits)

Uses of the API

Pass the pin path only when no --upstream argument was supplied, preserving the distinction between a pin-derived source and an explicitly supplied upstream.

  • update-shape.py L2422-2423

Tests

Update the upstream-opening stand-in for the added optional pin-path argument.

  • tests/test_update_shape_unreadable.py L228-229

Keep the family validator's source-text assertion aligned with its end-anchored date pattern.

  • tests/test_family_init_values.py L592

Add regression coverage for line-feed-terminated values, pin-specific refusal messages, and the inventory of end-sensitive patterns and ID-pattern readers.

  • tests/test_value_pattern_line_break.py L1-396

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

Independent review of #1166 (#1139) for lane openRepoShape-5

Reviewers: rv1166, then rv1166c (Opus 5.5), read-only, at head 2593952c9519c4b96de295657635fc711f75c695. That head equals refs/pull/1166/head by ls-remote at 20:04Z and again at 21:44Z. main is 961640c6 (#1155). From 038dafba, the base the body measured, to 961640c6, only docs/release-notes/v1.2.0.md changed.

Verdict: READY. There are no MUST or SHOULD findings.

  • Every row the body says fails on main does fail there, for the reason the body gives.
  • Every mutant I ran is killed.
  • The sweep found no value that main accepts and the change now refuses, other than a value that ends in a line feed.
  • Pinned copies stay compatible, every gate passes on the commit, and every merge-tree is clean.

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)

  1. The patterns. At the head these end in \Z:

    • DATE_RE and QUALIFIED_RE in both validators (validate-manifest.py:120-121, validate-family.py:194-195);
    • MEMBER_RE (scripts/family.py:312-313);
    • DATE_RE in scripts/shape_advisory.py:166;
    • REPOSITORY_RE in update-shape.py:211.

    fullmatch is used at validate-manifest.py:803 (manifest-id), validate-family.py:589 (family-id) and scripts/family.py:1137 (member-bad-id). That is what the body says.

  2. The rows fail on main. I swapped main's bytes of the 5 production files into the scratch worktree and ran tests/test_value_pattern_line_break.py (git 2.43.0, Python 3.12.3). The result was 19 failed, 10 passed in 26.78s. Afterwards I put the files back and checked their sha256 (restored=True).

    • The 19 failing ids are exactly the 19 the body lists (diff of the two sorted lists is empty).
    • The 10 that pass are the baselines the body names.
    • Each row fails for the answer the body's "What held on main" table gives. For example:
      • [id] fails on FINDING manifest-topic: ... expected 'xf-project-atlas\n' derived from id 'atlas\n';
      • [elected_on] and [leg-repository] fail on manifest ok, exit 0;
      • test_add_refuses_... fails on add, which mounted the member and committed 9f19175faf7a: .gitmodules, members/IRQS, family.yaml;
      • the pin rows fail on REFUSED unsafe-value: --upstream is 'opensoft/openRepoShape\n'.
  3. Mutants. I chose 4 of the writer's 13 and added 1 of my own. Each was applied alone to one production file at the head, then run against the new file plus test_family_member_name.py::test_every_spelling_of_the_member_name_rule_is_the_same_pattern. Each file was restored with its sha256 checked (restored=yes for all). All 5 are killed:

    Mutant File Result Killed by
    M3: the id read with .match validate-manifest.py 2 failed, 28 passed 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
    M8: a member's id read with .match scripts/family.py 1 failed, 30 passed test_add_refuses_a_member_whose_id_ends_in_a_line_break
    M11: prepare passes no pin update-shape.py 3 failed, 27 passed 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 failed, 29 passed test_a_pins_source_checked_value_refuses_is_named_as_the_pins
    X1 (mine): prepare names the pin even when --upstream is passed update-shape.py 1 failed, 29 passed test_an_upstream_passed_with_a_line_feed_is_named_as_passed

    The killers of M3, M8, M11 and M12 are the ones the body's table names. I did not run the other 9 of the writer's 13 (M1, M2, M4-M7, M9, M10, M13): not measured by me.

  4. The sweep: no value without a line feed changes its answer. I ran the same probe at the head and on main's production bytes (9 rows each, 9 passed both times). It rewrites, one at a time, every scalar line of a scaffolded project.yaml (32 lines), a family holder's family.yaml (35 lines) and an adoption plan (plan's own output, 62 lines) as a double-quoted value with a trailing \n. It also runs each file with CRLF line ends, and block scalars |, > and |-.

    • Of the 163 probe lines, 18 differ between main and the head. 16 of them are an answer that changed, and every one is a value that ends in a line feed:
      • project.yaml: id and id: | (manifest-topic became manifest-id), elected_on, elected_on: | and >, and the three legs' repository (manifest ok became a finding);
      • family.yaml: id, created_on and created_on: | (family ok became a finding), and two members' repository (member-repository-name became member-repository);
      • the plan's id (manifest-topic became manifest-id, both under plan-manifest-invalid);
      • add of a member whose id: ends in a line feed, and the rows it leaves (2 lines).
    • The other 2 differ only in a gitlink sha, because main's bytes build different fixture commits. They are the members' path, which is still member-gitlink-unnamed on both.
    • The 5 rows keyed by a sha or a temporary path (the member pins' commit and tree_sha256, the plan's local_path) give the same answer on both.
    • A CRLF project.yaml, family.yaml and plan each give exit 0 on both. load_yaml reads with universal newlines (repo_shape.py:1339). A \r was never taken by $ anyway, so no CR answer moves.
    • |- gives exit 0 on both. | and > keep the line feed by YAML's own rule, and are now refused by name. No tool writes a block scalar (repo_shape.py:950 says so), so only a hand edit reaches one.
    • By code reading (not measured end to end), the values a tool writes into these fields come from argv, which checked_value already refuses with a line feed, or from date.today(). None comes from a subprocess's output.
  5. Gates on the commit (dirty=0, Python 3.12.3, a stub gh first on PATH, a bogus token, no global git config), all rc=0. They ran at 20:06-20:28Z, and the head has not moved since:

    Run Git Result
    the 6 files of the stand-in gate (test_family.py, the four test_update_shape_* files, the new file) 2.43.0 298 passed in 257.80s
    tests/test_value_pattern_line_break.py 2.47.1 29 passed in 23.69s
    test_pinned_copy_text.py, test_path_guard_hygiene.py, test_repo_hygiene.py, test_agents_md_codes.py, test_cli_reference.py 2.43.0 306 passed in 63.98s
    15 more test files that name a changed function, pattern or code (adopt, scaffold, manifest-validator, family-init, member-name, member-path, family.py-unreadable, local-remote-dir and others) 2.43.0 1611 passed, 19 skipped in 1033.50s
    tests/test_shape_doctor.py 2.43.0 445 passed, 3 skipped in 745.67s

    CI on this head also passed: tests (the full suite on 3.14), tests-git-bash, package and SonarCloud. rv1166 read that at about 20:04Z, and it was not read again. The new file has no assert that joins conditions with and or or, and it is ASCII.

  6. Pinned-copy compatibility.

  7. The pin's refusal names the pin. It names root_of(args) / SHAPE_PIN, the same absolute spelling that read_pin and check_pin use. shape-doctor.py always passes upstream= (lines 3341 and 6226), so its rows are unchanged.

  8. Merge-trees. git merge-tree --write-tree <other> 2593952c at 2026-10-22T21:50Z (git 2.43.0), against main and every open PR that touches one of this PR's 8 paths, at heads re-listed at 21:50Z (45 open). All are clean:

    Against Head Tree
    main 961640c6 aa1de7bc
    family.py: a failed remove's by-hand lines spell a leg mounted at a path that is not UTF-8 as a printf word of octal escapes ending in a slash, so git -C <member>/<leg> and submodule update -- <leg> typed in any POSIX shell name that leg (#1086) #1098 dad7961a 5c4f23f1
    validate-family.py: gitmodules-path-twice and the check for values git dies on read the .gitmodules git reads, the index's where the work tree's is gone, and every section's lines end git add -- .gitmodules, so a plain git commit after them takes the section out of HEAD (#1077) #1124 8dfa32bd 6ae5e02b
    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 4c1f80cf eae12927
    update-shape.py: update-accept-local-missing-name asks a copy an earlier 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 a7bfee1b 67858a0b
    shape-doctor.py and shape_advisory.py: a .git or an agent file linked into a folder this user cannot search is said once in what is here or agent files, <name>: cannot be read (<reason>), never absent; a .git offers no way in (#1110) #1144 89486f69 630ea103
    shape-doctor.py, validate-pins.py and validate-manifest.py: a root whose own .git this user cannot read is named .git: cannot be read (<reason>), so the doctor never reads it as DRIFTED, and both pinned validators refuse it, exit 2 (#1146) #1147 b75babee 6f690ce8
    shape_advisory.py and shape-doctor.py: a leg's AGENTS.md this user cannot read leaves whether it is a leg unknown, so the way in offers neither command and points at what is here, and the other way in and the Triad row are silent (#1110) #1159 66e68f94 f4d96aa8
    family.py and the family root's siblings.py: a holder's .gitmodules this user cannot read, as through a link into a folder it cannot search, is gitmodules-unreadable, never no section for remove's rollback or no url for make siblings (#1110) #1164 3162b523 1e8e43c8
    bootstrap.py, validate-family.py and siblings.py: a root or holder whose .git this user cannot read is refused git-repo-unreadable, exit 2, not a traceback or not-a-git-repo, and the doctor's members row names it once, never drift (#1148) #1172 5aa0f116 bd57903d

Findings

  1. NIT, now measured end to end: the Goal's "Every ^...$ pattern that reads a value from a file" claims too much.

    • The naming-policy patterns from contracts/repository-naming.yaml are still read with .match (scripts/repo_shape.py:2264, 2266, 2432, 2435, and topic_pattern).
    • validate-repository-naming.py --project project.yaml runs in the root template's Makefile:48 and validate.yml:310. At the head (and on main) it exits 0 on a leg repository: "testorg/Atlas\n", and prints the line feed inside its table (' Atlas', then project-leg/assembly on the next line). The same holds for -spec and -code.
    • The project's gate as a whole still fails, because validate-manifest.py now refuses that manifest.
    • Suggest: narrow the Goal to the patterns the issue names, or open a follow-up for the naming-policy patterns.
  2. NIT, measured: the commit message says "the three readers of an id from a file ask it with fullmatch", but there are more.

    • adopt-project.py:6780 and :6791 (plan-bad-id) still read the plan's id: and the id derived from legs.assembly with .match.
    • Neither lets a line feed through:
      • the plan's id: "northwind\n" is plan-manifest-invalid, now in manifest-id's words, as the body says;
      • 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 at the head alike.
    • So this is wording only. Fix it in the squash message at landing.
  3. NIT, measured: the title says "a shape pin's source_repository: is refused in the pin's name", but one refusal still blames the flag.

    • With no --upstream, and run from the root, source_repository: "contracts" gives REFUSED update-upstream-not-a-repo: --upstream contracts exists but is not a git repository, exit 2.
    • source_repository: "scripts/repo_shape.py" gives the same.
    • The answer is the same on main. pin_path is in scope at update-shape.py:686-687.
    • The body discloses it. Make it a one-line follow-up, or narrow the title.
  4. FOLLOW-UP, measured: a holder that main's add already wrote stays family ok.

    • On main, add of a member whose project.yaml reads id: "irqs\n" commits a row id: "irqs\n", and the holder's gate then says family ok.
    • Under mutant M8 (the head with main's .match in add), the same row is written, and the head's validate-family.py still exits 0, family ok.
    • By code reading: the row's id is compared with the member manifest's id by string equality (validate-family.py:1608). _check_member_ids_unique does the same. Neither asks PROJECT_ID_RE of a member row.
    • The body names this as a follow-up candidate. The PR closes the add path, so no new holder gets such a row.
  5. NIT: two inputs move from one existing code to another.

    • A pin's source_repository: ending in a line feed, and argv --upstream "owner/repo<LF>", both go from unsafe-value to update-upstream-unresolvable. Measured on main and at the head by the rows above.
    • The body says so for argv. The issue's Verify says "No refusal code changes". That reads as "no code added or renamed", and none is.
    • Nothing pins the old answer: every gate and CI are green.
    • Suggest: one line in the body tying this to the issue's Verify.
  6. NIT: the body's Merge-trees section is stale.

  7. NIT, optional: the new guard covers only four pattern names.

    • test_every_spelling_of_a_value_pattern_is_in_the_table holds DATE_RE, QUALIFIED_RE, MEMBER_RE and REPOSITORY_RE only.
    • Nothing holds a new PROJECT_ID_RE.match reader of a file. The two in adopt-project.py are live examples, harmless today (finding 2).
  8. FOLLOW-UP, outside validate-manifest.py and validate-family.py: PROJECT_ID_RE, DATE_RE and QUALIFIED_RE end in $, so an id:, a date or a repository: that ends in a line break passes as well formed (#1126 follow-up) #1139: values that no pattern reads still take a trailing line feed. The sweep found these manifest ok / family ok, exit 0, at the head and on main alike:

    • in project.yaml: name, a spec or code leg's path (spec\n), shape.repository, 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 path: "spec\n" is checked only by _leg_path_findings, which asks no pattern.
    • 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 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's.

  9. NIT, for the record: with --upstream passed, a pin's source_repository: that ends in a line feed is not refused.

    • update-shape.py check --upstream <clone> over such a pin 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, 5121). I measured check only, not apply.
    • The body names this under "Not changed, seen while measuring". It belongs with finding 3's follow-up.

Logs

$Q/v124/rv1166/ on the lane-5 bench:

  • logs/onmain-new.log, logs/probe-head.log, logs/probe-main.log and logs/mut-*.log;
  • probe-head.txt, probe-main.txt and probe-M8.txt;
  • merge-trees-c.log, gates.summary and gates2.summary;
  • compat.txt.

Lane: openreposhape-5 (openRepoShape-5)

This was referenced Oct 22, 2026
…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>
@brettheap brettheap changed the title validate-manifest.py, validate-family.py, family.py and update-shape.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) 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) Oct 23, 2026
@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026
@brettheap

Copy link
Copy Markdown
Contributor Author

Re-check of #1166 (#1139) NIT round for lane openRepoShape-5

READY at 5102b4294

Re-checker rc1166b, read-only: no commit, push or GitHub write. I checked only the delta since the review's head 2593952c9. That delta is the ID_READERS guard commit plus the replaced title and body.

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.

Findings

MUST: none.

SHOULD: none.

NIT 1: a new fullmatch reader also fails the guard, so "read with fullmatch" alone does not pass it.

  • Where it is claimed:
    • tests/test_value_pattern_line_break.py:57-60 (module docstring);
    • :195-199 (the test's docstring: "it is read with fullmatch, or it is in ID_READERS ... or this fails");
    • the body: "A new match or search reader fails it by file and method, until it is read with fullmatch or listed with its reason".
  • What I measured:
    • I appended def _rc_probe(value): return PROJECT_ID_RE.fullmatch(value) to update-shape.py.
    • The guard failed with Left contains 1 more item: {('update-shape.py', 'fullmatch'): 1}.
    • The table is exact in both directions. Making one adopt-project.py reader unreachable (if False: in place of the plan-id: test at :6791) failed it too: {('adopt-project.py', 'match'): 2} != {('adopt-project.py', 'match'): 3}.
  • Why it is a NIT: the assert id_pattern_calls() == expected at :200 has no message, so the author sees only a dict diff.
  • Fix: say "listed in ID_READERS" in the docstring and the body, or give the assert a message that names ID_READERS. A sibling that adds any new reader, fullmatch included, must add a row. The body's scan found none among the open PRs.

NIT 2: for adopt-project.py the "argv --id: checked_value refuses a line feed after it" reason is false. A line feed is still refused.

  • Where it appears:
    • ID_READERS: the scaffold-project.py and scripts/family.py rows (:159, :161) and the adopt-project.py row (:162-167, "argv --id, as above");
    • the module docstring's "LEFT AS THEY ARE" bullet;
    • the body's "Left as they are" bullet and the new second-guard bullet;
    • the second commit's message ("the three match readers of argv (checked_value refuses a line feed after them)").
  • What I measured: adopt-project.py plan --id=northwind<LF> (head 5102b4294, a local source, through conftest's write_plan) gave:
    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-]*$.
    
    It exited 2 and wrote no plan.
  • Why the reason is wrong:
    • adopt-project.py never calls checked_value("--id", ...). _checked_flag_inputs checks --project, --tracking-branch, --pin-owner, the leg paths and --org.
    • tests/test_derived_id_refusals.py TOOLS pins that answer for scaffold and family only.
  • So the claim holds for two of the three argv readers. For adopt-project.py the refusal comes from the project's own validator, adopt-manifest-invalid in manifest-id's words, and the id is refused all the same.
  • Fix: correct the body now. Leave the checked_value clause out of the squash message, or say it of the scaffold and family.py init only. Rewording the adopt-project.py string in ID_READERS is optional, because it costs a new head. The issue itself says adopt's --id was "not measured end to end".

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).

  • _ID = PROJECT_ID_RE followed by _ID.match(value) in update-shape.py passes both guards (2 passed). By reading, from repo_shape import PROJECT_ID_RE as X does the same.
  • A match in a string or a comment is correctly not counted: 2 passed.
  • The body claims only PROJECT_ID_RE.x(...) and repo_shape.PROJECT_ID_RE.x(...), so nothing in it is false.

Review items the body says it fixed

The numbers are the review's, in issuecomment-6352173084.

  • [1] Goal over-claims: fixed.
    • The Goal now names the patterns the issue names, and a "What this PR does not cover" list says what is left.
    • The issue's own Direction names PROJECT_ID_RE, DATE_RE, QUALIFIED_RE and REPOSITORY_RE, "wherever the validators define or copy them".
    • The naming-policy follow-up is listed.
  • [2] Commit-message wording: fixed as a landing instruction only.
    • No pushed commit changed. The body's "For the squash message at landing" carries the measured facts, and the second commit's ID_READERS lists both adopt-project.py plan-bad-id readers.
    • The squash message must also avoid the NIT 2 clause.
  • [3] Title blames the flag for one refusal: fixed in the title text.
    • title.txt narrows the clause to "a pin's source_repository: that is no path or owner/repo name is named as the pin's". It is 250 characters.
    • update-upstream-not-a-repo is listed as not covered.
    • I did not read the live title back.
  • [4], [8], [9]: now under "Follow-ups (not in this PR)". I did not re-measure their contents, which are the review's.
  • [5] Two inputs change code: fixed. The "Behaviour changed" section states both inputs, and the "What held on main" table holds the old answers.
  • [6] Stale merge-trees: refreshed at 12:08Z, and the refresh reproduces. See NIT 3 for what has moved since.
  • [7] Guard: fixed, see the probes below.

Checked and true

  • Delta. git diff 2593952c9..5102b4294 --stat shows one file, tests/test_value_pattern_line_break.py, 62 insertions and no production file. It has no non-ASCII line and no assert that joins conditions with and or or.

  • Test file on the head, $Q/v110/run9.sh, EMAIL exported, dirty=0:

    Git Result
    2.43.0 30 passed in 31.59s
    2.47.1 (git version 2.47.1 confirmed first on the runner's PATH) 30 passed in 29.43s
  • Hygiene five files (test_pinned_copy_text, test_path_guard_hygiene, test_repo_hygiene, test_agents_md_codes, test_cli_reference), git 2.43.0: 306 passed in 75.03s.

  • Main's bytes.

    • I swapped in 961640c68's bytes of the five production files and ran the whole test file: 20 failed, 10 passed in 25.70s, with dirty=5 in the START line.
    • The 20 failing ids are identical to the body's list (compared as sorted sets).
    • The 10 passing ids are the seven test_a_pattern_takes_its_value rows, test_every_spelling_of_a_value_pattern_is_in_the_table and the two double-quoted rows.
    • The files were put back and checked: git status clean, tracked files equal to the head.
  • Plant probes. Each was applied alone to one tracked file at 5102b4294. I ran test_every_reader_of_the_id_pattern_is_in_the_table plus the first guard, then restored the file and checked its sha256 against the head's (restored=True each time):

    Probe New guard First guard
    P1 PROJECT_ID_RE.match(value) appended to update-shape.py failed: ('update-shape.py', 'match'): 1 more passed
    P2 repo_shape.PROJECT_ID_RE.search(value) appended to scripts/shape_advisory.py failed: ('scripts/shape_advisory.py', 'search'): 1 more passed
    P3 validate-manifest.py fullmatch changed to match failed: match 1 vs fullmatch 1 passed
    P4 validate-family.py fullmatch changed to match failed: same shape passed
    P5 scripts/family.py fullmatch changed to match failed: match 2 vs 1, fullmatch missing passed
    P6 main's bytes of the five files failed: the three readers named in the diff n/a

    My own plants X1 (new fullmatch), X2 (alias) and X3 (reader removed) are in NITs 1 and 4 and the NIT 1 discussion; X4 (string and comment) is in NIT 4.

  • Table against code. git grep at the head finds the readers ID_READERS lists:

    • adopt-project.py: :5140, :6780 and :6791, all match;
    • scaffold-project.py: :1126, match;
    • scripts/family.py: :1137 fullmatch and :2493 match;
    • validate-manifest.py:803 and validate-family.py:589, both fullmatch.

    The six (module, method) pairs and their counts equal the table.

  • Body merge-trees.

  • Open-PR numbers and sibling scans (from prlist.json at 12:02Z; all 54 other heads are present locally):

    • 55 open PRs, 24 of them touching adopt-project.py.
    • The PRs touching the 8 paths are exactly those in the body's table.
    • No other head adds or removes a line outside tests/ that names PROJECT_ID_RE.
    • None adds or removes a line naming open_upstream, def opening or REPOSITORY_RE in update-shape.py or tests/test_update_shape_unreadable.py.
  • Resume after the 15:46Z host reboot. All 13 of my logs ended with an END line before it. git status --porcelain showed only my untracked probe test. I moved it out of the worktree. The sha256 of the five production files, adopt-project.py and the test file equals the head's.

Not checked

  • CI on 5102b4294, and the live PR title as posted.
  • Any test run on the merge with ef619c74a. Only the merge-tree, which is clean, and the reader counts on it.
  • Python 3.14, which CI runs. The bench has 3.12 only. The new guard parses every tracked .py with ast, as the first guard's pattern_spelled already does.
  • The review's follow-up facts [1], [3], [4], [8] and [9], and the review's own gates (the 15-file run, test_shape_doctor.py, the sweep and the compat imports). They did not change in this round.
  • Mutants M1, M2, M4-M7, M9, M10 and M13. These are the writer's runs alone, as the body says.
  • Heads of open PRs after 12:02Z.
  • apply and the other adopt paths for --id. My NIT 2 measurement is plan only.

(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)

brettheap added a commit that referenced this pull request Oct 23, 2026
…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>
@brettheap

Copy link
Copy Markdown
Contributor Author

(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)

@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review November 8, 2026 14:47
@sourcery-ai

sourcery-ai Bot commented Nov 8, 2026

Copy link
Copy Markdown

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

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

@brettheap
brettheap merged commit 105b5da into main Nov 8, 2026
5 checks passed
@brettheap
brettheap deleted the fix/1139-anchored-patterns branch November 8, 2026 14:48
brettheap pushed a commit that referenced this pull request Nov 8, 2026
Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Nov 8, 2026
…e for: tests/test_value_pattern_line_break.py

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…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>
brettheap pushed a commit that referenced this pull request Nov 8, 2026
Main 0d5704b is five commits past #1144's squash b1f45fa (#1169, #1223,
#1166, #1133, #1104). This is the third of the three merges that restack
#1159 after #1144 landed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Nov 8, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant