Repository navigation
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
Conversation
… apply writes against its module as the run leaves it, so a copy an earlier apply kept, read `unchanged`, is asked too, and no `siblings.py` stopping on its import is copied (#1082) #536 built the guard's set of modules from the rows `--accept-local` names in this run, so a copy an earlier apply kept, which reads `unchanged` once the upstream stops changing it, was never asked. The next apply wrote an importer beside it unasked. Measured offline: a holder cut at 6476c17 that applied 2a119f2 keeping its own bootstrap.py was then written main's siblings.py, with its `untraced` import (#1075), by `apply --at 6da757c`. That apply exited 0 and committed, and `siblings.py --help` stopped on an ImportError. The same held for #1023's `member_make_environment` with #1023 landed in two halves. Each written .py copy is now asked against its module as the run leaves it: a copy this run keeps (`(kept)`), a copy an earlier run kept, whose pin records the project's own bytes (`(kept by an earlier run)`), or the upstream's own bytes at the target (`(the upstream's)`). The code stays `update-accept-local-missing-name`, and a refusal whose every line is one this run's --accept-local makes prints what it printed. The guard still runs before `confirm` and before any write. An importer this run does not write is still not asked, but for a kept one against a module this run writes (#604). So a copy an earlier run kept that imports a name the written module drops is a known limit, named in the docstring. Closes #1082 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideImplementationExpand apply-time missing-import validation to evaluate module bytes as the run leaves them and detect disagreements involving copies kept by earlier runs, with source-specific refusal and remediation behavior.
TestsAdd end-to-end and unit coverage for earlier-kept copies as importers and imported modules, including refusal timing, diagnostics, controls, and remediation paths.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Independent review of #1133 (#1082) at The change does what #1082's Verify section asks. Both two-step cases are now refused before anything is written, the one-step refusal prints exactly what main prints, and no valid run I tried is refused. There are two SHOULDs: a real gap the PR documents but that is still inside the issue's Goal, and one claim of unchanged words that no row pins. Neither blocks a draft. FindingsSHOULD 1: the known limit is a real gap inside #1082's Goal, so it needs a follow-up issue before this lands.
SHOULD 2: nothing pins the opening of a
NIT: the exits row does not check the row state the remedy promises.
NIT, the coordinator's #1111 check: there are none.
Checked and true
Could not verify
Checklist candidates
Scratch left
Lane: openreposhape-5 (openRepoShape-5) |
…rlier apply kept, read `unchanged`, as an importer too, so a plain apply that drops a name it imports is refused before any write, naming who kept it and giving it its exits (#1082) Fix round 1 of #1133, after rv1133's review. #604 asked only a copy this run's --accept-local keeps against a module this run writes, so the head left a known limit inside #1082's Goal. Measured by the reviewer offline: a holder whose `apply --accept-local scripts/siblings.py` kept its own siblings.py, edited to take `told_past` from bootstrap.py, read `unchanged` against a target whose bootstrap.py renamed it. A plain `apply` exited 0, copied bootstrap.py, committed, and `siblings.py --help` stopped on an ImportError. Main's tool and the head's behaved the same. The rows read `kept by an earlier run` as a module are now asked as importers too, beside the kept ones. `_names_dropped` still limits the refusal to names this run takes away, so a copy an earlier run kept whose names all stay still applies. Its line reads `(written) no longer defines X, which Y (kept by an earlier run) imports from it`; the general opening now fits every kind of line ("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"); and the remedy gives that copy its two exits, `--accept-local` on it once changed, or the upstream's bytes by `git show`. A refusal whose every line is one this run's --accept-local makes still prints what main prints, and a #604 row now pins its opening. The code stays `update-accept-local-missing-name`. The known-limit paragraph and the row that pinned the limit are replaced by rows that hold the refusal, its controls and its exits; the exits rows also check the state `check` reads after each exit. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re-check of #1133 (#1082) for lane openRepoShape-5 Verdict: READY at FindingsNIT 1: nothing pins how a refusal in both directions at once opens.
NIT 2: the Merges section predates #1166.
NIT 3: the title names only the first round's direction.
Checked and trueEach finding is met. I ran the reviewer's own scripts, copied into my folder, with the head's tool and with main's.
Rows fail where the body says. I ran the head's test file with the other tools in scratch clones:
Mutants. Each was applied alone in a scratch clone, then restored by checkout with its sha256 checked (
N3, N8, N9 and R2 fail the same counts the body gives. What the round could have broken. I found nothing.
Gates and runs, all through
Merges and body claims.
Could not verify
Scratch left
Lane: openreposhape-5 (openRepoShape-5) |
…ts the refusal opens with `KEPT_LEAD`, as a refusal of this run's own `--accept-local` always has, so an opening that falls back to the general words there fails it (#1082) rc1133's NIT 1 on #1133: update-shape.py:4561 gives a refusal whose lines are all ones this run's `--accept-local` makes the opening `KEPT_LEAD`, and `test_both_directions_in_one_run_are_one_refusal` (a `(kept) lacks` line plus a `(written) no longer defines` line) pinned its lines, its clause and its remedy but not its opening. rc1133's mutant O1, `lead = (KEPT_LEAD if len(sides) == 1 and sides <= {KEPT_MODULE, WRITTEN_MODULE} else LEFT_LEAD)`, survived the 185 rows. The row now asserts `startswith(KEPT_LEAD)`, and its docstring says why the opening is that one. Tests only; update-shape.py is untouched. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
update-accept-local-missing-name asks each copy an apply writes against its module as the run leaves it, so a copy an earlier apply kept, read unchanged, is asked too, and no siblings.py stopping on its import is copied (#1082)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)
|
Re-check of #1133 (#1082) NIT round for lane openRepoShape-5 READY at Checked and trueNIT 1: the
NIT 3: the title is true
NIT 2: the Merges row for #1166
Body and commit
Not measured
Lane: openreposhape-5 (openRepoShape-5) |
…py: an `id:`, a date or a `repository:` ending in a line feed is refused by name, and a pin's `source_repository:` that is no path or `owner/repo` name is named as the pin's (#1139) (#1166) The value patterns the issue names refuse a value that ends in a line feed, by name, as #1126 (#1017) makes `COMMIT_RE`, `SHA256_RE` and `COMMIT_40` do: - `DATE_RE` and `QUALIFIED_RE` in `validate-manifest.py` and `validate-family.py`, `MEMBER_RE` in `scripts/family.py`, `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`; - `PROJECT_ID_RE`, read with `fullmatch` where the two validators and `family.py add` read an id from a file. A shape pin's `source_repository:` that is neither a path on this machine nor an `owner/repo` name is refused in the pin's name, not `--upstream`'s. Values without a line feed keep their outcome, and no refusal code is added or renamed. Two inputs do move from one existing code to another (see "Behaviour changed", below). **What this PR does not cover, and the title does not claim:** - the naming-policy patterns that `scripts/repo_shape.py` reads from `contracts/repository-naming.yaml`, which `validate-repository-naming.py` still reads with `.match`; - `update-upstream-not-a-repo`, which still says `--upstream` when a pin names a folder that exists and is not a repository. Both are follow-ups, with the other findings of the independent review, under "Follow-ups (not in this PR)". This does not need #1126 and does not touch its files or its test file. Measured on main `038dafba` (Python 3.12.3, git 2.43.0) by a probe of each tool, before any change. Each value is written in the file as a YAML double-quoted `"...\n"`, which the reader takes as the value and a line feed. | Read from | Value | On main | |---|---|---| | `project.yaml` (`validate-manifest.py`) | `id: "atlas\n"` | only `FINDING manifest-topic: topic is 'xf-project-atlas', expected 'xf-project-atlas\n' derived from id 'atlas\n'`, exit 1 | | `project.yaml` | `elected_on: "2026-09-02\n"` | `manifest ok`, exit 0 | | `project.yaml` | a leg's `repository: "testorg/Atlas-spec\n"` | `manifest ok`, exit 0 | | `family.yaml` (`validate-family.py`) | `id: "inkrouter\n"` | `family ok: InkRouter (inkrouter` and a line break, exit 0 | | `family.yaml` | `created_on: "2026-09-04\n"` | `family ok`, exit 0 | | `family.yaml` | a member's `repository: "InkRouter/IRRS\n"` | `FINDING member-repository-name: ... whose name is 'IRRS\n'`, exit 1 | | a member's `project.yaml`, read by `family.py add` | `id: "irqs\n"` | mounted and committed, exit 0, with the row `id: "irqs\n"`; the holder's gate then said `family ok` | | `contracts/shape-pin.yaml`, `update-shape.py check` with no `--upstream` | `source_repository: "opensoft/openRepoShape\n"` | `REFUSED unsafe-value: --upstream is 'opensoft/openRepoShape\n'`, exit 2 | | the same | `source_repository: "opensoft/open Repo"` | `REFUSED update-upstream-unresolvable: --upstream 'opensoft/open Repo' is neither ...`, exit 2 | | the same | `source_repository: "-opensoft/openRepoShape"` | `REFUSED unsafe-value: --upstream is '-opensoft/openRepoShape'`, exit 2 | | an adoption plan (`adopt-project.py check`) | `id: "northwind\n"` | `FINDING plan-manifest-invalid: id is 'northwind\n', which the project's own validate-manifest.py refuses: manifest-topic: ...`, exit 1; `execute` refused it, exit 2, before any leg | In-process on the same main: `repo_shape.PROJECT_ID_RE.match('atlas\n')`, `shape_advisory.DATE_RE.match('2026-09-02\n')` and `REPOSITORY_RE.match('a/b\n')` each matched, and `date.fromisoformat('2026-09-02\n')` raised `ValueError`. - **`DATE_RE` and `QUALIFIED_RE`** in `templates/assembly-root/scripts/validate-manifest.py` and `templates/family-root/scripts/validate-family.py` end in `\Z`. So do `MEMBER_RE` in `scripts/family.py` (the third spelling `tests/test_family_member_name.py` holds equal to the two `QUALIFIED_RE`), `DATE_RE` in `scripts/shape_advisory.py` and `REPOSITORY_RE` in `update-shape.py`. - **`PROJECT_ID_RE` keeps its `$`.** Every id refusal prints its text (`must match ^[a-z0-9][a-z0-9-]*$`, pinned in six test files), the validators run against each project's own copy of `repo_shape.py`, and its definition sits inside #1126's hunk. The three callers that read an id from a file ask it with `fullmatch` instead: `validate-manifest.py` (`manifest-id`), `validate-family.py` (`family-id`) and `family.py`'s reading of a member's `project.yaml` (`member-bad-id`, which `add` now says before anything is mounted). - **`update-shape.py`.** `open_upstream(spec, pin_path=None)` takes the pin the value was read from, which `prepare` passes when no `--upstream` was given. Then `update-upstream-unresolvable` says ``<root>/contracts/shape-pin.yaml: `source_repository:` is '...', which is neither a path on this machine nor an `owner/repo` name``, with a remedy to write the pin's field or pass `--upstream`, and `checked_value` names the same field. **Code kept:** `update-upstream-unresolvable`, the existing code for a value that is not an `owner/repo` name. A line feed now reaches it, since the pattern no longer takes one. Before, `checked_value` refused it as `unsafe-value`. - **One existing test line:** `tests/test_family_init_values.py` finds validate-family's `DATE_RE` line by its text, which now ends `\Z`. - **One existing stand-in:** the `temporaries` fixture in `tests/test_update_shape_unreadable.py` stands in for `open_upstream` with `def opening(spec)`, which the new two-argument call refused with `TypeError: ... takes 1 positional argument but 2 were given`. It now takes `pin_path=None` and hands it on to the real `open_upstream`, so `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]` asks what it asked before. - **The rows** are in a new file of their own, `tests/test_value_pattern_line_break.py`. - **A second guard, in a second commit (`5102b4294`, a test only, no production file).** The first guard holds the four pattern names this PR ends in `\Z`, and nothing held `PROJECT_ID_RE`, which keeps its `$`. `test_every_reader_of_the_id_pattern_is_in_the_table` reads every tracked `.py` outside `tests/` with `ast` (it runs none) and holds each call of a method of `PROJECT_ID_RE` (`PROJECT_ID_RE.x(...)` or `repo_shape.PROJECT_ID_RE.x(...)`) to the table `ID_READERS`: - the three `fullmatch` readers of an id from a file (`validate-manifest.py`, `validate-family.py`, `scripts/family.py`); - the modules that still `match` it, each with its reason: argv `--id` in `scaffold-project.py` and `scripts/family.py`, where `checked_value` refuses a line feed after the pattern; argv `--id` in `adopt-project.py`, which the project's own `validate-manifest.py` refuses (`adopt-manifest-invalid`, below; `ID_READERS` still gives it the `checked_value` reason); and `adopt-project.py`'s two `plan-bad-id` readers, of the plan's `id:` and of the id derived from `legs.assembly`, which a line feed never passes (below). A new reader of `PROJECT_ID_RE`, `fullmatch` included, fails it by file and method until it is listed in `ID_READERS` with its reason. The table is exact both ways, so a reader taken away fails it too. **Left as they are, and why:** - the argv readers of `PROJECT_ID_RE` (`--id` in `scaffold-project.py`, `family.py init` and `adopt-project.py`). The first two hold the id to `checked_value` after the pattern, which refuses a line feed as `unsafe-value` naming `--id`, and `tests/test_derived_id_refusals.py` pins that answer for them. `adopt-project.py` never calls `checked_value` on `--id`: `plan` with `--id` set to `northwind` and a line feed exits 2 with `adopt-manifest-invalid`, in the words of the project's own `validate-manifest.py` (`manifest-id`), and writes no plan (lane 5's re-check, issuecomment-6371408915, NIT 2; measured on `plan` only); - `adopt-project.py`'s reading of a plan's `id:`: `check` and `execute` hold it to the project's own `validate-manifest.py`, which refused it on main (above) and now names it in `manifest-id`'s words. It is the same value, refused under the same code, `plan-manifest-invalid`, before any leg exists. The id derived from `legs.assembly` is read with `.match` too, and a line feed in `legs.assembly` is `unsafe-value` before that id is asked (the review measured both, on main and here alike). This keeps the open PRs on `adopt-project.py` (24 at 12:02Z) clear; - `PIN_ARG_RE`, and `NAME_RE`/`REF_RE`/`PATH_RE` in `setup-project.py`, as the issue says; - `update-upstream-not-a-repo` still says `--upstream`. A pin's value reaches it only when it names a path that exists, not through the pattern. **Behaviour changed, stated plainly, and what the issue's Verify means by "No refusal code changes":** the issue's Verify line reads as no code added or renamed, and none is. Two inputs do move from one existing code to another, exit 2 both before and after: - `update-shape.py --upstream "opensoft/openRepoShape<LF>"` was `unsafe-value` and is now `update-upstream-unresolvable`, still naming `--upstream`; - a pin's `source_repository:` that ends in a line feed, with no `--upstream`, was `unsafe-value` naming `--upstream` and is now `update-upstream-unresolvable` naming the pin's field. No test pinned either old answer: every gate below passed. `family.py add --member` is stripped before `MEMBER_RE`, so `add` answers as before. **Not changed, seen while measuring:** with `--upstream` passed, `update-shape.py` does not read the pin's `source_repository:` through `REPOSITORY_RE` at all, and it keeps the value as the upstream's repository name. `validate-family.py` does not hold a member row's `id:` to `PROJECT_ID_RE`; it compares the row with the member's manifest. Neither is a `$` question. Both are under "Follow-ups (not in this PR)". All in `tests/test_value_pattern_line_break.py`, run against main's production code: the five production files swapped in place from `origin/main` (`961640c68`), then put back with their sha256 matching (`dirty=5` in the run's START line is those five files). On the head `5102b4294` (30 rows) the result is **20 failed, 10 passed in 23.12s, git 2.43.0**. The 10 that pass are baselines: the seven `test_a_pattern_takes_its_value` rows, the spelling-table guard and the two double-quoted-without-a-line-feed rows. The first 19 are the first commit's rows (19 failed, 10 passed in 22.12s on its 29 rows at `038dafba`). The independent review reproduced them from main's bytes of the same five files (19 failed, 10 passed in 26.78s, the same 19 ids). The 20th, `test_every_reader_of_the_id_pattern_is_in_the_table`, is the second commit's: it fails on main because the three readers of an id from a file are `.match` there, not `.fullmatch`. - `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-QUALIFIED_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[family-DATE_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[family-QUALIFIED_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[family.py-MEMBER_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[shape_advisory-DATE_RE]` - `test_a_pattern_refuses_its_value_and_a_line_feed[update-shape-REPOSITORY_RE]` - `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]` - `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]` - `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[leg-repository]` - `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id` - `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` - `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` - `test_a_family_value_that_ends_in_a_line_break_is_a_finding[member-repository]` - `test_add_refuses_a_member_whose_id_ends_in_a_line_break` - `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` - `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[space]` - `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` - `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` - `test_every_reader_of_the_id_pattern_is_in_the_table` **13 of 13 killed.** Each mutant was applied alone to one file of the branch's production bytes (the same bytes as the first commit and as the head, which adds a test only), the rows were run, and the file was put back with its sha256 checked (`restored=yes` for all 13). Each run is `tests/test_value_pattern_line_break.py` plus `tests/test_family_member_name.py::test_every_spelling_of_the_member_name_rule_is_the_same_pattern`, git 2.43.0. | Mutant | File | Killed by | |---|---|---| | M1 `DATE_RE` back to `$` | `validate-manifest.py` | 2: `test_a_pattern_refuses_its_value_and_a_line_feed[manifest-DATE_RE]`, `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[elected_on]` | | M2 `QUALIFIED_RE` back to `$` | `validate-manifest.py` | 3: `...[manifest-QUALIFIED_RE]`, `...[leg-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M3 the id read with `.match` | `validate-manifest.py` | 2: `test_a_manifest_value_that_ends_in_a_line_break_is_a_finding[id]`, `test_adopt_check_names_a_plan_id_that_ends_in_a_line_break_as_an_id` | | M4 `DATE_RE` back to `$` | `validate-family.py` | 2: `...[family-DATE_RE]`, `test_a_family_value_that_ends_in_a_line_break_is_a_finding[created_on]` | | M5 `QUALIFIED_RE` back to `$` | `validate-family.py` | 3: `...[family-QUALIFIED_RE]`, `...[member-repository]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M6 the id read with `.match` | `validate-family.py` | 1: `test_a_family_value_that_ends_in_a_line_break_is_a_finding[id]` | | M7 `MEMBER_RE` back to `$` | `scripts/family.py` | 2: `...[family.py-MEMBER_RE]`, `test_every_spelling_of_the_member_name_rule_is_the_same_pattern` | | M8 a member's id read with `.match` | `scripts/family.py` | 1: `test_add_refuses_a_member_whose_id_ends_in_a_line_break` | | M9 `DATE_RE` back to `$` | `scripts/shape_advisory.py` | 1: `...[shape_advisory-DATE_RE]` (its pattern row alone: `date.fromisoformat` refuses the line feed after the pattern, so no tool row sees it) | | M10 `REPOSITORY_RE` back to `$` | `update-shape.py` | 3: `...[update-shape-REPOSITORY_RE]`, `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]`, `test_an_upstream_passed_with_a_line_feed_is_named_as_passed` | | M11 `prepare` passes no pin | `update-shape.py` | 3: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]`, `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` | | M12 `checked_value` names `--upstream` again | `update-shape.py` | 1: `test_a_pins_source_checked_value_refuses_is_named_as_the_pins` | | M13 the pin's refusal sentence removed | `update-shape.py` | 2: `test_a_pins_source_that_is_no_owner_repo_is_named_as_the_pins[line-feed]` and `[space]` | No mutant is added for the stand-in in `tests/test_update_shape_unreadable.py`: it is a test change. The independent review re-ran M3, M8, M11 and M12, and one mutant of its own (X1: `prepare` names the pin even when `--upstream` is passed). All five were killed, and each file was put back with its sha256 checked. The other nine of the 13 are this writer's runs alone. **The second commit's guard: 6 of 6 probes caught.** Each was applied alone to one tracked file at `5102b4294`, `test_every_reader_of_the_id_pattern_is_in_the_table` was run, and the file was put back with its sha256 checked (`restored=yes` for all six). The first commit's guard, `test_every_spelling_of_a_value_pattern_is_in_the_table`, was run with each plant too: it passes every one of them, which is the gap the second guard closes. | Probe | File | The new guard | The first guard | |---|---|---|---| | P1 a new `PROJECT_ID_RE.match(value)` reader appended | `update-shape.py` | failed: `('update-shape.py', 'match'): 1` is more than the table holds | passed | | P2 a new `repo_shape.PROJECT_ID_RE.search(value)` reader appended | `scripts/shape_advisory.py` | failed | passed | | P3 `fullmatch` back to `match` | `templates/assembly-root/scripts/validate-manifest.py` | failed | passed | | P4 `fullmatch` back to `match` | `templates/family-root/scripts/validate-family.py` | failed | passed | | P5 `fullmatch` back to `match` | `scripts/family.py` | failed | passed | | P6 main's bytes of those three files together (the 20th row above) | the three | failed | not run | **The second commit `5102b4294` (dirty=0)**, Python 3.12.3, with the same stub `gh`, bogus token and no global git config: | Run | Git | Result | |---|---|---| | `tests/test_value_pattern_line_break.py` | 2.43.0 | **30 passed** in 23.00s | | `tests/test_value_pattern_line_break.py` | 2.47.1 | **30 passed** in 23.30s | | `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 61.54s | That commit changes only the test file (62 lines added, no assert that joins conditions with `and` or `or`, ASCII), so the first commit's runs below are not repeated: they hold the production files and every other test file, and those are unchanged. CI on `5102b4294`, and an independent look at the second commit, were not done when this was written. **The independent review of the first commit** (rv1166c, READY, read-only) reports these gates on `2593952c9`, all passing: the stand-in gate's 6 files (298 passed), the new file on 2.47.1 (29 passed), the five hygiene files (306 passed), 15 more test files that name a changed function, pattern or code (1611 passed, 19 skipped) and `tests/test_shape_doctor.py` (445 passed, 3 skipped). It also read CI on that head at about 20:04Z: `tests` (the full suite on 3.14), `tests-git-bash`, `package` and SonarCloud all passed. **The first commit's gates ran on `2593952c` (dirty=0)**, Python 3.12.3, with the real `gh` unreachable (a stub first on `PATH`, a bogus token) and no global git config: | Run | Git | Result | |---|---|---| | the files that held every failure and setup error of the shards below: `tests/test_family.py`, `tests/test_update_shape_pr_base.py`, `tests/test_update_shape_unreadable.py`, `tests/test_update_shape_header_comments.py`, `tests/test_update_shape_messages.py` and the new `tests/test_value_pattern_line_break.py` | 2.43.0 | **298 passed** in 343.60s | | `tests/test_value_pattern_line_break.py` | 2.47.1 | **29 passed** in 21.31s | | `tests/test_pinned_copy_text.py`, `tests/test_path_guard_hygiene.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py` | 2.43.0 | **306 passed** in 69.12s | **The two shards, on the uncommitted tree** holding the same production bytes as the first commit (the 7 files' sha256 checked before and after), git 2.43.0: the validators' test files, the `REPOSITORY_RE`/`source_repository` files and the `update-shape.py`, `shape_advisory.py` and `family.py` files, 125 in all. - shard 1 (62 files): 4 failed, 5290 passed, 13 skipped in 56 min; - shard 2 (63 files): 3 failed, 4 setup errors, 3761 passed, 29 skipped in 50 min. Every failure and setup error is in the five existing files of the first row of the table above, which all pass on the first commit: - 2 were real: `test_a_read_refused_in_prepare_closes_the_upstream[check|apply]`, the `opening(spec)` stand-in under the two-argument call. The stand-in is fixed in the first commit. - The other 5 failures and the 4 setup errors were `update-conflict` on `scripts/validate-family.py` ("not a verbatim copy ... at the pinned commit"). This is an artifact of an uncommitted tree: `tests/test_family.py`'s `upstream_clone` commits the working tree into its clone, while `family.py init` pins the holder at the real `HEAD`, so an uncommitted `templates/family-root/` file is never verbatim at its pin. On the first commit they pass. Every other file of the two shards passed on these production bytes, so the shards were not re-run. `git merge-tree --write-tree <other> 5102b42` (git 2.43.0) at 2026-10-23T12:08Z, after re-fetching main (still `961640c6`; read again at 12:10Z, just before the push) and re-listing the open PRs at 12:02Z (55 open). Main has moved from the `038dafba` that this body measured to `961640c6` by one file, `docs/release-notes/v1.2.0.md`. Each open PR that touches one of this PR's 8 paths, at its current head, plus #1126: | Against | Head | Shares | Result | |---|---|---|---| | origin/main | `961640c6` | | clean, tree `4613729516ff` | | #1098 | `dad7961a` | `scripts/family.py` | clean, tree `4d1ff9517588` | | #1117 | `10dc011c` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only, which this PR does not touch (see below) | | #1124 | `8dfa32bd` | `validate-family.py` | clean, tree `cb2ec1073646` | | #1126 | `4c1f80cf` | none (its `scripts/repo_shape.py` and tests) | clean, tree `e82c8945ac37` | | #1133 | `a7bfee1b` | `update-shape.py` | clean, tree `a710870d1ffc` | | #1144 | `89486f69` | `scripts/shape_advisory.py` | clean, tree `2d3e3e1ef43d` | | #1147 | `b75babee` | `validate-manifest.py` | clean, tree `a82ac3b47a3f` | | #1159 | `66e68f94` | `scripts/shape_advisory.py` | clean, tree `4abeb7b38665` | | #1164 | `74096eb4` | `scripts/family.py` | clean, tree `ee285e1cb8cf` | | #1172 | `5aa0f116` | `validate-family.py` | clean, tree `aa32d41e6e2c` | | #1187 | `cc8a87ee` | `update-shape.py`, `tests/test_update_shape_unreadable.py` | clean, tree `25711eb1cac4` | | #1201 | `eac177af` | `scripts/family.py` | `scripts/family.py` merges clean; conflict in `docs/handbook.html` only (see below) | - #1117 and #1201 conflict in `docs/handbook.html` alone. #1201 is stacked on #1117 (their merge-base is #1117's head `10dc011c`). Each of them against origin/main alone gives the same conflict in the same file, so it is theirs: #1117's, from its older base `6da757cb`, which #1201 inherits. - #1187 and #1201 are new since the first merge-trees. #1187 shares `tests/test_update_shape_unreadable.py` with this PR; its diff adds or removes no line that names `open_upstream`, `def opening` or `REPOSITORY_RE`, so it brings no one-argument stand-in for the two-argument call. That was read for all 55 open heads at 12:12Z; only this PR's own lines name them. - No other open PR adds or removes a call of a method of `PROJECT_ID_RE`, which the second guard counts: the lines each of the 55 open heads adds or removes outside `tests/`, `docs/` and `*.md` were read at 12:04Z. Only this PR's own name it, and #1201's one new `MEMBER_RE.fullmatch` is another pattern. So a sibling landing first does not move the guard's table. Each is a candidate for its own issue. They come from the independent review (rv1166c, READY at `2593952c9`, issuecomment-6352173084), which measured each of them on main and at the head unless the item says it did not. The review's finding numbers are in brackets. - **The naming-policy patterns [1].** `scripts/repo_shape.py` reads the families, the roles and the topic pattern of `contracts/repository-naming.yaml` with `.match` (`family["_re"].match(name)`, `family["_split_re"].match(name)`, `role["_re"].match(name)`, and `topic_pattern`, which defaults to `^xf-project-[a-z0-9-]+$`). `validate-repository-naming.py --project project.yaml` runs in the root template's `Makefile` and `validate.yml`. At the head, and on main, it exits 0 on a leg `repository: "testorg/Atlas\n"` and prints the line feed inside its table; `-spec` and `-code` the same. The project's gate as a whole still fails, because `validate-manifest.py` now refuses that manifest. - **`update-upstream-not-a-repo` with a pin [3].** With no `--upstream`, a pin whose `source_repository:` names a folder that exists and is no repository (the review measured `contracts` and `scripts/repo_shape.py`) is `REFUSED update-upstream-not-a-repo: --upstream contracts exists but is not a git repository`, exit 2, on main and here. `pin_path` is in scope at `update-shape.py:686-687`, so that refusal can name the pin as `update-upstream-unresolvable` now does. - **A pin's line feed when `--upstream` is passed [9].** `update-shape.py check --upstream <clone>` over a pin whose `source_repository:` ends in a line feed exits 0, `nothing to do`. The value is kept as `upstream.repository`, which reaches the README shape line's check, `apply`'s commit subject and the pull request words (`update-shape.py:2525`, `3017` and `5121`). The review measured `check`, not `apply`. It belongs with the previous item. - **A member row's `id:` [4].** On main, `add` of a member whose `project.yaml` reads `id: "irqs\n"` commits the row `id: "irqs\n"`, and the holder's `validate-family.py` then says `family ok`. Under mutant M8 (this branch with main's `.match` in `add`) the same row is written, and the gate still says `family ok`. By code reading, not measured end to end: `validate-family.py` compares a row's id with the member manifest's by `str` equality (`member-id-mismatch`, `validate-family.py:1608`), and `_check_member_ids_unique` does the same, so neither asks `PROJECT_ID_RE` of a row. This PR makes `add` refuse such a member, so no new holder gets such a row. - **Values that no pattern reads [8].** The review's sweep found these `manifest ok` or `family ok`, exit 0, with a trailing line feed, at the head and on main alike: - in `project.yaml`: `name`, a spec or code leg's `path`, `shape.repository` and `shape.digests.digest_algorithm`; - in `family.yaml`: `org`, the family's own `repository`, `tracking_branch`, and the shape's and each member pin's `digest_algorithm`. No `^...$` pattern reads them, so they are not this PR's. A leg's `path` is checked only by `_leg_path_findings`, which asks no pattern, and the family's `tracking_branch` is refused in `project.yaml` (`manifest-tracking-branch`, U+000A) but not in `family.yaml`. `shape.commit` and `tree_sha256` with a line feed are also `manifest ok`; that is #1126's. The first commit's message says "the three readers of an id from a file ask it with `fullmatch` (`manifest-id`, `family-id`, `member-bad-id`)". That is true of those three, but it reads as if there were no others [2]. What the review measured instead: - `adopt-project.py` still reads the id derived from `legs.assembly` (`:6780`) and the plan's `id:` (`:6791`), both `plan-bad-id`, with `.match`; - neither lets a line feed through. A plan `id: "northwind\n"` is `plan-manifest-invalid`, now in `manifest-id`'s words. A plan with no `id:` and `legs.assembly: "NorthWind\n"` is `REFUSED unsafe-value: legs.assembly is 'NorthWind\n'`, exit 2, before the derived id is asked, on main and here alike. So it is the wording alone that overstates. The second commit's `ID_READERS` lists both readers with that reason. The title's pin clause is the narrower one (the pin's name only where the value is no path and no `owner/repo` name), and two inputs change from `unsafe-value` to `update-upstream-unresolvable` (see "Behaviour changed", above). Review: rv1166 and rv1166c, lane 5's independent opus reviewers, at `2593952c` (issuecomment-6352173084); then rc1166b, lane 5's independent sonnet re-checker, which wrote none of #1166: READY at `5102b429`, H0 of this landing (issuecomment-6371408915), with no MUST or SHOULD; its NITs 1 and 2 are corrected in the body. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 5102b42 passed tests, tests-git-bash and SonarCloud; the head 5c8afef is a clean update-branch merge of main 93d2931, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 3c89065 over main 11560eb: the family_init_values, update_shape_unreadable, value_pattern_line_break, repo_hygiene, agents_md_codes, cli_reference sets, 365 passed, 19 skipped. Landed on Brett Heap's word of 2026-10-23T13:10:26Z, relayed to every lane by lane 3 (#473 issuecomment-6365697123): "merge v1.2.1 PRs once reviewed and green". Closes #1139 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
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 |



Goal
#1082:
update-shape.py applyrefuses, 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 main038dafba) 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.What was measured on main, before the change
All probes are offline.
ghis a stub that exits 97,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/null, and git is 2.43.0. The tool is main'supdate-shape.py(sha256ca47f90e, bloba7ae7505, the same at2a119f28,6da757cb,4ed7c7bfand038dafba). Scripts and logs are in lane 5's scratchv118/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.6476c170has abootstrap.pywith nountraced. Its own edit to that file is committed.apply --at 2a119f28 --accept-local scripts/bootstrap.py --branch shape/firstexits 0. That was main before the family root's siblings.py: with a token in use, the clone and the fetch that carry it run in bootstrap.py'suntraced()environment, somake siblingsandfamily.py siblingswrite the token to no trace the caller asks git for (#1055) #1075, and itssiblings.pytakes nountraced.check --at 6da757cbreadsunchanged scripts/bootstrap.pyandupstream-changed scripts/siblings.py. the family root's siblings.py: with a token in use, the clone and the fetch that carry it run in bootstrap.py'suntraced()environment, somake siblingsandfamily.py siblingswrite the token to no trace the caller asks git for (#1055) #1075 changedsiblings.pyalone, andbootstrap.pyis blob3c1060d3at both commits.apply --at 6da757cb --branch shape/secondexits 0, printscopied scripts/siblings.py, and commits.scripts/siblings.py --helpexits 1 withImportError: cannot import name 'untraced' from 'bootstrap'.apply --at 6da757cb --accept-local scripts/bootstrap.py, exits 2 withscripts/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 atf1946d04, the parent of #1023's9e154d87. Itsbootstrap.pyhas nomember_make_environment, and itssiblings.pydoes not name it. Its own edit tobootstrap.pyis committed.MAKEFLAGSas it is, so it runs its jobs together again; every other make still gets no jobserver and no-j(#987) #1023 defined the name inbootstrap.pyand added it tosiblings.py's import in one commit.git log -S member_make_environmentover the two files names9e154d87alone.check --at <main>readsboth scripts/bootstrap.py.apply --at <main>isupdate-conflict.apply --at <main> --accept-local scripts/bootstrap.pyis refused already:scripts/bootstrap.py (kept) lacks member_make_environment, untraced, which scripts/siblings.py imports from itat6da757cb, andlacks member_make_environmentat2a119f28. The holder is unchanged.MAKEFLAGSas it is, so it runs its jobs together again; every other make still gets no jobserver and no-j(#987) #1023 landed in two halves, as the family root's bootstrap.py: git's update ended by a signal fails the run by name, and B4 with a token in use is told from fetches redone without a trace, so the token reaches no file (#998, #996) #1045 and the family root's siblings.py: with a token in use, the clone and the fetch that carry it run in bootstrap.py'suntraced()environment, somake siblingsandfamily.py siblingswrite the token to no trace the caller asks git for (#1055) #1075 landeduntraced, it does reach it. T1 is a commit on main withsiblings.pyas it was atf1946d04. That is7c695d71on2a119f28, and3d5a7edaon6da757cb.apply --at T1 --accept-local scripts/bootstrap.py --branch shape/firstexits 0.check --at <main>readsunchanged scripts/bootstrap.pyandupstream-changed scripts/siblings.py.apply --at <main> --branch shape/secondexits 0, copiessiblings.py, and commits.scripts/siblings.py --helpthen exits 1 withImportError: cannot import name 'member_make_environment' from 'bootstrap'.2a119f28and6da757cb.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/).4ed7c7bf. Its own edit tosiblings.pyaddsfrom bootstrap import told_past, which the upstream'ssiblings.pydoes not import, and is committed.siblings.py --helpstill exits 0.apply --at U1 --accept-local scripts/siblings.py --branch shape/firstexits 0. U1 is main plus an unrelated change tovalidate-family.py.told_pastinbootstrap.pyalone.check --at U2readsupstream-changed scripts/bootstrap.pyandunchanged scripts/siblings.py.apply --at U2 --branch shape/second, with no--accept-local, exits 0, printscopied scripts/bootstrap.py, and commits.siblings.py --helpthen exits 1 withImportError: cannot import name 'told_past' from 'bootstrap'.--accept-local scripts/siblings.py, was already refused by update-shape.pyapply --accept-local: a kept.pycopy that imports a name the run's newrepo_shape.pydropped passes, andmake bootstrapthen dies on an ImportError #604 withscripts/bootstrap.py (written) no longer defines told_past, which scripts/siblings.py (kept) imports from it.What changed
update-shape.py,_refuse_kept_copies_missing_names(#536, #604):.pycopy is asked against its module as this run leaves it, whoever kept it. The reading is_copies_as_left,_written_copies_lackingand_names_as_left. The module may be:--accept-local: as before, the line says(kept);unchangedrow 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);upstream-changedrow or an--addfile),already-at-target, orunchangedwith the target's bytes. The line says(the upstream's). No shipped copy meets this today: the reader rows oftests/test_update_shape_kept_helper.pyfind every name each shipped importer takes defined beside it. So it refuses only a target whose own copies disagree.apply --accept-local: a kept.pycopy that imports a name the run's newrepo_shape.pydropped passes, andmake bootstrapthen dies on an ImportError #604 asked only a copy this run's--accept-localkeeps against a module this run writes (_kept_copies_importing_dropped). The rows_copies_as_leftreads askept by an earlier runare now passed beside the kept ones, under a new side,WRITTEN_UNDER_EARLIER.scripts/bootstrap.py (written) no longer defines told_past, which scripts/siblings.py (kept by an earlier run) imports from it.KEPT_IMPORTERSmaps each(written)side to its importer's word, so a(kept)importer's line is unchanged._names_droppedstill 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.unchangedcopy holding the upstream's own bytes is still not asked as an importer: that pair is two of the target's own copies.if not kept: returnis gone. A run with no--accept-localis asked too. The guard still stands afterupdate-conflict,update-local-driftandupdate-rewrite-symlink, and beforeconfirmand any write.--accept-localmakes.(kept)or(written)-under-(kept)refusal prints byte for byte what main prints. In fix round 1, with the head53791e45:cmpof the masked one-step logs gives 1312 bytes for the family root's siblings.py: with a token in use, the clone and the fetch that carry it run in bootstrap.py'suntraced()environment, somake siblingsandfamily.py siblingswrite the token to no trace the caller asks git for (#1055) #1075's(kept)refusal and 1331 bytes for thetold_past(written)refusal, both identical to main's tool. A update-shape.pyapply --accept-local: a kept.pycopy that imports a name the run's newrepo_shape.pydropped passes, andmake bootstrapthen dies on an ImportError #604 row now pins that opening (SHOULD 2).(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.checkreads itunchanged, 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 definesline 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 owngit showline names that copy), commit, and run again. All four exits are tested end to end, and each checks the statecheckthen reads:locally-modifiedafter the first,already-at-targetafter the second.run this again with an --at whose copies agree.KEPT_LEAD, the code and every(kept)line are as main has them.tests/test_update_shape_kept_helper.py:earlier_standard, models the family root's bootstrap.py and siblings.py: under GNU make 4.4's named-pipe jobserver, a member's make that says it is 4.4 or later is handed the holder'sMAKEFLAGSas it is, so it runs its jobs together again; every other make still gets no jobserver and no-j(#987) #1023 in two halves in the suite's own clone, from A's bytes, with no historical commit:before-1023: the holder'sbootstrap.pyspellsmember_make_environmentby the family root's bootstrap.py and siblings.py: a member's make inherits no jobserver from the holder'smake -jN, somake -j2 bootstrap,validate,parkandresumeno longer warnjobserver unavailableof a rule the person does not own (#888) #950'smember_make_env, and itssiblings.pydefines its own;bootstrap-1023: A'sbootstrap.pyback;siblings-no-name: the control;kept-no-nametodropping_standard, a branch offkept-basethat changesvalidate-family.pyand adds or takes away no name. A holder cut atkept-basekeeps its ownbootstrap.py, which importsDROPPED_NAMEfromrepo_shape.py, through an earlier apply there (kept_by_an_earlier_apply, shared withkept_earlier);droppedis the next apply's target. This is rv1133'stold_pastscenario in the file's own fixtures.refusal()gainsleft=andat_target=, andassert_stops_on_the_importgainsscript=.assert_the_kept_copy_starts_as_beforeandassert_the_exit_appliesare shared by the--accept-localrows and the earlier-kept rows.andoror(205 asserts, checked on the syntax tree).untraced()environment, somake siblingsandfamily.py siblingswrite the token to no trace the caller asks git for (#1055) #1075's docstring line (six names) is kept as main has it.Fix round 1, after rv1133's review
git merge origin/main(038dafba) was clean: merge commit7aee5efa, tree27a2785e, the same asgit merge-tree --write-tree aa7ddf59 038dafba. The fix is one new commit on it,53791e45. Nothing was amended, rebased or forced.test_an_earlier_kept_copy_is_asked_when_the_next_apply_drops_its_name: end to end,checkreads the keptbootstrap.pyunchangedandrepo_shape.pyupstream-changed; the plain apply todroppedis 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 keptbootstrap.pybesidedropped'srepo_shape.pystops on the import.test_an_earlier_kept_copy_importing_what_this_run_keeps_applies(2 controls): a namerepo_shape.pylacked before too, and one the writtenrepo_shape.pystill 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 statecheckreads after it; the apply then writesrepo_shape.py, andbootstrap.pystarts.test_an_importer_an_earlier_run_kept_is_asked_against_a_written_module(2, in process): the line, the opening, the exits, and theagainwording alone and beside a copy this run keeps.test_an_importer_holding_the_targets_bytes_is_not_askedreplacestest_an_importer_this_run_leaves_alone_is_not_asked, whose second param pinned the limit.limit-h/): the second apply exits 2 withscripts/bootstrap.py (written) no longer defines told_past, which scripts/siblings.py (kept by an earlier run) imports from it, the holder is unchanged, andsiblings.py --helpexits 0. The one-step, first-apply andchecklogs match main's tool once paths and shas are masked. the family root's siblings.py: with a token in use, the clone and the fetch that carry it run in bootstrap.py'suntraced()environment, somake siblingsandfamily.py siblingswrite the token to no trace the caller asks git for (#1055) #1075's probe (probe-h1075/) is refused at its second apply too, with(kept by an earlier run) lacks untraced.test_keeping_a_copy_that_imports_a_name_the_run_drops_is_refused, a update-shape.pyapply --accept-local: a kept.pycopy that imports a name the run's newrepo_shape.pydropped passes, andmake bootstrapthen dies on an ImportError #604 end-to-end(written)row, now assertsstartswith(KEPT_LEAD). rv1133's R2 is killed (below).test_either_exit_the_remedy_names_appliesand the new one) check thatcheckreadslocally-modifiedafter the change exit andalready-at-targetafter the drop exit. The first exit's id now says the edit was changed by hand, as the remedy'schangeddoes.Rows that fail on main
The new test file (185 rows) was run against main's
update-shape.py(038dafba, sha256ca47f90e, in a scratch clone): 17 failed, 168 passed (base-main.log). Againstaa7ddf59's: 13 failed, 172 passed (base-aa7.log). With the change: 185 passed.aa7ddf59test_an_earlier_kept_copy_is_asked_when_the_next_apply_writes_its_importerassert 0 == 2: the second apply copiessiblings.pytest_with_branch_an_earlier_kept_copy_is_refused_before_any_branchassert 0 == 2test_either_exit_the_remedy_names_applies(2 params)assert 0 == 2: the apply the remedy follows is not refusedtest_a_written_copy_is_asked_against_its_module_as_this_run_leaves_it(4 params)test_each_kind_of_module_is_given_its_own_exit(2 params)merged, notchangedtest_every_kind_of_line_in_one_run_is_one_refusal(2 params)(kept)linetest_an_earlier_kept_copy_is_asked_when_the_next_apply_drops_its_nameassert 0 == 2: the plain apply copiesrepo_shape.pytest_either_exit_for_an_earlier_kept_importer_applies(2 params)assert 0 == 2: the apply the remedy follows is not refusedtest_an_importer_an_earlier_run_kept_is_asked_against_a_written_module(2 params)(kept)lineThese 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 pinsKEPT_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), andtest_an_importer_holding_the_targets_bytes_is_not_asked.Mutants
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):_kept_copies_importing_dropped({}), asaa7ddf59had it(kept)in its line(written)-under-(kept)sideKEPT_LEADopening kept for a(written)line under a copy an earlier run keptunchangedwhy and the--accept-local on each copy an earlier run keptexit given to an earlier-kept module onlygit show(written)-under-(kept)line onlyunchangedrow asked as an importer, the target's own bytes tootest_an_importer_holding_the_targets_bytes_is_not_askeda copy this run writes or keeps imports ...)(written)-only refusal given the general opening (sides <= {KEPT_MODULE})test_keeping_a_copy_that_imports_a_name_the_run_drops_is_refused(SHOULD 2); it survived ataa7ddf59keptset restored_write_copiesNo 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):keptset restored (left = dict.fromkeys(kept, KEPT_MODULE))unchangedrows asked as importers (against the modules this run writes) but the written importers asked against the oldkeptset only_write_copies(refused after a write)copied(kept)--accept-local on each copy an earlier run keptexit droppedunchangedsentence droppedRuns
All runs went through lane 5's
run9.sh, with the stubgh,GIT_SSH_COMMAND=false,GIT_CONFIG_GLOBAL=/dev/nullandEMAILexported. No more than two ran at once. The full suite was not run.Fix round 1, at
53791e45(eachSTARTline records the head anddirty=0; logs inv120/f1133/):tests/test_update_shape_kept_helper.pyown243.logown247.logtests/test_update_shape.py,tests/test_update_shape_messages.py,tests/test_update_shape_paste_lines.pyrunC.logtests/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.pyrunD.logtests/test_update_shape_add_not_a_directory.pyrunF.logtests/test_path_guard_hygiene.pyand the trio (tests/test_repo_hygiene.py,tests/test_agents_md_codes.py,tests/test_cli_reference.py)runE.logNo failure and no skip in any run at this head. The two runs of the new file went through
run9.shon 2026-10-22 at 13:31Z, the next three on 2026-10-22 from 15:16Z to 15:21Z, andrunF.logat 15:23Z; eachSTARTline readshead=53791e45...anddirty=0. Of the 19tests/test_update_shape*.pyfiles, these 9 ran at this head. The other 10 did not run at this head: all 10 ran at the first round's head7b447602in shard A or B (below), over the first round'supdate-shape.py, which this round's commit changed. The full suite was not run.The first round, at
7b447602(treee0e2d19e, the same tree asaa7ddf59): 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.Merges
Read at
53791e45, after a re-fetch of main (origin/mainis still038dafba, the second parent of the branch's merge commit7aee5efa):git merge-tree --write-tree origin/main HEADexits 0 and writes treebfa8c1d4, which isHEAD's own tree: the branch holds038dafbaas an ancestor, so the merge adds nothing.7aee5efa(parentsaa7ddf59and038dafba) has tree27a2785e, the same tree asgit merge-tree --write-tree aa7ddf59 038dafba, which exits 0. It was clean.gh pr list --state open --limit 100 --json number,headRefName,headRefOid,files. None but this one touchesupdate-shape.pyortests/test_update_shape_kept_helper.py, so there is no other head to merge against. The list isopen-prs-d1.jsoninv120/f1133/.git diff --name-only origin/main HEADnames those two files and no other.NIT round, after rc1133's re-check
rc1133 re-checked
53791e45(#1133 issuecomment-6347273216): READY, with three NITs. This round takes all three, in one new commit,a7bfee1b, on53791e45. It was pushed as a fast-forward of the branch; nothing was amended, rebased or forced. The commit changestests/test_update_shape_kept_helper.pyalone (+7, -4).update-shape.pyis53791e45's, sha2565b386f8b, so the fix and every measurement in the sections above are unchanged._missing_name_refusalopens withKEPT_LEADwhere every line is one this run's--accept-localmakes. A run with a(kept) lacksline and a(written) no longer definesline is such a run, buttest_both_directions_in_one_run_are_one_refusalpinned 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 assertsresult.stderr.startswith(KEPT_LEAD), result.stderr, right afterassert_refused_writing_nothing, and its docstring says why the opening is that one.id:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #1166. Its row is in "Merges ata7bfee1b" below, and it replaces the Merges section's "Open pull requests" line.Mutant O1, before and after
Applied alone in this worktree and restored by checkout, sha256
5b386f8bchecked, status clean afterwards. The mutant is rc1133's (mutate_rc.py, copied unchanged).53791e45's, without the new asserto1-before-row.log). rc1133 measured the whole file: 185 passeda7bfee1b's, with the new asserto1-after-file.log). The failure istest_both_directions_in_one_run_are_one_refusalat the new assert: the opening reada 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/main961640c6, sha256ca47f90e) swapped into the worktree, gives 1 passed (main-row.log); the file was restored to5b386f8bafterwards. 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
a7bfee1bAll through lane 5's
run9.sh, eachSTARTline athead=a7bfee1b...anddirty=0, with the stubghandEMAILexported; two ran at once, and the full suite was not run. No failure and no skip. Logs are in lane 5's scratchv124/n1133/.tests/test_update_shape_kept_helper.pyandtests/test_path_guard_hygiene.pyown243.logtests/test_update_shape_kept_helper.pyown247.logThe 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
andoror(206 asserts, checked on the syntax tree), no non-ASCII byte, and its widest line is 79 characters.Merges at
a7bfee1bRead after a re-fetch of main:
origin/mainis961640c6(#1155, which addsdocs/release-notes/v1.2.0.mdand nothing else). It touches neither of this PR's files.git merge-tree --write-tree origin/main HEADexits 0 and writes tree98765a8b. The branch does not hold961640c6, and the predicted merge differs fromHEADbydocs/release-notes/v1.2.0.mdalone.id:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #1166 (fix/1139-anchored-patterns, head2593952c95, opened 2026-10-22T17:31:57Z) touchesupdate-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 a7bfee1b 2593952c9519c4b96de295657635fc711f75c695exits 0 and writes tree67858a0b. Against53791e45the same command writes37498506, rc1133's tree, which is reproduced. rc1133's tests on the predicted merge of53791e45and validate-manifest.py, validate-family.py, family.py and update-shape.py: anid:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #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 ata7bfee1b, which changes one assert and a docstring in a test file validate-manifest.py, validate-family.py, family.py and update-shape.py: anid:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #1166 does not touch.gh pr list --state open --limit 100 --json number,headRefName,headRefOid,files. Besides this one, only validate-manifest.py, validate-family.py, family.py and update-shape.py: anid:, a date or arepository:ending in a line feed is refused by name, and a pin'ssource_repository:that is no path orowner/reponame is named as the pin's (#1139) #1166 touchesupdate-shape.py, and none but this one touchestests/test_update_shape_kept_helper.py, so no other head needs a merge-tree. The list isopen-prs.json.Closes #1082
Claim: #1082 (comment)
Lane: openreposhape-5 (openRepoShape-5)
🤖 Generated with Claude Code
Summary by Sourcery
Prevent update-shape applies from creating unresolved Python imports when local copies preserved by earlier runs conflict with files written by the current run.
New Features:
Bug Fixes:
Enhancements:
Tests: