Repository navigation
adopt-project.py: check's rehearsal and _stored_as_written take a parent's -c settings, attr.tree too, and the source's objects, GIT_ATTR_SOURCE is refused by name before any question, and each throwaway reads its environment first (#1136) - #1169
Conversation
…` of the source in the work directory, and `_resolved` and `_answers` make their repositories as #910's throwaways are made (#1083) Work in progress for #1083, stopped at a usage checkpoint. Built: `_split_clone_asked` (asked first in `_entry_findings`; a failure is `adopt-git-unasked`, which `execute` refuses before any leg); `_resolved` and `_answers` made with `_throwaway`/`_throwaway_made` in the work directory, no hooks, `--template=<empty>`, `-b`, the source's format, under `_inherited_env()`; `_throwaway_git`; `_submodule_findings` skips the url questions once one throwaway could not be asked. 16 rows added or changed in tests/test_adopt_check_throwaway_repos.py (45 rows: all pass on this head; 14 fail and 31 pass over 2a119f2's adopt-project.py). Left: the TMPDIR docstring in tests/test_adopt_reencoded_files.py, the gate, and the PR body. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e_checkout_writes_is_never_written_through` says the throwaway repository is made in the work directory, as #910 makes it, and no longer in `TMPDIR` (#1083) The last item of #1083's (c): the docstring still said the throwaway repository the link is asked in is in `TMPDIR`. Since #910 it is made in the work directory, which is as deep below `tmp_path` as the `TMPDIR` the row sets, so the row asks the same question and only its words change. (The issue's `adopt-project.py:466` names `_pin_row_problem` at 74c1770; this docstring is the one PR #1031's body named.) Gate on the merged tree (main 6da757c): see the pull request body. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… parent git hands it, as execute's does, and no variable naming another repository; _resolved and _answers read their environment before their throwaway (#1083) r1119's fix round on #1119. SHOULD-2: `_split_clone_asked` makes the clone in `_split_clone_env()`: `_inherited_env()` plus `GIT_CONFIG_PARAMETERS` where it is set. Measured on bdcf995 with git 2.43, `GIT_CONFIG_PARAMETERS="'init.defaultbranch'='a..b'"` gave `plan ok` from `check`, and `execute` made both legs and stopped extracting the spec leg. Now `check` says `adopt-git-unasked` and `execute` refuses before any leg, as for the setting in a config file or in `GIT_CONFIG_COUNT`. A parent's `-c core.hooksPath` runs no hook: the clone's own `-c` comes after it. SHOULD-1: a row runs `check` under `GIT_OBJECT_DIRECTORY` naming the source's own objects and holds the source's object directory unchanged. It kills M6: in the whole environment the clone wrote `objects/info/alternates` into the source. A row under `GIT_DIR` naming the source, whose own config rewrites the on-disk url, kills M14 (`_answers` in the whole environment) and holds the source's config unchanged. NIT-1: the docstring says git ignores `--shared` for a shallow source, so that clone is a full copy, a cost `execute`'s own clones pay too. NIT-2: the unmade-work-directory NOTE says it asked nothing about the urls of the submodules the plan keeps either. NIT-3: `_resolved`, `_answers` and the clone read their environment before they make their throwaway, so a raise there leaves none behind. tests/test_adopt_check_throwaway_repos.py: 63 rows (45 before), 18 new. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….fsmonitor=false`, and the rows r1119b named pass on main: the clone-env row appends to the environment's count, and the ref-probe row fails only the probe's `git init` r1119b's second review of #1119, after the merge of main 038dafb (#1091, #1111): - MUST-1: `test_the_clone_takes_a_parent_gits_settings_and_no_other_repository` built its `GIT_CONFIG_COUNT` from `{}`, which `test_no_test_sets_a_git_config_count_of_its_own` refuses. It now appends to `os.environ`, and asserts the last entry kept and no `attr.tree`. - MUST-2: main's `test_check_says_where_git_would_not_make_the_ref_probe` set `GIT_DEFAULT_HASH=bogus`, which now stops the split's own clone first (`adopt-git-unasked`, exit 1). A stub `git` first on PATH fails only `git init` of a `refs-probe-*` directory, with git's words; skipped on Windows. - SHOULD-A: `_throwaway_made` returns `-c core.fsmonitor=false` beside `core.hooksPath`, as the rehearsal and `_stored_as_written` give theirs. A counting fsmonitor program ran 8 times in `_resolved`'s two repositories in one `check`; now 0, in the global config and in `GIT_CONFIG_COUNT`. New rows: that one and its control. - NIT-A: a row with `--work-dir` inside the source, whose config rewrites the url (kills R8). - NIT-B: a row with that rewrite handed down by a parent git in `GIT_CONFIG_PARAMETERS`, which `_answers` leaves out (kills A18). - NIT-C: `_split_clone_asked`'s docstring says a shallow source makes the clone run the `uploadpack.packObjectsHook` of protected config, as `execute`'s clones do. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inside the source also rewrites the url to a network, so a `--get-url` asked around the throwaway, which names it in a NOTE as not asked, fails it f1119c's mutant R8a, which moved only `_answers`' `git ls-remote --get-url` out of its throwaway repository into the throwaway's own directory, survived the row that kills r1119b's R8: the expansion there only decides between the disk and a network, and the url itself was still asked in the throwaway. A source config that rewrites the on-disk url to `https://f1119c.invalid/Thing` makes R8a print the NOTE of a url on a network it did not ask; on this head `check` prints none, inside the source or outside it. Now killed by the row's `a-network` case. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r system config as where the `uploadpack.packObjectsHook` a shallow source's clone runs comes from, since a parent's `-c` did not reach it cea2553's NIT-C sentence said "protected config ... or a `-c`". f1119c measured it on git 2.43, with a shallow source and a counting hook: - in the global config, 1 run in `check` and 1 in a clone made as `_extract_leg` makes it; - in the source's own config, 0 and 0; - in `GIT_CONFIG_PARAMETERS`, as a parent `git -c` hands it down, 0 and 0. So the `-c` is dropped. The commit message of cea2553 says "of protected config" for the same sentence; this is the measured form. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd _stored_as_written, refuses GIT_ATTR_SOURCE by name, and reads each throwaway's environment first (#1136), no tests yet Work in progress for #1136, stacked on #1119 (fix/1083-check-asks-the-split-clone). Holds the tool change for all three items: - _split_checkout_env(): _inherited_env() plus a parent's GIT_CONFIG_PARAMETERS less any attr.tree (_parent_settings, _parameter_entries, _single_quoted, read as git's parse_config_env_list reads it); taken by _SplitRehearsal and _stored_as_written. - _attr_source_unasked(): any GIT_ATTR_SOURCE in the environment is adopt-git-unasked, by name, before the split's clone and every other throwaway question, so execute refuses it before either leg. - _AttributesProbe, _stored_as_written and _SplitRehearsal read their environment before _throwaway (_probe_env now takes it as an argument). Measured on git 2.43 with the scratch harness, base bba168b vs this tool (check / execute): the parent protectHFS case and the parent clean-filter case went from plan ok / stop after both legs to plan-path-git-refuses and plan-attributes-reencoded, refused before any leg; every GIT_ATTR_SOURCE value went to adopt-git-unasked / refused before any leg; controls unchanged. NOT YET: no new test rows, no mutants, no gate run, no pr-body. No test has been run on this commit. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ows (45, passing on the head tool); the older GIT_ATTR_SOURCE rows in two other files are not updated yet The new `# #1136` section covers a parent's `core.protectHFS` and failing clean filter in both forms (check and execute), a parent's `attr.tree` left out of the rehearsal, a parent's hooks and fsmonitor running nothing, `GIT_ATTR_SOURCE` as one `adopt-git-unasked` (seven values) and execute's refusal before any leg (three values), `_parent_settings` against git's own `--list`, and three more environment-read rows. They passed on 3b308d3's tool: 45 passed in 52 s. Not yet done: the GIT_ATTR_SOURCE params of `test_no_tree_this_machine_names_steers_the_question` (two files) and `..._steers_the_split_question` still expect a finding; base runs, mutants and the gate have not run. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t by name before any question; the `attr.tree` rows stay, and each docstring points to `test_git_attr_source_is_one_finding_that_names_it` `test_adopt_nested_attributes.py`'s two `test_no_tree_this_machine_names_*` functions and `test_adopt_reencoded_files.py`'s one each set `GIT_ATTR_SOURCE` to `HEAD` and to the empty tree and expected the finding their question gives. The head tool answers each with one `adopt-git-unasked` first, so all six failed there (measured at b3b48b3: 6 failed, each on `FINDING adopt-git-unasked`). Both values are held by `test_adopt_check_throwaway_repos.py`'s `test_git_attr_source_is_one_finding_that_names_it`. Run after the change: test_adopt_nested_attributes.py 75 passed, test_adopt_reencoded_files.py 51 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideImplementationMake the rehearsal and stored-as-written checks mirror the split’s checkout environment, borrow the source’s objects, refuse
TestsUpdate regression coverage for inherited settings, attribute trees, early
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-22T21:25:15Z) Independent review r1169 of #1169 at
Lane 1 rules on SHOULD-1 and runs the fix round in a later window. PR #1169 review by r1169: verdict NOT READYI stopped early on lane 1's STOP. No run of mine is still live: every run was a foreground call and all of them finished. The mutant clone's The verdict rests on SHOULD-1. If lane 1 rules that gap a known limit, and the body and the row (c) docstring are corrected, I would expect READY on the parts I covered. What I measured that matches the PR body
FindingsSHOULD-1: dropping Where:
Fix: at minimum, add a measured Known-limits bullet. Then correct the body's "Why NIT-1: nothing pins "before any question". Mutant M6 moves NIT-2: no row separates entries with a tab, LF or CR. Mutant M10 ( NIT-3: the
Fix: give NIT-4: an NIT-5: the #1145 overlap line is stale. It names NIT-6, by reading only: a branch in Mutants11 were run, in
Not covered
Logs, probe outputs and scripts are in Lane: openreposhape-1 (openRepoShape-1) |
…ehearsal and `_stored_as_written` (r1169's SHOULD-1, fix (b)), and `_Unasked` takes an ending of its own for `GIT_ATTR_SOURCE` (NIT-3); tests not updated yet `_split_checkout_env()` is now this process's environment but for what `_NOT_INHERITED` names other than `GIT_CONFIG_PARAMETERS`: a parent's `-c` settings, in `GIT_CONFIG_PARAMETERS` and `GIT_CONFIG_COUNT`, are taken as they stand, `attr.tree` among them. `_parent_settings`, `_parameter_entries`, `_single_quoted` and `_PARAMETER_SPACE` go, since nothing is dropped any more. `_stored_as_written` borrows the source's objects through `_objects_borrowed`, which the rehearsal now uses too, so an `attr.tree` object id names there the tree it names in the split's clones (measured with git 2.43: without alternates a full tree id read no rule and an abbreviated one was ignored; with them both read the tree's rules; `HEAD`, a branch name and `bogus` were ignored either way). UNFINISHED, by lane 1's LAST STEP at 45%: no test of this commit has run. Expected to fail until the tests are updated: test_adopt_check_throwaway_repos.py's (g) rows (no `_parent_settings`), row (c), the (f) stored-as-written and split-rehearsal params (they patch `_inherited_env`, which `_split_checkout_env` no longer calls), the `GIT_ATTR_SOURCE` rows' expected ending, and the count-form empty-tree rows in test_adopt_reencoded_files.py and test_adopt_nested_attributes.py. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eads it, and new rows hold `execute` verifying under it (p8) and a tree the source holds naming a failing filter for the root (p2, p7) in every form (r1169's SHOULD-1) Row (c) is renamed `test_a_parent_gits_attr_tree_is_read_in_the_rehearsal_as_the_split_reads_it` and gains the global-config form: under the empty tree `check` says `plan ok` in each form, and with no setting it finds the CRLF file. `test_execute_reads_a_parent_gits_attr_tree_as_check_does` runs a real `execute` under the same settings. `side_tree_handed` builds the p2/p7 source with a neutral root (`*.png binary`), so the rehearsal's own refresh reads no failing rule. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…since `_parent_settings` is gone under fix (b), and `test_the_checkout_questions_take_the_settings_and_no_attr_source` asserts both forms whole, `attr.tree` kept `handed_by_a_parent`, `appended_by_hand`, `listed`, `READABLE`, `UNREADABLE` and their two rows are removed, and the unused `command_line_entries` import with them. The variables naming another repository are one constant, `ANOTHER_REPOSITORY`, read by the clone's row and the checkout's. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as_written` and the rehearsal read it now, before each makes its throwaway Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OURCE_ENDS`, r1169's NIT-3), and a new row holds that `execute` refuses it before the split's own clone is asked (NIT-1) `ATTR_SOURCE_WHY` now ends at "can answer for them"; `ATTR_SOURCE_ENDS` replaces `UNASKED_ENDS` in `unasked_attr_source`, and the execute row asserts the refusal ends with it. `test_git_attr_source_is_refused_before_the_splits_own_clone_is_asked` records `_split_clone_asked` and finds it never called. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `plan ok` under the empty tree, as the split's own git reads no rule there and `execute` verifies, each with its no-setting control; `SITES` follows the reencoded row's new name `test_adopt_reencoded_files.py`'s row is now `test_a_tree_this_machine_names_steers_the_question_as_it_steers_the_split`, and `test_adopt_nested_attributes.py`'s split-question row is now `test_a_tree_this_machine_names_steers_the_split_question_as_the_split`. Measured with git 2.43 on the head tool: under the count-form empty tree each source is `plan ok`, and a real `execute` says `adoption verified`; with no setting each is found and `execute` refuses before any leg. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reads_it`, so its `def` line fits 79 columns; the reencoded row's docstring is rewrapped, and the `ATTR_SOURCES` comment names the older rows as they are called now No behaviour changes. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check's rehearsal and _stored_as_written take a parent git's -c settings but attr.tree, a GIT_ATTR_SOURCE is refused by name before any question, and each throwaway reads its environment before it is made (#1136)check's rehearsal and _stored_as_written take a parent's -c settings, attr.tree too, and the source's objects, GIT_ATTR_SOURCE is refused by name before any question, and each throwaway reads its environment first (#1136)
…fig, step 1 of 3 to restack #1169 onto main after #1119's squash One conflict, in the module docstring of tests/test_adopt_check_throwaway_repos.py: #1119 added its #1033 paragraph and #1136 added its own at the same place. Both stay, #1033 first. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… step 2 of 3 to restack #1169 onto main after #1119's squash Two conflicts, adopt-project.py and tests/test_adopt_check_throwaway_repos.py, both #1119's files; both took this branch's side with git checkout --ours. git diff 9b85644 b7edd88 is empty over the whole tree, so the merge changes no file: its tree is the tree of the commit before it. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-23T20:43:13Z) r1169d, lane 1's independent reviewer (not the writer), finished r1169c's review of #1169 at
The full reviewr1169d: review of PR #1169 at
|
| Mutant | Change | Ids | Result |
|---|---|---|---|
| K1 (M1) | --cached at both calls |
throwaway file, -x |
killed: (k) [global-config] |
| K2 (M2) | --cached at the AS_HELD call |
same | killed: (k) [global-config] |
| K3 (M3) | --cached at the AS_SPLIT call |
129 + 126 | survives, not equivalent (SHOULD-1); the row kills it, 3 of 3 |
| K4 | --cached where GIT_CONFIG_PARAMETERS names attr.tree |
throwaway file, -x |
killed: (k) [git-config-parameters] |
| K5 | --cached where a GIT_CONFIG_KEY_<n> is attr.tree |
same | killed: (k) [git-config-count] |
| K6 | --cached where a global config file is named |
same | killed: (k) [global-config] |
| K7 | every .gitattributes checked out first |
129 (survived) + 126 | killed by 2 rows |
| K8 | --cached where attr.tree is unset |
129 + 126 | survives, equivalent |
6 of 8 are killed, which is 6 of the 7 non-equivalent ones, and 7 of 7 with SHOULD-1's row. Every run restored the tool, with restored to HEAD's blob: True after each mutant.
Not covered
- CPython 3.14 and a 3.14-like shim: no reviewer built one. The body reports the writer's shim run of the 67 ids, and the changed line has no path check.
- The full suite.
- The body's Gf2, G3, G9 and H2 gate calls: no reviewer ran them at
76c9b767. They ran at1e958ca6, whose tool has the same AST. - f1169's 13 older mutants, at this head.
- A real
executefor the both-rules-in-the-tree case, and for the followed copies advice: onlycheckwas run. - The earlier rounds' rows (a) to (i), beyond G2's 129 passing.
- The restack onto
main, and the merge with adopt-project.py:checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180'sa0e8cded. Lane 1 checks those.
The evidence is in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1169c/: probe-out/, logs/, expA.sh and expB.sh. My probes, including the proposed row as test_r1169d_probes.py, are in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1169d/: out/ and logs/ (status.txt, nr-K3/K7/K8.log, pr-*.log).
Lane: openreposhape-1 (openRepoShape-1)
Lane: openreposhape-1 (openRepoShape-1)
… `attr.tree`, which names `-filter` for the tree's rule and not `-ident` for the root's, so `--cached` on the `AS_SPLIT` call of `_rules_reaching` (r1169d's SHOULD-1, M3) fails all 3 ids Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-23T20:53:09Z) The fix round for r1169d's review is pushed at
Lane: openreposhape-1 (openRepoShape-1) |
|
(lane openRepoShape-1, 2026-10-23T21:34:31Z) r1169e, lane 1's independent re-checker (not the writer), re-checked #1169's fix round at
The full re-checkr1169e: re-check of PR #1169's fix round at
|
| Tool | Result |
|---|---|
25805102 |
3 failed, all -ident at :2048 |
bba168bd (blob b17f7a41) |
global form failed at :2048 with -ident; parameters and count failed at :2043 with assert 0 == 1 (plan ok) |
The block equals r1169d's. git diff -U0 fe25bb14 d9224ecf adds 44 lines, 40 of them non-blank. They are identical to the python block in r1169d-on-1169.md: review non-blank: 40 added non-blank: 40, equal (non-blank, exact): True.
Placement and names. The row sits after row (k), at lines 2008-2051. COPY_UPPER, COPY_UPPER_CLEAN, OWN_COPY_RULE, TREE_COPY_RULE and COPIES each have one definition, and no other test file uses them.
Whole file at head. tests/test_adopt_check_throwaway_repos.py: 151 passed, 1 skipped in 198.60s. The skip is :482, "needs a disk that ignores case". It is adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119's row and not this PR's.
Id count. Collecting the file at main 50ce8ab1 gives 90 ids and at head 152. Head has 62 ids not in main, and none removed. That matches the body's "59 ... row (l) makes 62".
2. SHOULD-2 and NIT-1: each of the five body items, and the NIT-1 sentence, is in and says what r1169d measured.
The "Known limits" bullet is now titled "A misnamed second finding wherever both rules are read from one place." (the old title was "where both rules are the source's own").
| r1169d's item | In the body | Matches |
|---|---|---|
| 1. Retitle the bullet | The title above | yes |
| 2. Side-tree results | "in each of the three forms, head gives exit=1 with plan-attributes-reencoded and also plan-file-reencoded naming specs/001-feature/spec.md; 25805102 gives plan-attributes-reencoded alone; bba168bd gives exit=0, plan ok", with the *.md text-alone control as plan ok |
yes |
| 3. The traced cause | The refresh's exit code is never read; it exits 128 with fatal: .gitattributes: clean filter 'w1136' failed; diff-files exits 0 and lists .gitattributes specs/spec.md; with no filter the refresh exits 0 and diff-files lists nothing |
yes (see below) |
| 4. "Comes with X1" | "This case comes with ruling X1: the read without --cached now reaches spec.md through the tree's *.md text ... where base said plan ok" |
yes |
| 5. Same change in "Not in this PR" | "... wherever both rules are read from one place: the source's own root, or the side tree attr.tree names, which this PR's read without --cached brings in" |
yes |
- Item 3: one phrase goes past r1169d's wording, and it holds. The body says the files are "the files it did not refresh". I reproduced r1169c's expB in the probe environment, with the same rows, the same filter,
update-index -q --refreshagainst a fresh index, thendiff-files.== failing: refresh fatal: .gitattributes: clean filter 'w1136' failed refresh exit=128 .gitattributes ctime: 0:0 specs/spec.md ctime: 0:0 diff-files exit=0 out: .gitattributes specs/spec.md == none: refresh refresh exit=0 .gitattributes ctime: 1792790432:171663157 specs/spec.md ctime: 1792790432:171663157 diff-files exit=0 out:ctime: 0:0onspecs/spec.mdafter the failed refresh shows it was never refreshed, so the phrase holds. - Item 4 and the own-rules sentences. The body keeps "The own-rules case predates this PR, which only lets its parameters form reach it". r1169d agreed with this for the own-rules case. The body also corrects the adopt-project.py: under a failing clean filter that a rule names for
.gitattributes,checkalso printsplan-file-reencodedfor a file the split stores as its blob, beside the rightplan-attributes-reencoded(#1136) #1192 claim to the two-places case (row (k)), as r1169d asked. - adopt-project.py: under a failing clean filter that a rule names for
.gitattributes,checkalso printsplan-file-reencodedfor a file the split stores as its blob, beside the rightplan-attributes-reencoded(#1136) #1192. Lane 1's comment with the trace is there, posted 2026-10-23T20:44:08Z. - The NIT-1 sentence. It is in the "The findings point at the source's own rules" bullet and matches r1169d's measurement. I measured it independently with a probe module that imports the file's helpers:
"25805102 loops the same way, with the generic line" is r1169d's, and I did not re-measure it. "Row (l) checks only which attribute is named" is true of the row's code:
attr.tree, own root `*.png binary` (no five lines): exit=1 plan-file-reencoded, the 5 `-filter` lines (3 forms) attr.tree, own root `*.png binary` + the five `-filter` lines: exit=1 plan-file-reencoded, the SAME 5 lines (global, parameters, count) no attr.tree, own root `/scripts/*.py filter` + the five lines: exit=0 plan ok no attr.tree, own root `/scripts/*.py filter` alone: exit=1 plan-file-reencoded, the same 5 linesstore 5 pinned shape copies,-filterper copy, no-ident.
3. The mutant paragraph
All mutants ran in clones of d9224ecf, with the file restored in a finally and its blob checked against HEAD's after each.
| Mutant | Ids | Result | Claim |
|---|---|---|---|
| K3 | row (l), 3 | 3 failed (item 1) | killed by (l): holds |
| K7 | test_adopt_nested_attributes.py and test_adopt_reencoded_files.py (126) |
2 failed, 124 passed in 188.50s |
killed by the two named rows: holds |
| K8 | the throwaway file | 151 passed, 1 skipped |
equivalent: holds |
| K8 | nested and reencoded files | 126 passed in 188.68s |
equivalent: holds |
| K1 | (j) and (k), 17 | 15 failed, 2 passed |
body's "M1: 15 failed": holds |
| K2 | (j) and (k), 17 | 9 failed, 8 passed |
body's "M2: 9 failed": holds |
Head over the same 17 ids: 17 passed.
- K7's two failures are exactly the two rows the body names:
test_adopt_nested_attributes.py::test_an_encoding_the_root_attributes_is_not_in_is_found:assert set() == {'plan-file-reencoded'}.test_adopt_reencoded_files.py::test_every_file_is_read_again_whatever_the_clock:.gitattributes ctime: 0:0,assert 1 > 1.
- K7 and K8 are r1169d's mutants. My tool blobs equal r1169d's
status.txt: K762b23082..., K8e1f78c0a.... - K8's reason holds in the code.
_NOT_INHERITEDnamesGIT_ATTR_SOURCEat 11972-11973._split_checkout_env()returns only names that areCONFIG_PARAMETERSor not in_NOT_INHERITED._attr_source_unaskedraises_Unaskedbefore any throwaway whenever the variable is set.- The work tree holds no
.gitattributeswhen_rules_reachingasks, which is the precondition K7 breaks.
- The tally "6 of 8 killed before row (l), K3 by (l), K8 equivalent: 7 of 7 non-equivalent killed" follows from the table.
4. Nothing else changed.
git diff fe25bb14 d9224ecf:1 file changed, 44 insertions(+),tests/test_adopt_check_throwaway_repos.py,44 0, one commit.adopt-project.py's blobs.So the tool atd9224ecf a530fdfacc9be36995080ba0cdae051698ab751e fe25bb14 a530fdfacc9be36995080ba0cdae051698ab751e 76c9b767 a530fdfacc9be36995080ba0cdae051698ab751e 50ce8ab1 b17f7a41b2d74d3b31d1c9ff1c7b8f8ecbeb15b7 bba168bd b17f7a41b2d74d3b31d1c9ff1c7b8f8ecbeb15b7d9224ecfis byte-equal to76c9b767's, and main's tool is byte-equal tobba168bd's.- The two diffs you named. I compared them by changed lines (the
+and-lines, headers left out).git diff -U0 50ce8ab1 d9224ecf -- adopt-project.py 268 lines sha256 b9d14b4f43f6...ddf git diff -U0 bba168bd 76c9b767 -- adopt-project.py 268 lines sha256 b9d14b4f43f6...ddf numstat: 211 57 and 211 57 - The whole tree. The changed lines of
50ce8ab1..fe25bb14and ofbba168bd..76c9b767are both 985 lines with sha2565c59dd53e8c0...5c81. That is the value lane 1 recorded.git diff --numstat 50ce8ab1 d9224ecflists the same 5 files, with 674/5 in the throwaway file (630/5 plus row (l)'s 44). - The restack statement.
fe25bb14has parents93a561c9and50ce8ab1.93a561c9has parentsce1938a8andb7edd881.ce1938a8has parents76c9b767and9b856443.gh pr view 1119gives MERGED with merge commitb7edd881, and9b856443as its final head.git diff 9b856443 b7edd881andgit diff ce1938a8 93a561c9are both empty (0 lines).- The module docstring has adopt-project.py:
refused_paths'update-index -z --index-infois untested withcore.ignorecaseand case twins (aandA) on a case-insensitive disk #1033's paragraph at line 47 and adopt-project.py: #910's throwaways leave out a parent's -c settings and GIT_ATTR_SOURCE, so check says plan ok where execute stops after both legs, and three read the environment after making the throwaway (follow-up of #1083) #1136's at line 55. - PR base is
main, and the PR is a draft on milestone v1.2.1. origin/mainhas moved one commit since toe04826db, which touchestemplates/family-root/scripts/validate-family.pyandtests/test_validate_family_index_gitmodules.py, neither of which the PR touches.git merge-tree --write-tree origin/main d9224ecfexits 0 with treeaa2a2d7c, no conflict.
5. Exactly one closing form. Holds.
3:Closes #1136
closingIssuesReferences: 1136
- A grep for close/fix/resolve forms before an issue reference finds only line 3.
- "fix (b)" at line 42 and the branch name
fix/1145-...at line 136 are not closing forms. - The body has no "Part of" or "Covers".
New findings
NIT-1: the NIT-1 control omits "then the five lines". The body's wording is ambiguous.
- Where: the body's bullet "The findings point at the source's own rules", its sentence "with the same rule in the own root and no
attr.tree,plan ok". It sits in the restack-era body, where r1169d's text sits. - Evidence (the probe above): the rule in the own root with no
attr.treeisplan okonly when the five-filterlines follow it. With the rule alone and noattr.tree, the copies finding comes (exit=1, the same five lines). r1169d's control was "the same rule in the own root, then the five lines". - Fix: write "with the same rule in the own root, followed by the five lines, and no
attr.tree,plan ok".
NIT-2: the mutant paragraph names the restack's fe25bb14 but not row (l)'s head for M3.
- Where: the body's M3 bullet. It says M3 was measured "at
fe25bb14plus the row". - Evidence: I measured at
d9224ecf, andadopt-project.py's blob is the same,a530fdfa, at both commits. So the claim is true. - Fix: none needed. Optionally say "at
d9224ecf".
What I did not cover
- Not measured at
d9224ecf.- The doctor's four shards, G3, G4, G9 and the hygiene gates. The tool and the other four test files are byte-equal to what lane 1's and w1169e's calls ran, and row (l) adds only tests.
- R1's other three test files: they are unchanged since
fe25bb14, where R1 ran. - The doc trio, which this round does not touch.
- Not re-measured. The older body claims from earlier rounds, which I did not touch. These include f1169's 13 older mutants and the own-rules both-rules table: the
bba168bdglobal and count forms giving two findings, and the parameters formplan ok. A realexecuterun of the side-tree both-rules case and of the copies advice: onlycheckwas run. - No CPython 3.14. I used the 3.12 shim for row (l) only. The changed lines have no path check.
- The full suite.
Evidence is in /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1169e/:
logs/:mut-K3-x.log,run-K7-nr.out,run-K8-nr.out,run-K8-thr.out,head-thr.log,swap-p25805102.log,swap-pbba168bd.logprobe/:out.txt,test_r1169e_probes.pyexpB/run-2/ids-main.txt,ids-head.txtmutate.py,swap_tool.py
Lane: openreposhape-1 (openRepoShape-1)
Lane: openreposhape-1 (openRepoShape-1)
|
|
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 23 hours by commenting |
Merges main at 11560eb into fix/1161-check-names-case-twins at 58aa9d2 (merge-base ef619c7, #1158's squash). One conflict, tests/test_adopt_check_throwaway_repos.py, in the module docstring: #1189 and #1169 each added a paragraph after #1033's. Both are kept, #1161's first, because it opens "so `check` passed twins", which follows from #1033's measurement, then #1169's "#1136 followed". Every row and helper of both sides auto-merged untouched. adopt-project.py auto-merged: both sides change `_entry_findings` (#1189 its case twins after the source path findings; main the `GIT_ATTR_SOURCE` and split-clone questions before `_paths_git_refuses`, and `_submodule_findings`' `unasked`) and the `check` paragraph of the module docstring, in separate lines. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `--local-remote-dir` path in `execute`'s `bare`, leg and `split` lines, are named with `_shown`, as #1154 names `--out` and `--work-dir`, so a bidi control, an escape, a line feed or a byte that is not UTF-8 in either no longer reaches the terminal as it was typed (#1152) (#1177) ## What #1154 (#1115) names `--out` and `--work-dir` in `plan`'s and `check`'s report with `_shown`. #1152 lists the two groups of paths it leaves raw: the `--plan` path in `check` and `execute`, and the `--local-remote-dir` path in `execute`. A bidi control (U+202E), an ESC or a line break in either reached the terminal as it was typed, where the other plan lines name a path with `_shown`. This change spells each of the 21 sites with `_shown(str(...))`, in `adopt-project.py` only. A path with a character that is not printable is named as `_spelled` names it, between backticks. Every other path prints byte for byte as it did, and no refusal code is new. The `--plan` path, 16 sites: - the five `N finding(s) in <plan>` lines: `_refuse_a_mistyped_plan`, `_refuse_what_check_finds`, the `plan-split-branch-taken` refusal, and `cmd_check`'s two branches; - the `<plan>: ...` refusals: `plan-wrong-kind`, `plan-unsupported-mode`, `plan-unreadable`, `plan-no-source`, the three `plan-bad-id` ones, `plan-not-the-source`, `adopt-topic-too-long`, and the `legs.assembly` and `org:` ones. The `--local-remote-dir` path, 5 sites: - the `<bare> already exists` refusal; - the ` bare <bare>` line; - the two leg lines, in `_extract_leg` and `_seed_leg`, which end `-> <url>`; - the `split <commit> on <branch> -> <url>` line. It names the source's directory under `--local-remote-dir`. The `NEXT:` clone command is not touched. It already quotes the url with `shlex.quote`, and a row pins that it still does. ## Behaviour change A path that is not UTF-8 now prints `\udcff` between backticks, as #1154's paths do, where it printed raw before. A plain path prints as it did. ## Names #1152 calls the wrong-kind refusal `plan-bad-kind`. The code's name for it is `plan-wrong-kind`, and that is the code this change spells. No refusal was renamed. ## Not in this PR `repo_shape.load_yaml`'s own refusals, `yaml-unreadable`, still print a path raw. They live in `scripts/repo_shape.py`, and they are a separate follow-up: see #1168. ## Proof `tests/test_adopt_plan_and_remote_paths_spelled.py` is new, ASCII only and POSIX only, with every non-ASCII character written as an escape. - **Rows: 148.** - 135 plan rows: 14 refusal families (27 family and command pairs) by 5 path forms (U+202E, ESC, a line feed, the byte `\xff` through `surrogateescape`, and a plain control). - 5 `--local-remote-dir` rows for the `bare` and leg lines (the code leg is extracted, the spec leg seeded). - 3 `split` rows. `execute` refuses a source path that holds U+202E, ESC, a line feed or `\xff` as `adopt-unwritable-value` before any leg exists, so these rows use U+200B in the parent directory and a name that ends in a space, and a plain control. - 5 in-process rows for `{bare} already exists`, which call `_create_leg_remote`. - **On main's `adopt-project.py`: 118 failed, 30 passed.** Each failure is the raw path where the spelled one was expected. The 30 that pass are exactly the 30 plain controls. - **On this head: 148 passed.** - **Mutants: 23, all killed.** 21 put one spelling back to raw, one at a time, and 2 swap `_shown` for `_spelled` at one site. Each was restored with its sha256 checked. - **Gate:** a scratch merge of this head with main `961640c68` (tree `61063bb76`, which is the tree of this PR merged into that main). Targeted files only: the new file, `test_stdout_encoding.py`, `test_non_utf8_name_callers.py`, `test_adopt_plan_escapes.py`, `test_path_guard_hygiene.py`, `test_repo_hygiene.py`, `test_scaffold_leg_path_collisions.py`, and every test file that greps for `finding(s) in` or `already exists` (47 files), in 6 shards, and `test_shape_doctor.py` (448 ids) in 8 shards. Every shard exited 0, with 0 failed. | Shard | Result | | --- | --- | | g0 | 617 passed | | g1 | 344 passed | | g2 | 439 passed | | g3 | 605 passed | | g4 | 902 passed | | g5 | 764 passed, 1 skipped | | d0 | 56 passed | | d1 | 56 passed | | d2 | 56 passed | | d3 | 55 passed, 1 skipped | | d4 | 55 passed, 1 skipped | | d5 | 55 passed, 1 skipped | | d6 | 56 passed | | d7 | 56 passed | That is 3671 passed and 1 skipped in the file shards, and 445 passed and 3 skipped in the doctor shards. The skips are Windows-only rows. - **Merge-trees** (`git merge-tree --write-tree`, this head against each), all clean: - `origin/main` `961640c68`; - #1154's head `8a782733`: clean, so this PR is based on main and not stacked on it; - the other 15 open PRs that touch `adopt-project.py`, at their heads: #1171 `159815d75`, #1170 `4df784ac4`, #1169 `b8e63bcf0`, #1153 `ff33ba12a`, #1143 `7af5fabc3`, #1141 `7a2711d03`, #1125 `9549006fe`, #1123 `5c3908f8d`, #1122 `c01c4d73d`, #1121 `1c5173758`, #1120 `c55e55e33`, #1119 `bba168bdd`, #1118 `2307036ab`, #1106 `504a1dd65`, #1105 `b28f40de0`. The other open PRs do not touch `adopt-project.py`. None of the 16 that do adds a line that names `plan.path`, `{bare}`, `finding(s) in` or `already exists`, apart from a tuple in #1121 that is not printed. Closes #1152 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…repository as `plan-case-twins` on every disk, a one-blob set as a NOTE in a leg and, in the root, two paths in one folder, and `execute` refuses the finding before any leg (#1161) (#1189) `adopt-project.py check` printed `plan ok` for a plan that keeps case twins in one repository: source paths that differ only in case (`A` and `a`, `D/x` and `d/x`), or a path that names in another case a folder above another (`F/x` and `f`). The plan is written on one disk and the clone that loses a twin is made on another, and a clone on a macOS or Windows disk checks out one path of each set. On a disk that ignores case, `execute` could then stop with `check` having said `plan ok`: as `adopt-lost` or at `git rm` mounting the legs, after both legs were pushed, or at git's `will not add file alias`, before any leg existed. - `check` now exits 1 and names `plan-case-twins`, one finding per repository the split makes (the root, the spec leg, the code leg). A set is every source path that one answered entry sends into one repository, whose paths differ only in case, or where one path names in another case a folder above another. It is asked on every disk, since `core.ignorecase` where `check` runs says nothing about the reader's disk. `execute` runs the same checks first, so it refuses the finding as `plan-case-twins` before it makes either leg. - A set whose paths all fold to one name and hold one blob in one mode is a NOTE, and `check` still says `plan ok` for it. `execute` prints no NOTE and does not refuse it. - In a leg that holds for every such set, folder twins (`src/D/x`, `src/d/x`) and sets of three or more included. - In the root the NOTE takes only two paths whose names differ only in their last part, `A` and `a` or `D/A` and `D/a`. Folder twins of one blob there (`D/x`, `d/x`), `D/A` beside `d/a` and sets of three or more (`AB`, `Ab`, `ab`) stay in the finding, since on the FUSE disk `execute` met git's `will not add file alias` for them. - The remediation says to rename all but one twin of each set in the source, commit, and re-run `plan`. It no longer offers to answer the twins into different repositories, because on a disk that ignores case `execute` loses those too (#1183). - Paths git refuses (`plan-path-git-refuses` names them), `drop` and unanswered entries are left out. At most 10 paths are named per repository, and the rest are counted. - Behaviour change: a source that keeps `Makefile` and `makefile` side by side in the root is now `plan-case-twins`. Before, `check` printed `plan ok`, and on a disk that keeps case `execute` split it and verified it. Two params of `tests/test_adopt_make_reads_first.py` changed, and `test_execute_refuses_makefile_beside_makefile_before_any_leg` asserts the refusal: no bare repository, no assembly or leg clone in the work directory, no split branch in the source. - Measured with git 2.43, on the bench disk (keeps case) and on a FUSE disk that ignores case and keeps it (`casefold` lookups), against base `4c7b8acd`: - On base, `check` printed `plan ok` for every scenario on the bench disk. On the FUSE disk `execute` stopped on three of them: `F/x` and `f` in the root, exit 1, `adopt-lost: F/x`, both legs and the split already pushed; `src/A`, `src/a`, `src/D/x`, `src/d/x` with own blobs in the code leg, exit 2 in 3 of 5 runs at `git rm -r -q -- src` after both legs were pushed; and `D/x` and `d/x` in the root with own blobs, refused before any leg as `plan-file-reencoded`. - On head, `check` names `plan-case-twins` for each of those three (then `plan-file-reencoded` where git refuses the alias). - The NOTE sets verified in `execute` on the FUSE disk: `A` and `a` in the root (3 of 3 runs), `D/A` and `D/a` in the root (2 of 2), `src/A` and `src/a` in the code leg (3 of 3), `specs/A` and `specs/a` in the spec leg (1 run). - In the root, folder twins and three paths of one blob (2 runs each, at `a131fc25`): `check` printed `plan-case-twins` and `plan-file-reencoded`, and `execute` refused both before any leg as `plan-case-twins`. - The whole of `tests/test_adopt_check_throwaway_repos.py`, CPython 3.12: at the head `58aa9d2b`, 91 passed and 2 skipped; on base 38 of its ids fail (r1189c). Mutants of the new code: 36 of 36 killed, each file restored and its blob checked against HEAD's. - Known limits: - `casefold` is full case folding, so `ß` and `ss` are named as a set, though a disk may hold them apart. - Unicode normalization is not asked: a precomposed `é` beside `e` and a combining accent is no finding, though a macOS disk takes them as one name. - The cut at 10 named paths can fall inside a set. - The one-blob NOTE, and the root's narrower rule for it, rest on the FUSE disk with git 2.43; real APFS and NTFS were not measured. The row for a disk that ignores case skips on every CI disk, which keeps case. - Not in this PR, each a follow-up: case twins the plan sends to different repositories, or a twin it drops beside one it keeps (#1183); the rehearsal's alias refusal, which `check` reports as `plan-file-reencoded` though its words say the bytes were re-encoded (#1184); twins a kept path makes with a file the shape writes into the root (#1191). Review: lane 1's independent reviewers reviewed each round. r1189 found `8684af86` NOT READY, for two MUSTs and four SHOULDs (#1189 issuecomment-6356435838). r1189b found `4d40ce9f` NOT READY for one SHOULD, that the one-blob NOTE covered root shapes that do not verify on a disk that ignores case (issuecomment-6364543347). The next round narrowed the root's rule and the sentence, and r1189c, lane 1's independent re-checker, found `58aa9d2b` READY, with no MUST, no SHOULD and two notes that do not block (issuecomment-6375603307). It re-ran 15 of the 36 mutants, and all 15 were killed. After #1158 landed as `ef619c74`, the branch was restacked onto it by merges of #1158's final head and of its squash, and its changed lines equal the reviewed delta (sha256 `04b2909a`). After #1169 and #1223 landed, main `11560eb9` was merged in as `5c4fcd9d`; its one conflict, a docstring both sides added to, keeps both, and the changed lines equal the reviewed delta (issuecomment-6722938949). `7d2f651e` leaves out of lane 4's #1121 rows the two trees that keep case twins, which `execute` now refuses, with lane 4's agreement (issuecomment-6722980082). After #1105 landed, main `12122952` was merged in cleanly as `47478d51`, whose 787 changed lines equal the delta before it (sha256 `637968e3`), and `472944b5` makes the cut agreed on #473 (issuecomment-6723000456) of the three #1105 rows that keep case twins. Lane 1's check carries READY to `472944b5` (issuecomment-6724542222). Tests, under a stub `gh` with a bogus token: at `58aa9d2b`, r1189c's re-check gave 91 passed and 2 skipped for `tests/test_adopt_check_throwaway_repos.py` (the same under a 3.14-like shim) and 33 passed for `tests/test_adopt_make_reads_first.py`. Main touched both files after the restack, so lane 1's gate ran on the refreshed tree itself, below: the PR's two test files, `tests/test_scaffold_leg_path_collisions.py`, the doc trio, the path-guard and no-auto-maintenance scans, and `tests/test_shape_doctor.py` in 4 shards. At `472944b5`: `tests/test_adopt_check_throwaway_repos.py` 194 passed and 2 skipped (the disk that ignores case), `tests/test_adopt_make_reads_first.py` with `tests/test_adopt_next_make_reads_first.py` 71 passed, `tests/test_adopt_collision_make_reads.py` 28 passed, `tests/test_adopt_single_repository_record.py` with `tests/test_scaffold_leg_path_collisions.py` 143 passed, the two scans 179 passed and 2 skipped, the doc trio 171 passed, and `tests/test_shape_doctor.py` in 4 shards 446 passed and 3 skipped. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its H0 `472944b5` is green on Linux and SonarCloud. The branch was never rebased. After H0, the only commit is update-branch's clean merge of main, giving dae7250, which is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 9 pytest calls over `tests/test_adopt_check_throwaway_repos.py`, `tests/test_adopt_make_reads_first.py`, `tests/test_adopt_next_make_reads_first.py`, `tests/test_adopt_collision_make_reads.py`, `tests/test_adopt_single_repository_record.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_repo_hygiene.py`, `tests/test_path_guard_hygiene.py`, `tests/test_no_auto_maintenance.py` and `tests/test_shape_doctor.py` in 4 shards: 1,233 passed, 7 skipped. Closes #1161 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…: a kept `GNUmakefile/` is a directory GNU make stops at, `python3 scripts/bootstrap.py` names `python` on Windows, and the template says what to run where `make` reads another file (#1207) **#1132, the parts that hold on main: the assembly-root template says what to run where GNU make reads a kept `GNUmakefile` or `makefile` first; a kept DIRECTORY of either name is named as one GNU make stops at, in the `CONTRACTS_DIR` line and in `execute`'s NEXT; every sentence that names `python3 scripts/bootstrap.py` for a person to run names `python` on Windows too; and two follow-up texts no longer say more than is true (RV1105b's NIT-2 and NIT-3a).** This PR was built on main `961640c6`. Lane 4's #1121 (#1047) landed as `0ed3e57f8` at 21:37Z, and this round merged it in (`6fa1edd56`): NEXT now words a kept file (#1121's) and a kept directory (this PR's) through one lookup and one clause. #1105 landed as `12122952b` on 2026-11-08, and this PR merges it in (see "Main merge after #1105"). The items that live only in #1105's code, and the parts of #1121's that this merge does not do, are in their own issue, #1240 (see "Left"). Item 5 is #1206. Lane 4's #1105 (#1048) landed as `12122952b` at 16:45Z. This round merges main into the branch with `git merge origin/main`, never a rebase, and does it twice: - main `46fe6ac19` as merge `7b0192bde`; - then NIT-A in its own commit, `e5e077940`; - then main `9729315f1` as merge `01e93b6d1`, the head. Lane 1's #1123 had landed meanwhile, and one of its rows meets this PR's directory line (see "The second merge, after #1123"). The first merge conflicted in `scripts/shape_materialize.py` only: 3 hunks, all in `collision_follow_up`, as "Landing order" said. `adopt-project.py` and `templates/assembly-root/AGENTS-shape.md` merged with no conflict. Both sides' behaviour stays: #1105's second `GNU_MAKE_READS`, its `_predict_collisions` lookup and its collision line, and this PR's `_make_reads_first`, `_make_reads_first_clause` and NEXT. None of #1240's unification is done here (one tuple, one lookup). ### The three hunks | hunk | this PR (`6fa1edd56`) | main (`46fe6ac19`, #1105) | resolution | |---|---|---|---| | 1. The docstring's NIT-4 sentence | "This said "both callers say it", and `execute` calls it nowhere" | "`Materialized.follow_ups()` would say this of them, and nothing calls it" | **Main's.** Both sides rewrote the same sentence for the same finding. Main's also names `Materialized.follow_ups()`, which #1240's item G changes. So this PR no longer touches that sentence, and the last `scripts/shape_materialize.py` bullet under "What changed" is #1105's text now. | | 2. The docstring's Makefile paragraphs | the `BOOTSTRAP_COMMAND` sentence and NIT-2's tail | the heading "WHERE `make` READS THE MERGED FILE", and the "BUT GNU MAKE READS" paragraph | **Both.** Main's heading and both paragraphs. This PR's sentences on `BOOTSTRAP_COMMAND` and on NIT-2's tail go in the first paragraph, and one new sentence goes in the second: "Both name that command as `BOOTSTRAP_COMMAND` does, `python` on Windows as well (#1132)." | | 3. The returns | two `return`s, each one sentence | `arrive`, `joint`, `meanwhile` and `until` set per branch, then one pair of `return`s | **#1105's structure, with a per-branch `then` in place of `until`.** With no `reads_first`, the line is this PR's, byte for byte. With one, it is #1105's, byte for byte, except that the command a person runs is `BOOTSTRAP_COMMAND`, both in `meanwhile` and in "and then ... runs it". | - **Hunks 1 and 2 carry no code.** In the merged file they sit at lines 1022-1023 and 1032-1070, inside `collision_follow_up`'s docstring (lines 1014-1084). - **The rendered lines.** I loaded the function from `6fa1edd56`, from main, and from the merge, and rendered each `reads_first` (none, `GNUmakefile`, `makefile`) with and without `scripts/bootstrap.py` aside. - With no `reads_first`, the merge equals `6fa1edd56` (2 of 2). - With one, it equals main with " (`python` on Windows)" after `python3 scripts/bootstrap.py` in those two places (4 of 4). For a root that keeps `Makefile` and `GNUmakefile`, the line now ends "...; until then `python3 scripts/bootstrap.py` (`python` on Windows) is what the shape's `make bootstrap` runs". - **NIT-2's tail where `make` reads another file** stays #1105's own: "neither `make bootstrap` nor `python3 scripts/bootstrap.py` runs the shape's bootstrap until then, and then `python3 scripts/bootstrap.py` (`python` on Windows) runs it". #1105's review fixed that branch for the same finding, so it needs nothing more. - **Of #1240's list,** this round does item C for #1105's collision line, as #1240 asks a resolution to say. So the "with `python3`" part of "Left"'s second bullet is done. Item B, the kept directory, stays: #1105's lookup asks `_root_keeps`, so a root that keeps `Makefile` and `GNUmakefile/` is told GNU make "reads the source's `GNUmakefile`". Mutant U3 below shows that no row pins this line for a directory. ### #1105's test pins `tests/test_adopt_collision_make_reads.py` is #1105's file, and it pins the texts this PR changes, as "Landing order" expected. - **Four pins move.** `MERGE_IS_ENOUGH` and `BOTH_MERGES` (no `reads_first`) move to this PR's words. `MEANWHILE` and `SCRIPT_MERGED` (with one) move to `python` on Windows. All four go through a new `COMMAND`. - **One comment changes.** The comment on the `Makefile+scripts` row now says what NIT-2's tail says. - **Measured:** before the move, the six adopt and pin files gave 11 failed and 242 passed, and the 11 are exactly the rows that use those four pins. After the move, 253 passed. - **On main's code:** main `46fe6ac19`'s four production files (`adopt-project.py`, `scripts/shape_materialize.py`, `templates/assembly-root/AGENTS-shape.md` and `README.md`) were swapped into the mutant worktree at `e5e077940`. 64 of the three files' 106 rows fail: - in this file, exactly those 11 rows, and the other 20 pass; - in the new file, 34 of 35, as before (NIT-4's row passes); - in `tests/test_adopt_next_make_reads_first.py`, 19 of 40, as before. The four files were then restored, each matched the head by sha256 (4 of 4), and the tree was clean. ### NIT-A `tests/test_adopt_next_make_reads_first.py:64` called #480's NOTE "unchanged", but the merge of #1121 moved that pin to `python` on Windows. The comment now says the NOTE "names `python` on Windows too since #1132". The change is a comment only, in its own commit. ### The second merge, after #1123 Lane 1's #1118 and #1123 landed while the first merge was being gated: `9ec0dc79c` at 17:30Z and `9729315f1` at 17:47Z. Both merge with `e5e077940` with no conflict in the text. The predicted merge with `9ec0dc79c` passed (gate P below). The predicted merge with `9729315f1` failed 1 row of 254 (gate P2): - **The row:** #1123 adds `test_a_name_only_a_disk_that_ignores_case_takes_for_makefile[makefile-directory]` to `tests/test_adopt_make_reads_first.py`. For a kept DIRECTORY `makefile/`, it expects the `CONTRACTS_DIR` line a kept `makefile` FILE gets ("only once `makefile` does"). #1123's own message says that line is not true for a directory, and that the row works as a crash guard for `twins` in `_makefile_follow_up`. This PR changes exactly that line: a kept directory is one GNU make stops at. - **The fix, in the merge `01e93b6d1`:** the row now expects `SHAPES + written_as("makefile")`, as #1123 wrote it, which is the part that guards `twins`. This PR's directory clause follows, through a new `stops_at` helper beside `reads`. The comment above `ANSWERED` and the row's docstring now say so. Nothing else changes in the merge. - **Mutants on the row.** Each ran alone in the mutant worktree at `01e93b6d1` against `tests/test_adopt_make_reads_first.py` (34 rows). Each was restored with `git checkout` and matched by sha256 (`adopt-project.py` `3c2cb5f5bea5`), and the tree was clean after each. | id | mutant | rows failing | result | |---|---|---|---| | D1 | `_make_reads_first` drops a directory's `/`, so the line is the one #1123's row pinned | 1 of 34, the row | killed | | D2 | #1123's R10: `twins` reads the entry with its slash | 1 of 34, the row, with `IndexError` | killed | ### Mutants on the resolution H3a-H3g and U1-U3b are the first merge's. All ran in the mutant worktree at `e5e077940`. - **Method:** each mutant was applied alone, then restored with `git checkout`. Each restore matched by sha256 (`shape_materialize.py` `b18f09d7c396`, `adopt-project.py` `2055a81acdc6`), and the tree was clean after each. - **Rows:** H3a-H3g, U3 and U3b ran against 93 rows: `tests/test_adopt_collision_make_reads.py` (31), the new file (35) and `tests/test_adopt_e2e.py` (27). U1 ran against the new file, `tests/test_adopt_make_reads_first.py` (33) and `tests/test_adopt_collision_make_reads.py`, 99 rows. U2 ran against the new file and `tests/test_adopt_next_make_reads_first.py` (40), 75 rows. - **Unmutated,** every row passes (gate A below). | id | mutant | rows failing | result | |---|---|---|---| | H3a | hunk 3, no `reads_first`: `meanwhile` back to main's plain `python3` | 4 of 93 | killed | | H3b | hunk 3, no `reads_first`: the script tail back to main's "before both merges are made" | 4 of 93 | killed | | H3c | hunk 3, with `reads_first`: `meanwhile` without `BOOTSTRAP_COMMAND` (main's) | 5 of 93 | killed | | H3d | hunk 3, with `reads_first`: "and then ... runs it" without `BOOTSTRAP_COMMAND` (main's) | 3 of 93 | killed | | H3e | hunk 3, with `reads_first`: this PR's tail in place of #1105's | 3 of 93 | killed | | H3f | hunk 3: `reads_first` ignored, so the resolution is this PR's side whole | 15 of 93 | killed | | H3g | hunk 3, with `reads_first`: #1105's `joint` ("; and") lost | 3 of 93 | killed | | U1 | `_makefile_follow_up` drops `_make_reads_first` (`first = None`) | 31 of 99 | killed | | U2 | `_next_steps` drops `_make_reads_first` (`first = None`) | 35 of 75 | killed | | U3 | `_predict_collisions` asks `_make_reads_first` in place of #1105's lookup | 0 of 93 | **survived** | | U3b | `_predict_collisions` passes no `reads_first`, so #1105's lookup is lost in the merge | 15 of 93 | killed | - **H3c and H3d** are killed only by rows that use the moved `MEANWHILE` and `SCRIPT_MERGED`. Those pins are what hold `BOOTSTRAP_COMMAND` in #1105's branch. - **U3 survives,** and that is #1240's item B. For a kept file, both lookups give the same name. They differ only for a kept directory, where `_make_reads_first` adds the `/`, and no row runs `_predict_collisions` on a root that keeps `Makefile` and `GNUmakefile/`. A row now would pin the line #1240 is to correct, so this round adds none. ### Runs Each run went through the lane's runner, with git-filter-repo 2.47.0 (CI's pin) on PATH, on a commit with a clean tree. | run | git | files | passed | failed | skipped | |---|---|---|---|---|---| | gate A2 | 2.43.0 | `01e93b6d1`, the head: the 12 files below | 1096 | 0 | 3: the PowerShell rows (`test_shape_doctor.py:10516`), which run only on a Windows host | | gate B2 | 2.47.1 | `01e93b6d1`: the same 12 files | 1096 | 0 | 3: the same | | gate A | 2.43.0 | `e5e077940`, after the first merge: the 12 files below | 1095 | 0 | 3: the same | | gate B | 2.47.1 | `e5e077940`: the same 12 files | 1095 | 0 | 3: the same | | gate P | 2.43.0 | the predicted merge of `e5e077940` with main `9ec0dc79c` (#1118; tree `ba3f3c1f3`, a local commit object, not pushed): the six adopt and pin files, plus #1118's `tests/test_adopt_root_shape_paths.py` and `tests/test_adopt_symlink_target_bytes.py` | 509 | 0 | 0 | | gate P2 | 2.43.0 | the predicted merge of `e5e077940` with main `9729315f1` (#1123; tree `0eb290ae2`, a local commit object): the six adopt and pin files | 253 | 1: #1123's `makefile-directory` row, which the second merge moves | 0 | | gate P4 | 2.43.0 | the predicted merge of `01e93b6d1` with main `801a7d6d0` (#1106; tree `25bc27d39`, a local commit object): the six adopt and pin files, plus #1106's `tests/test_adopt_one_byte_locale.py` and `tests/test_adopt_unenterable_source_and_locale.py` | 277 | 0 | 0 | The files are the six adopt and pin files (`tests/test_adopt_collision_make_reads.py`, `tests/test_pinned_copy_text.py`, the new file, `tests/test_adopt_next_make_reads_first.py`, `tests/test_adopt_e2e.py` and `tests/test_adopt_make_reads_first.py`, 253 rows), plus `tests/test_shape_doctor.py` and `tests/test_scaffold_leg_path_collisions.py`, which adopt changes need, plus the hygiene five (`test_path_guard_hygiene.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py` and `test_pinned_copy_text.py`). That is 1098 rows in one run at `e5e077940`, and 1099 at `01e93b6d1`, where #1123 adds one row. All of this was measured on main `961640c68` with GNU make 4.3, offline. | item | on main | |---|---| | 1, the template | `AGENTS-shape.md` (the "Work from here" paragraph and the five-commands table) and `README.md` (Get started, and the parking steps) give `make bootstrap` with no caveat. With the template's own `Makefile` beside a plain `GNUmakefile` or `makefile`, `make -n bootstrap` exits 2, `No rule to make target 'bootstrap'`. | | 2, a kept directory | With `GNUmakefile/` or `makefile/` beside `Makefile`, `make bootstrap` exits 2 with `make: *** GNUmakefile: Is a directory. Stop.`, or the same with `makefile`. It does this for every target, and for each directory alone too. Main's `CONTRACTS_DIR` line says "`make` there exports `CONTRACTS_DIR` only once `GNUmakefile` does". NEXT prints `make bootstrap` for a root that keeps only `GNUmakefile/`. Beside a kept `Makefile`, it prints #480's NOTE that the targets "arrive only once the follow-up merges the two". Rendered by the lane's `probe_texts.py` on main. | | 3, `python3` | The template's `scripts/bootstrap.py` says `python3 scripts/bootstrap.py (python on Windows)`. #480's NOTE and the Makefile collision follow-up's "until then" name `python3` alone. | | NIT-2 | The no-`reads_first` script-aside tail says "neither `make bootstrap` nor `python3 scripts/bootstrap.py` runs the shape's bootstrap before both merges are made". In a root that keeps its own `Makefile` and `scripts/bootstrap.py`, merging the script alone makes `python3 scripts/bootstrap.py` print the shape's `SHAPE-BOOT`, while `make bootstrap` still exits 2. Measured again by a row below. | | NIT-3a | `_predict_collisions` says the wording comes from "the same function `execute` reports its actual collisions through", and `collision_follow_up` says "Both callers say it". `execute` never calls `collision_follow_up`: it carries the plan's stored `follow_ups:` into the split message. | `adopt-project.py`: - **`_make_reads_first(surviving)`** gives the first of `GNUmakefile` and `makefile` the root keeps. It reads `GNU_MAKE_READS`' order, asks the way `_root_keeps` asks, and adds a `/` for a DIRECTORY. **`_make_reads_first_clause`** words the clause. For a file, the words are main's, byte for byte. For a directory, it says: "GNU make at the root stops at the source's directory `GNUmakefile/`, the first of `GNUmakefile`, `makefile` and `Makefile` it finds (`GNUmakefile: Is a directory.`), so ... only once the root keeps no directory of that name". - **`_makefile_follow_up`** (the `CONTRACTS_DIR` line) asks both. A kept file's line is unchanged: `tests/test_adopt_make_reads_first.py` passes as it is. - **`_next_steps`** takes `surviving=`, the plan's root paths, which `cmd_execute` passes as `paths_for["root"]`, as #1121 does. - Where a kept DIRECTORY stops make, the command is `python3 scripts/bootstrap.py`. The NOTE says GNU make stops there. Beside a kept `Makefile`, it also says the targets run there once the follow-up merges the two, and only once the directory is gone. Where the script went aside too, alone or with the `Makefile`, the NOTE names that command after the merges. - Since the merge of #1121, a kept FILE goes through the same lookup and the same clause. For a file the clause gives #1121's words byte for byte, and its NOTEs end in `BOOTSTRAP_COMMAND` too. - **`BOOTSTRAP_COMMAND`** is in every NOTE: #480's, #1121's and the directory's. Each command line a person pastes stays `python3 scripts/bootstrap.py`. - **`_predict_collisions`' docstring** says what the plan stores and what `execute` carries (NIT-3a). `scripts/shape_materialize.py`: - **`BOOTSTRAP_COMMAND`** = "`python3 scripts/bootstrap.py` (`python` on Windows)", as the template's script names it. - **The Makefile collision follow-up** says "until then `python3 scripts/bootstrap.py` (`python` on Windows) is what `make bootstrap` runs". - **Where the script went aside too,** the follow-up says "so `python3 scripts/bootstrap.py` (`python` on Windows) runs the shape's bootstrap once the script is merged, and `make bootstrap` once both merges are made" (NIT-2). - **The docstring's "Both callers say it"** paragraph says what is true (NIT-3a's twin, #1105's review NIT-4). #1105 made the same fix, and the merge keeps main's text (see "Main merge after #1105"). `templates/assembly-root/`: - **`AGENTS-shape.md`:** the "Work from here" paragraph adds "; where the root keeps a `GNUmakefile` or a `makefile`, which GNU make reads before `Makefile`, run `python3 scripts/bootstrap.py` (`python` on Windows), which is what the shape's `make bootstrap` runs." A sentence after the five-commands table says they are the shape's Makefile's targets, which `make` at the root reaches only where the file it reads first holds them, or includes a Makefile that does. - **`README.md`:** one sentence after Get started covers `make bootstrap` "here and below". A second says `make park` and `make resume` below work there only where that file holds them, or includes a Makefile that does. - **This is a template byte change, so every project re-pins.** No digest of either file is stored in this repository. `tests/` was grepped for every changed line, and only this PR's own rows pin them (see "Template pins"). Tests: - **`tests/test_adopt_e2e.py`:** two pins move to the new words, at :312 (the "until then" sentence) and at :447-450 (NIT-2's tail). - **`tests/test_adopt_next_make_reads_first.py`** (#1121's): the merge moves three pins, `OWN_MAKEFILE_NOTE`, `RUNS` and `script_aside`, to `python` on Windows. #480's NOTE merged with no textual conflict, so without them its rows fail. - **The new file `tests/test_adopt_make_bootstrap_followups.py`** has 35 rows: - the template's sentences, held to its `Makefile` (`PYTHON ?= python3`, `bootstrap:` runs `$(PYTHON) scripts/bootstrap.py`) and to `scripts/bootstrap.py`'s own words; - `make -n bootstrap` against the template's `Makefile` beside each kept name, each row asserting the `AGENTS-shape.md` caveat that measurement backs; - `_make_reads_first` (10 roots); - the `CONTRACTS_DIR` line for 4 directory roots; - NEXT for a directory, beside a kept `Makefile` (both names), after both merges, and after the script's merge alone; - #480's NOTE, called both as main's signature allows and with `surviving=`; - the collision follow-up's two sentences; - NIT-2 measured with the template's `Makefile` and stand-in scripts, and tied to the follow-up's new tail; - three real adoptions of a source holding `GNUmakefile/rules.mk`. One is kept by a `--path-policy` rule, so `plan` writes the line, `check` prints it and the split message carries it. One is answered `root`. In both, NEXT is asserted, `make -n bootstrap` in the pasted clone exits 2 with `GNUmakefile: Is a directory.`, and the printed command exits 0. In the third, the question is answered `code`: NEXT names `make bootstrap`, and that command runs in the pasted clone. ### This round (#1207's review, issuecomment-6374516382) | finding | what changed | |---|---| | SHOULD-1 | A row pins NEXT where only `scripts/bootstrap.py` went aside beside a kept `GNUmakefile/` (`test_next_after_the_script_merge_names_python3_where_make_stops`). It fails under the review's mutant X1. | | SHOULD-2 | The "Left" list is one issue, #1240. | | SHOULD-3 | "Landing order", below. #1121 landed while this round ran, and is merged and resolved here. | | NIT-1 | `AGENTS-shape.md`'s sentence after the table loses "(above)": nothing above names a Makefile that includes another. | | NIT-2 | `README.md` names `make park` and `make resume`. | | NIT-3 | Beside a kept `Makefile`, the directory NOTE names the merge as well as the directory. Its row is parametrized over `GNUmakefile/` and `makefile/`. | | NIT-4 | An adoption sends `GNUmakefile/` to the code leg, and NEXT names `make bootstrap`. It fails under the review's mutant X6. | | NIT-5 | The notes for the squash message at landing, below. | Measured at the merge head `6fa1edd56`, in a scratch worktree: - main `0ed3e57f8`'s bytes of the four production files (`adopt-project.py`, `scripts/shape_materialize.py`, `templates/assembly-root/AGENTS-shape.md`, `templates/assembly-root/README.md`) were checked out over the head; - the new file (35 rows) and `tests/test_adopt_next_make_reads_first.py` (40 rows) ran through the lane's runner; - the four files were restored with `git checkout`, and each matched the head by sha256 (`c94bd19b`, `12555f39`, `8660bafb`, `b3f9409b`). | file | fail on main | pass on main | |---|---|---| | the new file | 34 | 1: `test_an_adoption_sending_a_gnumakefile_directory_to_a_leg` | | `tests/test_adopt_next_make_reads_first.py` | 19 | 21 | Main now has #1121, so its `_next_steps` takes `surviving=`. The NEXT rows for a kept directory therefore fail on their words, not on a `TypeError`. Main reads `GNUmakefile/` as a file: "then run `python3 scripts/bootstrap.py`; GNU make at the root reads the source's `GNUmakefile`, ...". This is the case for the SHOULD-1 row, the after-the-merges row, both directory rows, and NIT-3's `makefile/` row. The new file's other rows fail on main for these reasons: - the template rows: main's `AGENTS-shape.md` and `README.md` lack the sentences; - `_make_reads_first`: main has no such helper; - the `CONTRACTS_DIR` line: "only once `GNUmakefile` does"; - the NOTE and collision rows: plain `python3`, and NIT-2's old tail; - the two adoptions that keep the directory. The one new row that passes on main is NIT-4's. It sends `GNUmakefile/` to the code leg, and there main's NEXT is already `make bootstrap`. It guards what main has: under the review's mutant X6, which hands NEXT every leg's paths, it is the row that fails. The 19 rows of #1121's file that fail on main are exactly the rows that use the three pins this merge moved (`OWN_MAKEFILE_NOTE`, `RUNS`, `script_aside`). The 21 that pass use none of them. Earlier heads, on main `961640c68` and with the same method: `1c7ca5c6d` failed 29 of 35 rows, and `8b499842f` failed 34 of 35 (the new file plus the three `tests/test_adopt_e2e.py` rows the mutants use). **This round**, at `75c90b6db`, in a scratch worktree. Each mutant was applied alone and restored, and each file's sha256 was checked against the commit (4/4 every time). Each ran against 95 rows: the new file (35), `tests/test_adopt_e2e.py` (27) and `tests/test_adopt_make_reads_first.py` (33). Unmutated, all 95 pass. | id | mutant | rows failing (of 95) | result | |---|---|---|---| | X1 | the review's: the script-only NOTE names `make bootstrap` after the merge (`if stops and makefile`) | 1 | killed | | X6 | the review's: `cmd_execute` passes every leg's paths | 1 | killed | | N1 | NIT-3 undone: beside a kept `Makefile`, no merge named | 2 | killed | | N2 | the merge named where the script went aside too | 1 | killed | | N3 | the merge named where no `Makefile` was kept | 4 | killed | | T1 | NIT-1 undone: "(above)" back | 1 | killed | | T2 | NIT-2 undone: README's `make park` / `make resume` sentence removed | 1 | killed | The merge with #1121 rewrote the clause code that N1-N3 mutate (`stopped` became `reads`, and the merge condition gained `stops`). The mutants were not run again at `6fa1edd56`. **Earlier,** 18 mutants at `1c7ca5c6d` against 43 rows, each applied alone in a scratch worktree and restored with its sha256 checked: | id | mutant | rows failing (of 43) | result | |---|---|---|---| | M1 | `_make_reads_first` drops the `/` of a directory | 14 | killed | | M2 | the clause's directory branch never taken | 10 | killed | | M3 | NEXT ignores a kept directory (`stops = None`) | 6 | killed | | M4 | NEXT's command stays `make bootstrap` for a directory | 4 | killed | | M5 | `cmd_execute` passes `surviving=()` | 2 (the two adoptions) | killed | | M6 | `BOOTSTRAP_COMMAND` without `(python on Windows)` | 12 | killed | | M7 | NIT-2's tail back to "neither ... before both merges are made" | 2 | killed | | M8 | `AGENTS-shape.md`'s "Work from here" caveat removed | 1 | killed | | M9 | the sentence after the five-commands table removed | 1 | killed | | M10 | `README.md`'s sentence removed | 1 | killed | | M11 | #480's NOTE back to plain `python3` | 1 | killed | | M12 | the collision follow-up's "until then" back to plain `python3` | 2 | killed | | M13 | after the merges, NEXT says `make bootstrap` where a directory stops make | 1 | killed | | M14 | the lookup takes the LAST kept name (`reversed`) | 4 | killed | | M15 | the directory NOTE's lead ignores a kept `Makefile` | 1 | killed | | M16 | the lookup folds case (`fold=True`) | 4 | killed | | M17 | the clause drops GNU make's own words (`GNUmakefile: Is a directory.`) | 10 | killed | | M18 | the file clause says `does` whatever `has` is | 0 | **survived** | M18 was equivalent before the merge: the file clause's only caller was the `CONTRACTS_DIR` line. Since the merge, #1121's NOTE calls it with `has them`, and `tests/test_adopt_next_make_reads_first.py` pins that wording. M18 was not run again. Each run went through the lane's runner: `gh` stubbed first on PATH with a bogus token, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, `EMAIL` set, `-p no:cacheprovider`, and `tmp_path_retention_policy=failed`. Each run is on a commit, with a clean tree. | run | commit | git | files | passed | failed | |---|---|---|---|---|---| | gate M | `6fa1edd56`, the merge | 2.43.0 | the new file, `test_adopt_next_make_reads_first.py`, `test_adopt_e2e.py`, `test_adopt_make_reads_first.py`, `test_path_guard_hygiene.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_pinned_copy_text.py`, `test_scaffold_leg_path_collisions.py` | 618 | 0 | | gate N | `6fa1edd56` | 2.47.1 | the new file and `test_adopt_next_make_reads_first.py` (75), then `test_adopt_e2e.py` (27), run one after the other | 102 | 0 | | gate H | `6fa1edd56` | 2.47.1 | the hygiene five | 379 | 0 | | gate A2 | `75c90b6db` | 2.43.0 | the new file, `test_adopt_e2e.py`, the hygiene five (`test_path_guard_hygiene.py`, `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_pinned_copy_text.py`), `test_adopt_make_reads_first.py`, `test_adopt_contracts_dir.py`, `test_adopt_printed_sentences.py`, and the template's readers: `test_scaffold_e2e.py`, `test_scaffold_free_text_values.py`, `test_free_plan_hint.py`, `test_template_quoted_values.py`, `test_template_values_single_pass.py`, `test_template_text_placeholders.py`, `test_bootstrap_placement_texts.py`, `test_park_resume_targets.py`, `test_update_shape_hardlink.py` | 1128 | 0 | | gate A | `1c7ca5c6d` | 2.43.0 | 17 files (the new file, `test_adopt_e2e.py`, the hygiene five, `test_scaffold_leg_path_collisions.py`, `test_adopt_make_reads_first.py`, `test_adopt_contracts_dir.py`, `test_adopt_printed_sentences.py`, `test_adopt_attributes_eol_block.py`, `test_adopt_leg_path_collisions.py`, `test_scaffold_e2e.py`, `test_bootstrap_placement_texts.py`, `test_park_resume_targets.py`, `test_template_text_placeholders.py`) | 966 | 0 | | gate B | `1c7ca5c6d` | 2.47.1 | the new file, `test_adopt_e2e.py`, `test_path_guard_hygiene.py`, `test_repo_hygiene.py` | 209 | 0 | | doctor | `1c7ca5c6d` | 2.43.0 | `test_shape_doctor.py`, `test_scaffold_leg_path_collisions.py` | 549 (3 skipped: the PowerShell rows, which run only on a Windows host) | 0 | `tests/` was grepped for every line this PR changes in `templates/assembly-root/` ("They are the shape", "reads first holds", "includes a Makefile that", "(above)", "here and below", "Where this root keeps", "it is what the shape", "before `Makefile`", "`python` on Windows"). Only `tests/test_adopt_make_bootstrap_followups.py` pins them. Four other files read the README as a whole, and each passes in gate A2: - `test_scaffold_free_text_values.py` counts its lines against the template; - `test_free_plan_hint.py` looks for "Free plan" and `--repo`; - `test_template_quoted_values.py` and `test_template_values_single_pass.py` render it. `git merge-tree --write-tree` of the head `01e93b6d1`, on git 2.43.0, at 18:11Z. - **Main** `9729315f1`, fetched again at 18:11Z, is the main this round merged last. The merge-tree is clean, and its tree is the head's own, `f6488542f`. - **Main moved to `801a7d6d0`** at 18:14Z, when lane 3's #1106 landed (`adopt-project.py` and two locale test files). The merge with it is clean, tree `25bc27d39`, and gate P4 passed on it. It was not merged in: it touches none of the code this round resolves, and landing refreshes the branch anyway. - **The open PRs:** one `gh pr list --state open --limit 100 --json number,headRefName,headRefOid,files` read at 18:11Z returned 51 open PRs. 26 of them touch one of this PR's paths. That includes `tests/test_adopt_collision_make_reads.py` and `tests/test_adopt_make_reads_first.py`, which this PR now changes. 25 merge cleanly: #1106, #1120, #1122, #1125, #1141, #1143, #1153, #1154, #1170, #1171, #1177, #1178, #1186, #1189, #1194, #1198, #1210, #1225, #1244, #1249, #1251, #1252, #1253, #1265 and #1268. #1106 has landed since (above). - **Lane 1's #1189** (`472944b5d`, read fresh at 18:11Z) edits three test files that this round changes too: `tests/test_adopt_next_make_reads_first.py`, `tests/test_adopt_collision_make_reads.py` and `tests/test_adopt_make_reads_first.py`. The merge is clean, tree `7fd030b29`. #1189's hunks are in the imports, in the clone rows' parameter lists and docstrings, and in a new refusal row, away from the moved pins, the `makefile-directory` row and NIT-A's comment. On that predicted merge, a local commit object, the three shared files pass on git 2.43.0: `tests/test_adopt_make_reads_first.py` and `tests/test_adopt_collision_make_reads.py` give 62 passed, and `tests/test_adopt_next_make_reads_first.py` gives 38 passed. There are fewer rows than at this head because #1189 leaves out the clone rows that keep case twins. - **#1237** (`b557859fd`) conflicts in `tests/test_adopt_check_throwaway_repos.py`, a file this PR does not change. Main conflicts with #1237 in that file too, while `6fa1edd56` did not, so the conflict is main's, carried in by the merge. - **Main moved again after the round** (rv1207e, at 19:25Z): lane 1's #1143 landed as `81eaf757c` (`adopt-project.py`) and lane 4's #1117 as `9252ef208` (README, the handbook and `scripts/family.py`). The head merges clean with both (trees `e40373967` and `279f8c6ce`), and rv1207e's gate P passed on the predicted merge with `9252ef208` (359 passed). At 19:22Z, 48 PRs were open, 24 touch this PR's files, and 23 merge clean; #1237's conflict is main's, as above. - **No longer on the list:** #1105, #1121, #1118 and #1123 are on main. Since `6fa1edd56`, #1104, #1160, #1169, #1180, #1190 and #1223 landed too. The conflicts are the same in every order, measured offline both ways round and with predicted squashes: - **#1121:** 4 hunks, 84 lines, in `adopt-project.py`'s `_next_steps` and `cmd_execute`'s comment. - **#1105:** 3 hunks, 65 lines, in `scripts/shape_materialize.py`'s `collision_follow_up`: its docstring's NIT-4 sentence, its paragraph on the Makefile's merge, and its two `return`s. - #1121 and #1105 merge with each other cleanly, and so do #1104 and this PR. So the order decides only who resolves them. **Both have landed**, and lane 5 resolved both on this branch: - **#1121** (merge `6fa1edd56`). The resolution is one lookup and one clause, `BOOTSTRAP_COMMAND` in #1121's NOTEs, and #1121's three test pins moved. - **#1105** (merge `7b0192bde`, see "Main merge after #1105"). The resolution keeps #1105's `reads_first` branch and runs `BOOTSTRAP_COMMAND` through it. NIT-2's tail is this PR's where `make` reads the merged `Makefile`, and #1105's where `make` reads another file. Four of #1105's test pins moved: `MERGE_IS_ENOUGH`, `BOTH_MERGES`, `MEANWHILE` and `SCRIPT_MERGED`. Main was merged in once more after lane 1's #1123 (merge `01e93b6d1`): one of #1123's rows pinned the line this PR changes for a kept `makefile/` directory, and it moved (see "The second merge, after #1123"). No other PR has to land first now. `6fa1edd56` was re-checked READY (rv1207b, rv1207c and rv1207d, issuecomment-6722602375). The head, `01e93b6d1`, needs a check of its delta since then: the two merges' resolutions, the four moved pins in #1105's file, #1123's moved row, and NIT-A. #1240 tracks what this PR leaves: - #1132's item 4 (one `GNU_MAKE_READS`, one lookup); - #1105's collision line, for a kept directory (its `python3` part is done here: see "Main merge after #1105"); - #1121's file NOTE beside a kept `Makefile`, which does not name the merge (as this PR's directory NOTE now does); - #1105's second review's NIT-1, NIT-3b, NIT-3c, NIT-4 and NIT-5; - `_make_reads_first_clause`'s `has` argument, now live; - rv1207e's NIT-1: the docstring sentence "With none, the Makefile's follow-up is what it was, byte for byte" (`scripts/shape_materialize.py` about :1067), kept from #1105, is no longer true, since this PR changes the line with no `reads_first`. It sits beside #1240's items F and G, which reword the same docstring. The local branch `fix/1132-template-bootstrap-stacked` (`9eb5cce0b`, never pushed and never tested) is a model of the end state on #1105 + #1121 + main. - **Item 5,** follow-ups that go stale after a hand edit of the plan, is its own issue, filed as #1206. It also holds the three related cases this work found: - a kept `makefile/` directory gets "until the two are merged"; - a directory kept only through deeper entries is not seen by `_root_keeps`; - a kept `Makefile/` directory is treated as a Makefile. - **This PR's `answered-root` row meets item 5 too.** `plan` asks about `GNUmakefile/`, and its stored `CONTRACTS_DIR` line, written before the answer, names no directory. The row asserts what NEXT and `make` do there, and not that line. - **Not measured:** a disk that ignores case, and GNU make other than 4.3. Review: rv1207b and rv1207c, lane 5's independent opus reviewers, and rv1207d, lane 5's independent sonnet reviewer, none of which wrote #1207: READY at `6fa1edd5` (issuecomment-6722602375). Then rv1207e, lane 5's independent opus reviewer, which wrote none of it: READY at `01e93b6d`, H0 of this landing, after the main merges past #1105 and #1123 (issuecomment-6726033282), with no MUST or SHOULD; its three NITs are answered in the body. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 01e93b6 passed tests, tests-git-bash and SonarCloud; the head c9be952 is a clean update-branch merge of main 9cb156a, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 0a01d35 over main 9cb156a: the adopt_collision_make_reads, adopt_e2e, adopt_make_bootstrap_followups, adopt_make_reads_first, adopt_next_make_reads_first, repo_hygiene, agents_md_codes, cli_reference sets, 333 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 #1132 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e `.gitattributes` it leaves and reads its exit code, so the root's failing filter is `plan-attributes-reencoded` alone, and a refused compare is named in git's words (#1192) (#1253) On main at `93d29314` (#1169's squash), `adopt-project.py check` asked the mount's `git rm` as a content compare: `update-index -q --refresh` over its `TAKEN` index, then `diff-files --name-only`. That index holds what the plan sends to a leg or drops, and also every `.gitattributes` of the tree, the root's own kept one included, so the refresh ran the clean filter of rules `git rm` never compares, and nothing read its exit code. Under a required clean filter that fails for the root's `.gitattributes`, the refresh died there and `diff-files` listed every file it had not reached. `check` then added a false `plan-file-reencoded`, naming a file the split stores as its blob (`specs/001-feature/spec.md`), to the one true finding, `plan-attributes-reencoded`. Outside v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z - The compare now asks what `git rm` asks. Each `.gitattributes` the mount leaves stays in `TAKEN`, so git still reads the rules from the index, and is marked with `update-index -z --assume-unchanged --stdin`, which `update-index --refresh` and `diff-files` both skip. A `.gitattributes` the plan sends to a leg or drops is in `moved` and is still compared. The root's own failing filter is then `plan-attributes-reencoded` alone (#530's D5(a) question), and a kept nested `.gitattributes` whose filter fails is named by the split's `git add -u` (`STORE_REFUSED`). - The refresh's exit code is read. `-q` makes a file that needs an update no error, so a nonzero exit is git refusing the compare. Its list is then not read, and the finding is git's last line (`COMPARE_REFUSED`, through `_refused_finding`), in the groups' order: the store's, the compare's, the copies'. The plainest reading, "read the code and name nothing on a failure", was not taken: for a filter that fails for a file the plan sends to a leg or drops, `execute` with `check`'s refusal patched out made both leg repositories and stopped extracting one, so naming nothing would have been a false `plan ok`. - `STOPS_AT_A_READ` says where `execute` stops for such a refusal: "at the first step that reads that file again: extracting the spec leg, extracting the code leg once the spec leg is pushed, or, once both legs are pushed, cloning the source for the split or mounting the legs". Which of them it is, the clock decides (`READ_AGAIN`). `STOPS_AT_A_CLONE`'s comment now says each leg is extracted and then pushed in turn, the spec leg first. - Not changed: D5(a)'s question (`_stored_as_written`), the split's side (`git add -u`), the pinned copies' question, `execute`'s own commands, what `TAKEN` holds, and any doc, `--help` text or doc-trio row. `tests/test_adopt_check_throwaway_repos.py` holds 27 new ids in 10 functions: 18 by `18c2aaef`, and 9 in the review round (3 in `898e9a5e`, 6 in `ad70474d`). - Measured with git 2.43 and CPython 3.12.3, base `93d29314` against head `18c2aaef` (the review round changes the stop text, comments and rows, not the compare): - #1192's case (a side-branch tree named by `attr.tree` says `/.gitattributes filter=w1136` and `*.md text`; `filter.w1136` is `clean=false`, `smudge=cat`, `required=true`, set in a global config, in `GIT_CONFIG_PARAMETERS` and in `GIT_CONFIG_COUNT`) went from exit 1 with `plan-attributes-reencoded` and the false `plan-file-reencoded`, to exit 1 with `plan-attributes-reencoded` alone. In process on base, the refresh gave exit 128 (`fatal: .gitattributes: clean filter 'w1136' failed`) and `diff-files` listed `.gitattributes specs/001-feature/spec.md`; at head the `--assume-unchanged` call and the refresh give exit 0 and `diff-files` lists nothing. - A root rule `/notes.txt filter=w1136`, with `notes.txt` sent to the spec leg, the code leg or dropped, went from `plan-file-reencoded` read off the files the dead compare never reached, to the refusal in git's words (`fatal: notes.txt: clean filter 'w1136' failed`). `execute` refuses before any leg on both. - The 18 rows: 11 failed and 7 passed on base, 18 passed at head. The 7 are the controls, which give the same result on both: a spec committed with CRLF (the compare runs to its end and names it), a CRLF `docs/.gitattributes` sent to the spec leg under the root's `* text eol=lf` (only the rules `git rm` leaves are skipped), and the side tree's `*.md text` alone (`plan ok`). The 9 round rows: 9 failed on base, 3 failed at `18c2aaef` (the `execute` ids that run as if each extraction trusted the file's stat data), 9 passed at `ad70474d`. On base, a kept `.github/<folder>/.gitattributes` whose filter fails, its folder named in UTF-8 or not, got a second finding, a false `plan-file-reencoded` naming `specs/001-feature/spec.md`: a second form of #1192, cured by the change and held by no row until the round. - Mutants of the changed lines, each run in a scratch clone and restored after (the blob read back): 10 of 10 killed at `18c2aaef`, and 11 of 11 at `ad70474d`. r1253's mutants R2 and R9 survived every row at `18c2aaef`; the kept row kills both. r1253b's own 14 are killed too. - Where `execute` stops with `check`'s refusal patched out, over 72 runs at `ad70474d`: extracting the spec leg 47, extracting the code leg once the spec leg was pushed 1, and, with the filter kept out of both extractions, cloning the source for the split with both legs pushed 24 of 24. All 72 passed, none skipped. r1253b's 144 runs under load all passed, none skipped (72 at the spec leg, 72 at the split's clone). - Known limits: - Where git refuses the compare, only its first refusal is named. The files it did not reach are not named until it no longer refuses, as with the split's refused `git add -u`. - Where `execute` stops for such a refusal is the clock's. The mount's `git rm` was never the first to read the file in any run: after a checkout that trusted the file's stat data, no later index write falls in the second it was written, unless the clock steps back. In r1253b's probe the mount was the first read only with the index's mtime set by hand (r1253b's NIT-A). In a run where no step reads the file again, `execute` may go on: the finding names a source that can fail, as the moved-files finding does (`READ_AGAIN`). - git's words are its last line in this machine's locale, as for every refusal the rehearsal names. Measured with git 2.43 only: that `update-index --refresh` and `diff-files` skip an `--assume-unchanged` entry is git's documented behaviour, and CI's git was not measured. - A path that is not UTF-8 prints as U+FFFD in git's words (pre-existing, r1253's NIT-2). `_SplitRehearsal.refused` decodes git's stderr with `replace`, so the round's row holds every word of that finding but the folder's name. It reaches every refusal the rehearsal names. Follow-up text, if wanted: "adopt-project.py: `_SplitRehearsal.refused` decodes git's stderr with `replace`, so a path that is not UTF-8 in a refusal `check` names prints as U+FFFD, not as `_spelled`'s escape (measured on #1253's branch with git 2.43: `fatal: .github/caf<U+FFFD>/.gitattributes: clean filter 'w1136' failed`)". Review: lane 1's independent reviewers reviewed each round. r1253 found `18c2aaef` NOT READY, for two SHOULDs and two NITs with no MUST (#1253 issuecomment-6723519114): SHOULD-1, the stop text left out cloning the source for the split, and SHOULD-2, no row for a kept nested `.gitattributes` or a name that is not UTF-8. The fix round `ad70474d` (`845666aa`, `898e9a5e`, `ad70474d`) answered both and NIT-1, the `STOPS_AT_A_CLONE` comment, and left NIT-2 as a Known limit (issuecomment-6724083501). r1253b found `ad70474d` READY, with no MUST or SHOULD and two NITs (issuecomment-6724522433). NIT-A's sentence is applied to the body and to this message. NIT-B, an optional rewrite of the optional row so that it forces each read instead of waiting for the clock, is left as a follow-up idea. Tests, under a stub `gh` with a bogus token, at `ad70474d` with CPython 3.12.3 and git 2.43, each call under 9 minutes and at most two at once: the #1192 section (10 functions) 27 passed; `tests/test_adopt_check_throwaway_repos.py` 178 passed and 1 skipped (the row that needs a disk that ignores case); `tests/test_adopt_reencoded_files.py` with `tests/test_adopt_nested_attributes.py` 126 passed; `tests/test_adopt_printed_sentences.py` with `tests/test_adopt_single_repository_record.py` 93 passed; `tests/test_scaffold_leg_path_collisions.py` 104 passed; `tests/test_path_guard_hygiene.py` with `tests/test_no_auto_maintenance.py` 179 passed and 2 skipped (both `test_no_auto_maintenance.py:222`, which the probe environment's `GIT_CONFIG_GLOBAL=/dev/null` causes); the doc trio 171 passed; `tests/test_shape_doctor.py` in 4 shards 112 passed and 1 skipped, 111 passed and 1 skipped, 112 passed, and 111 passed and 1 skipped (the skip is the PowerShell row, which runs only on a Windows host). r1253b's summary lines at `ad70474d` equal every one of those counts but the shards, which its brief forbade; on a clean merge of `origin/main` at `12122952` it got 178 passed and 1 skipped for the throwaway file. At `18c2aaef` the same files gave the same counts, summed over each pair, but for the throwaway file's 169 passed and 1 skipped, the 9 round ids being the difference. It lands by fast refresh (Brett Heap, 2026-10-15T22:48:35Z). Its first H0, `ad70474d`, ran `tests` on 2026-11-08; after #1293 mended main, the branch was refreshed over it, and that head, `dcd5d8b0`, is its H0. Its H0 `dcd5d8b0` is green on Linux and SonarCloud. The branch was never rebased. Any commit after H0 is update-branch's clean merge of main, and the head that lands, 91c8932, is green on SonarCloud and Git Bash. Lane 1's gate passed on its tree: 23 pytest calls over `tests/test_adopt_check_throwaway_repos.py`, `tests/test_adopt_reencoded_files.py`, `tests/test_adopt_nested_attributes.py`, `tests/test_adopt_printed_sentences.py`, `tests/test_adopt_single_repository_record.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_path_guard_hygiene.py`, `tests/test_no_auto_maintenance.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`, `tests/test_repo_hygiene.py`, `tests/test_adopt_tracking_branch.py`, `tests/test_adopt_unreadable_git_parts.py`, `tests/test_chain_link_fold.py`, `tests/test_naming_policy_line_break.py`, `tests/test_pinned_copy_columns.py`, `tests/test_siblings_family_name.py`, `tests/test_update_shape_apply_lines_and_headers.py`, `tests/test_update_shape_header_comments.py`, `tests/test_update_shape_messages.py`, `tests/test_update_shape_unreadable.py`, `tests/test_validate_family_member_id_pattern.py`, `tests/test_validate_family_unreadable.py`, `tests/test_validate_pins_spelled_and_named.py`, `tests/test_value_pattern_line_break.py` and `tests/test_shape_doctor.py` in 4 shards: 2,257 passed, 7 skipped. Closes #1192 Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not text as `_read_as` says what the plan's reader read, "records `commit:` as the number 7", where it spelled it as Python does, `commit: 7` and `True`, so `check` and `execute` name it in the words the plan's other refusals use under the same `plan-pin-malformed` (#1114) (#1198) This PR lands only after the v1.2.0 tag, on Brett's word. ## What `_pin_row_problem` holds each of a pin's five fields (`product`, `repository`, `commit`, `tree_sha256`, `digest_source`) to text. A value that is not text it spelled with Python's `repr`: a number as `commit: 7`, a boolean as `True`, a list as `['a', 'b']` and a mapping as `{'k': 'v'}`. Every other refusal of a plan's value says what the reader read, with `_read_as`: "the number 7", "the boolean true", "a list of 2 item(s)", "a mapping of 1 key(s)". W1020 found the gap while working on #1020. It says it that way now, field by field, in `adopt-project.py`'s `_pin_row_problem`, in the `not_text` branch only: - one field: ``records `commit:` as the number 7, which is not text`` - two or more: ``records `commit:` as the number 7, `tree_sha256:` as the boolean true, which are not text`` What stays as it was: - the code, `plan-pin-malformed`, the remedy, and the sentence's ending, `which is not text`; - every text value's spelling, byte for byte (`commit: '7'`, which is not 40 hex); - a null or a blank: it is "records no `commit:`" under the code it has always had, and no value is spelled there. Every field takes the one site (`PIN_FIELDS`). The docstring and a comment say so. The change is 13 lines added and 4 removed in `adopt-project.py`. ## Behaviour change A pin field that is a number, a boolean, a list or a mapping is named by what it was read as, where it was named as Python spells it. A text value, a null and a blank print as they did. ## Rows `tests/test_adopt_pin_row_values.py` is new, ASCII only, with 45 rows. Each runs `check` and then `execute --yes`, with a stub `gh` first on PATH and `--local-remote-dir`, and asserts one finding, `execute` refusing with that same finding, an empty `gh` log, no remotes directory made and no Traceback. | Group | Rows | On main's code | On this head | | --- | --- | --- | --- | | The matrix: 5 fields by number, boolean, list, mapping | 20 | 20 failed | 20 passed | | The whole sentence: `commit: 7`, and a boolean `digest_source` | 2 | 2 failed | 2 passed | | Edge forms: 0, false, 7.5, -3, 007, `[]`, `{}`, `[a]` | 8 | 8 failed | 8 passed | | Two fields at once, a non-text commit beside bad text, entry 2 | 3 | 3 failed | 3 passed | | Null and blank controls | 6 | 6 passed | 6 passed | | Text controls (`commit: "7"` and `"true"`, another product, no owner, short digest, source by hand) | 6 | 6 passed | 6 passed | | **All** | **45** | **33 failed, 12 passed** | **45 passed** | Every one of the 33 failures is the finding text holding the repr spelling (``records `commit: 7` ``, `True`, `['a', 'b']`, `{'k': 'v'}`, `{}`, `[]`). There is no crash or other cause. The 12 that pass on main are the controls, which pin what must not change. Two existing rows pinned the old spelling, and no others did. Each now expects the new one, and both pass on this head (2 passed): - `tests/test_adopt_pins.py`, the `a-number-commit` row: `` `commit:` as the number 1234567 ``; - `tests/test_adopt_plan_value_order.py`, the `not-text` row: `` `digest_source:` as the number 7 ``. ## Mutants 19 mutants of this head's `adopt-project.py`, all killed. Each was run against the new file (first failure with `-x`, or a whole run where the table says so), and each run was restored with its sha256 checked. | Mutant | Result | | --- | --- | | M1: main's repr form put back | killed, 33 failed and 12 passed | | M2: `!r` inside the new "as" form | killed, 33 failed and 12 passed | | M3: `str()` in place of `_read_as` | killed | | F-repr-product, F-repr-repository, F-repr-commit, F-repr-tree_sha256, F-repr-digest_source: repr put back for one field only | 5 of 5 killed, each by that field's own rows | | M4: "is" in place of "as" | killed | | M5: singular and plural swapped | killed | | M6: only the first field named | killed | | M7: `missing` takes any falsy value (an existing guard; the zero, false and empty rows pin it) | killed | | M8: only a list and a mapping counted as not text | killed | | M9: the ending without "how it was read" | killed | | M10: the code `plan-pin-unrecorded` | killed | | T1 to T4: the text branches unquoted (commit, product and repository, digest, source) | 4 of 4 killed | ## Gate A scratch merge of this head `d38e4e36d` with main `961640c68`: commit `1ef3f0783`, tree `b12197610`, which is this head's own tree. Targeted files only, in the landing gate's environment (a stub `gh` first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, an empty global git config). The set is 47 test files: every one that names `plan-pin-malformed`, `plan-pin-unrecorded`, `_pin_row_problem`, `_read_pins` or `pins:`, the new file, and `test_path_guard_hygiene.py`, `test_repo_hygiene.py` and `test_scaffold_leg_path_collisions.py`; and `test_shape_doctor.py` (448 ids, each in exactly one shard, run from a script that reads each list into an array because 25 ids hold spaces). Every chunk exited 0, with 0 failed. | Chunk | Files | Result | | --- | --- | --- | | f01 | `test_adopt_pins.py` | 150 passed | | f02 | `test_adopt_plan_yaml_quoting.py`, `test_adopt_plan.py` | 606 passed, 1 skipped | | f03 | `test_adopt_execute_refusals.py`, `test_adopt_pin_row_values.py`, `test_adopt_plan_escapes.py`, `test_adopt_plan_value_order.py`, `test_adopt_reencoded_files.py`, `test_adopt_root_symlinks.py`, `test_adopt_shape_pin_and_work_dir.py`, `test_adopt_template_values.py` | 661 passed, 3 skipped | | f04 | `test_bump_remedies_family_root.py`, `test_chain_link_fold.py`, `test_chain_pin_sources.py`, `test_chain_remedy_callers.py`, `test_doctor_gitlink_repairs.py`, `test_doctor_recorded_answers.py`, `test_family_add_member_id.py`, `test_family_makefile_siblings.py` | 255 passed | | f05 | `test_family_tracking_branch.py`, `test_legs_not_a_list.py`, `test_link_source_chain_defaults.py`, `test_manifest_validator.py`, `test_naming_policy.py`, `test_neutral_pin_entry_one_spelling.py`, `test_neutral_pin_validator.py`, `test_non_list_project_fields.py` | 757 passed | | f06 | `test_path_guard_hygiene.py`, `test_pin_annotated_tag.py`, `test_pin_source_env_collisions.py`, `test_pinned_copy_text.py`, `test_reader_refusals_named.py`, `test_repo_hygiene.py`, `test_scaffold_e2e.py`, `test_scaffold_leg_path_collisions.py` | 599 passed | | f07 | `test_scaffold_pin_and_reuse.py`, `test_scaffold_pin_collisions.py`, `test_scaffold_referent_chain_links.py`, `test_setup_project_py.py`, `test_template_text_placeholders.py`, `test_update_shape_add_ignored.py`, `test_update_shape_landing.py`, `test_validate_pins_named_and_spelled.py` | 1078 passed | | f08 | `test_yaml_flow_brackets.py`, `test_yaml_flow_quotes.py`, `test_yaml_indicator_openers.py`, `test_yaml_reader_spaces.py` | 813 passed | | d01 | `test_shape_doctor.py`, 56 ids | 56 passed | | d02 | the same, 56 ids | 56 passed | | d03 | the same, 56 ids | 56 passed | | d04 | the same, 56 ids | 56 passed | | d05 | the same, 56 ids | 56 passed | | d06 | the same, 56 ids | 56 passed | | d07 | the same, 56 ids | 56 passed | | d08 | the same, 56 ids | 53 passed, 3 skipped | The file chunks hold 4919 passed and 4 skipped (f02: `SHAPE_MEDXEHR_CLONE` is unset; f03: the reftable backend needs git 2.45). The doctor shards hold 445 passed and 3 skipped (d08: the PowerShell line is printed only on a Windows host). In all, **5364 passed, 7 skipped, 0 failed.** **Run on the merge with #1125's head.** A scratch merge of this head with #1125's head `9549006fe`: commit `8a291d8b2`, tree `88530bb7`. The new file (45), `test_adopt_pins.py` (150), `test_adopt_plan_value_order.py` (40), `test_adopt_execute_refusals.py` (181) and `test_adopt_check_plan_name.py` (45): **461 passed.** ## Merge-trees `git merge-tree --write-tree`, this head against each, all clean (exit 0), at the heads of 2026-10-23T08:20Z: - `origin/main` `961640c68`; - the 23 open PRs that touch `adopt-project.py`: #1194 `900f77af9`, #1190 `00f959c61`, #1189 `4d40ce9fd`, #1186 `07486a498`, #1180 `b316f4157`, #1178 `c4beba193`, #1177 `5208e874e`, #1171 `8162b874e`, #1170 `24ee133aa`, #1169 `258051022`, #1154 `8a7828332`, #1153 `ff33ba12a`, #1143 `7af5fabc3`, #1141 `7a2711d03`, #1125 `9549006fe`, #1123 `5c3908f8d`, #1122 `c01c4d73d`, #1121 `1c5173758`, #1120 `c55e55e33`, #1119 `bba168bdd`, #1118 `2307036ab`, #1106 `504a1dd65`, #1105 `b28f40de0`; - #1126 `4c1f80cf4`, which touches `tests/test_adopt_pins.py`. ## This PR needs no #1125 #1125 (for #1020) makes `_read_pins` say an entry that is not a pin as `_read_as` says it. Its `adopt-project.py` hunks are at the `_read_pins` docstring and at the non-dict entry's `{row!r}` (about line 625 on main), and they do not touch `_pin_row_problem`. Its test hunks do not overlap the lines this change edits. The two are textually independent: this PR is based on main, not stacked, the merge-tree with #1125's head is clean, and the run above on the merge with its head passed. An entry that is neither a name nor a mapping stays #1020's, in #1125. ## Not in this PR The other `repr` in the pin rows are on a value that is text by then: the later branches of `_pin_row_problem` (spelled as before), a text product in an entry's name, and an entry that is a name. They are not gaps. `docs/release-notes/v1.2.0.md` is left alone. Its lines 297 to 299 mention #901's pins wording, and the release owner may want a line there. ## Out of scope Four other places in `adopt-project.py` put a plan value of any type into a refusal with `!r`, as the pin rows did: `plan-wrong-kind`, `plan-unsupported-mode`, `plan-bad-leg` and `plan-bad-id`. They are not pin rows, so this change leaves them. They were found by reading the code, not by running it. They are filed as #1199, with each site's line and what it would print. Closes #1114 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blems against its source, in the order a person mends them (another repository, a refused name, a moved source, an unanswered entry), so `check`'s first finding is `execute`'s refusal for each pair (#1113) (#1210) **Stacked on #1125.** This PR's base is `fix/1020-bad-id-first-problem` (#1125's branch, head `9549006f`). It lands after #1125 and stays a draft until then. #1125 makes `check`'s first finding `execute`'s first refusal for a bad `id:` or `org:`. This PR extends that rule to the four pairs #1113 measured, which have neither. **Fix round.** The first gate failed six rows of `tests/test_adopt_relative_submodule_url.py` that pinned `check`'s old order on a plan hand-edited into another organisation, the relative url before `plan-not-the-source`, a plan `execute` already refused `plan-not-the-source` first on #1125's head; they now expect `plan-not-the-source` first, and a new row asserts that both commands name it first. ## The rule and the order One generator, `_against_its_source`, asks a plan's four problems against its source in one order, and both commands read it. `execute` refuses its first problem. `check` lists each one, in that order and in its own words, before the split branch and the entries. The order is the one a person mends them in: 1. a plan for another repository (`plan-not-the-source`); 2. a name the naming policy refuses (`naming-unclassified`, or `naming-role-mismatch`); 3. a source that has moved on since `plan` (`plan-stale`); 4. an unanswered entry (`plan-unresolved`). Why this order: - **An unanswered entry last.** Each of the first three is mended by running `plan` again, as its remediation says, and a fresh plan takes back every answer given before. An answer given first would be lost. - **Another repository first.** Nothing in such a plan is this source's own: it is mended by `plan` under the source's own name, or from the source it is about. And `execute` must ask the naming policy nothing about the plan's names, and no remote anything, before it refuses it (#499). `_against_its_source` is a generator, so `execute` asks nothing after the first problem, and that still holds. - **A refused name before a moved source.** The name is mended by `plan` given another `--project`, or after the source is renamed on GitHub. That run reads the source as it is now, so it mends the moved source too. `plan` run again as it was, for the moved source, would leave the name to mend. - **The least change to each command.** The order keeps `execute`'s order for the plan's names (#499, #716) and `check`'s order of a moved source before its entries. Only the pairs that disagreed change. ## Before and after Measured on #1125's head `9549006f` and on this head, with a stub `gh` (exit 97), `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, a proxy that goes nowhere, an empty global git config and `--local-remote-dir`. No run made a leg. `check` exits 1 and `execute` exits 2 in every row. | The plan holds | `check` before | `execute` before | both, here | |---|---|---|---| | a moved source and an unanswered entry (#1113) | `plan-stale` | `plan-unresolved` | `plan-stale` | | another repository and an unanswered entry (#1113) | `plan-unresolved` | `plan-not-the-source` | `plan-not-the-source` | | another repository and a moved source (#1113) | `plan-stale` | `plan-not-the-source` | `plan-not-the-source` | | a refused name and an unanswered entry (#1113) | `plan-unresolved` | `naming-unclassified` | `naming-unclassified` | | a refused name and a moved source (not in #1113's table) | `plan-stale` | `naming-unclassified` | `naming-unclassified` | | all four | `plan-stale` | `plan-not-the-source` | `plan-not-the-source` | | another repository and a refused name | `plan-not-the-source` | `plan-not-the-source` | `plan-not-the-source` (unchanged) | The fifth row is not in #1113's table, but the two commands disagreed there too, and the same rule decides it. The same probe, comparing each command's output before and after, with paths and hashes masked: - A plan with one of the four problems: both commands print the same stderr and stdout before and after. - Both commands' stdout is the same before and after in every row. - `check`'s stderr in each pair row holds the same lines as before, reordered: the same findings and the same remediation lines. - `execute`'s stderr changes in one row only, a moved source beside an unanswered entry: `REFUSED plan-stale`, in its own words, where it said `REFUSED plan-unresolved`. No refusal's code or wording changes, and no code is added. Re-run on this head after the fix round, the probe gives the same: the 24 plans (these 11, and the 13 of the next section) each get one first problem from both commands, no run makes a leg, and the comparison with #1125's head is the one above, line for line. ## #1113's suspected case: a `--work-dir` that cannot be written Measured on #1125's head and on this head, the two commands agree in every row, and nothing here changes it: - **A bad `id:` or `org:` beside a `--work-dir` this user cannot write in** (`chmod 500`): `plan-bad-id` or `unsafe-value` to both. #1125's review round 1 (RV1125's SHOULD-1) made both commands ask the two before `_work_root`, which settled this case. - **Each of the 11 plans above beside the same `--work-dir`:** `adopt-work-dir-unwritable` first to both. ## What changed, in `adopt-project.py` - **`_against_its_source` and `_Unmended` (new).** `_against_its_source` is the rule. It asks: - `_refuse_a_plan_for_another_repository`; - `_check_names`, given the source only where the plan names no other repository, as `check` gave it (#716); - `_refuse_an_unrunnable_plan`. It yields each problem as an `_Unmended`: the `Refusal` `execute` refuses it with, and `check`'s FINDING lines and remediation for it, in `check`'s words as they were. `check` lists an unanswered entry with the entries, each by its path (`_leg_findings`), after the rule's problems. - **`cmd_execute`.** It refuses the first problem, `next(_against_its_source(...))`. Before, it called `_refuse_a_plan_for_another_repository`, `_check_names` and `_refuse_an_unrunnable_plan` in turn. - **`cmd_check`.** It lists the rule's findings right after the Triad advisory, where it listed a moved source. Another repository and the names are no longer listed after the entries. Three things are still asked where they were, and only where no name was refused: - the names' classification lines on stdout; - the findings about what the disk or GitHub will not hold (#863); - the leg question (#798). The remediation lines it prints after its findings follow the findings' order, so the rule's come first. - **`_refuse_an_unrunnable_plan`.** It asks a moved source before an unanswered entry. Its docstring says why. - **`_refuse_a_plan_for_another_repository`.** Unchanged. The rule calls it. ## Tests `tests/test_adopt_plan_against_its_source.py` (new, ASCII only), 11 rows. Each runs `check` and then `execute --yes` on one hand-edited plan, both with the same `--local-remote-dir`, and asserts that no leg is made. - **7 pair rows** (the six pairs and all four). Each asserts: - `check` exits 1 and `execute` exits 2; - both name first the same code, the rule's first; - `check` lists every problem the plan holds, in the rule's order. - **4 controls**, one per code: the plan holds that problem alone, and both commands name it. - **Against #1125's head `9549006f`:** - the 6 pair rows that disagreed fail, each because the two commands named different codes, for example `('plan-stale', 'plan-unresolved') != ('plan-stale', 'plan-stale')`; - the `another-and-name` guard row and the 4 controls pass. - **Here:** 11 passed. `tests/test_adopt_relative_submodule_url.py`, 6 rows changed and 1 new (the fix round): - **The 6 rows** are `test_check_keeps_a_relative_url_its_github_leg_resolves_alike` for `[https]`, `[scp]`, `[scp-other-case]`, `[ssh-port]` and `[ssh-github-443]`, and `test_a_credential_in_the_source_remote_is_never_printed`. - Each hand-edits the plan's `org:` to `otherorg`, so the plan names `otherorg/Thing` and the source is `testorg/Thing`: a plan for another repository. - Each expected `check` to list the relative url first and `plan-not-the-source` after it, `check`'s old order. They now expect `plan-not-the-source` first. Their other assertions are unchanged; the credential is still printed nowhere. - **The new row,** `test_check_names_another_repository_first_as_execute_refuses_it`, runs one such plan. It asserts that `check`, with and without `--local-remote-dir`, lists `plan-not-the-source` and then the url, that `execute` refuses `plan-not-the-source`, and that no leg is made. - **Measured on #1125's head** for the six source remotes: `execute` already refused each such plan `plan-not-the-source`, and its output is the same on both heads. Only `check`'s order changed. No spelling of the source's `origin` reads as another repository: with `org:` left as the source's, `check` passes on both heads, as the rows assert. Both files together, 46 rows: - **Against #1125's head:** 13 failed, 33 passed. The 13 are the 6 pair rows above and the 7 relative-url rows, each on the order: `plan-not-the-source` where the url was expected first, or the url first in the new row. - **Here:** 46 passed. ## Mutants Each was applied to `adopt-project.py`, run against both files (46 rows), and restored, with its sha256 checked (`4c8c1c9e`, the same after each one). Unmutated, the 46 pass. M1-M6 are the first round's; M7 and M8 are the fix round's. | Mutant | Killed by | |---|---| | M1, `execute`: `_refuse_an_unrunnable_plan` asks an unanswered entry before a moved source (pair 1) | 2: `moved-and-unanswered`, `all-four` | | M2, `check`: lists another repository after the entries (pair 2) | 11: `another-and-unanswered`, `another-and-moved`, `another-and-name`, `all-four`, and the 7 relative-url rows | | M3, `check`: lists a moved source before another repository (pair 3) | 3: `another-and-moved`, `name-and-moved`, `all-four` | | M4, `check`: lists a refused name after the entries (pair 4) | 3: `name-and-unanswered`, `name-and-moved`, `all-four` | | M5, `check`: lists a moved source before a refused name (pair 5) | 2: `name-and-moved`, `all-four` | | M6, the rule: asks a refused name before another repository (both commands) | 2: `another-and-name`, `all-four` | | M7, `execute`: asks `_refuse_what_check_finds` before the rule | 8: the pair rows but `another-and-name`, the `plan-stale` control, and the new relative-url row | | M8, `check`: lists all the rule's problems after the entries | 14: the pair rows but `another-and-name`, the `plan-stale` control, and the 7 relative-url rows | ## Gate Run on the scratch merge `743ebe176`: `origin/main` `961640c68` plus this head `90eca4144` (tree `d71d6f00a`, never pushed). It ran in the landing gate's own environment (a stub `gh`, `GH_TOKEN=bogus`, an empty global git config, `EMAIL` set, the `LANES_*` and `GIT_SSH_COMMAND` variables unset), one shard at a time, each started with at least 19G free. The ids were collected from that tree, 4405 + 448 = 4853, and each is in exactly one shard. The file list is the 46 test files W1113's gate named and four more, added in the fix round (50 in all): `test_adopt_c_locale_bytes.py` and `test_adopt_submodule_url_answers.py` (they import helpers and the module-scoped `adopter` fixture from the changed test file, so they sit in its shard, r03), `test_reader_refusals_named.py` (it hand-edits a plan's `org:` and checks `check`'s exact findings) and `test_github_name_limits.py`. Every shard exited 0. | Shard | Result | |---|---| | r01 | 600 passed | | r02 | 613 passed | | r03 | 464 passed, 1 skipped (`test_adopt_plan.py::test_plan_on_medxehr_matches_the_rulings`) | | r04 | 547 passed | | r05 | 599 passed, 19 skipped (all in `test_family_init_values.py`) | | r06 | 542 passed | | r07 | 489 passed | | r08 | 531 passed | | d01 to d07 | 56 passed each (`test_shape_doctor.py`) | | d08 | 53 passed, 3 skipped (`test_shape_doctor.py`) | - **r01 to r08:** 4385 passed, 20 skipped (4405 ids). - **d01 to d08:** 445 passed, 3 skipped (448 ids). - **Total:** 4830 passed, 23 skipped, none failed, none errored. Main has moved since this gate: `origin/main` is `ac5f28d2a` (#1188, #1130 and #1098 landed). They change `scaffold-project.py`, `scripts/family.py` and four test files. None of them is a file this PR changes, a module `adopt-project.py` imports, or one of the 50 gated test files, so the gate was not run again on that tip. The merge-trees below were. ## Merge-trees `git merge-tree --write-tree`, run again on 2026-10-23 at 15:31Z against the current `origin/main`, every one clean: - **`origin/main` `ac5f28d2a`:** clean, tree `1a65ab9bd`. - **#1125 `9549006fe`:** clean. This branch is on it. - **#1177 `5208e874e`** (`fix/1152-plan-and-remote-paths-spelled`, which edits a line of `_refuse_a_plan_for_another_repository`): clean, with this head and three-way on the scratch merge. - **Every open PR that touches `adopt-project.py` or either test file, 25 of them, #1125 and #1177 among them:** clean, both with this head and three-way on the scratch merge of `ac5f28d2a` and this head, `98afb128a` (`git merge-tree --write-tree <that merge> <its head>`). Since the first run, #1207 is new, and #1119, #1189 and #1190 have moved; all four are clean. No open PR touches either test file. The list is in a code block, so that it does not mention each PR: ``` PR head with this head on the scratch merge #1105 b28f40d clean (tree 32fce74c6) clean (tree 490f555f9) #1106 504a1dd clean (tree 3abfa1b6a) clean (tree db57682e9) #1118 2307036 clean (tree 79a3888bf) clean (tree 629f167ca) #1119 195d993 clean (tree a2926127e) clean (tree 6aec4c863) #1120 c55e55e clean (tree 6171a52f8) clean (tree 0cb310664) #1121 1c51737 clean (tree 3acb80467) clean (tree 2771fbfd0) #1122 c01c4d7 clean (tree af9a4c414) clean (tree ef00f52f7) #1123 5c3908f clean (tree eb7cb34b3) clean (tree 4c311748b) #1125 9549006 clean (tree 239df6d) clean (tree 1a65ab9bd) #1141 7a2711d clean (tree e1c8f7169) clean (tree 3afeda790) #1143 7af5fab clean (tree 35cd165de) clean (tree aae00cfc7) #1153 ff33ba1 clean (tree 57da668e8) clean (tree f91ce3d8d) #1154 8a78283 clean (tree 28699ba0c) clean (tree 3e6b1ae98) #1169 1e958ca clean (tree e3ef802be) clean (tree f4566b2c4) #1170 24ee133 clean (tree 94b9291c8) clean (tree 848bb0ed0) #1171 8162b87 clean (tree 65ca8196d) clean (tree f15571ef3) #1177 5208e87 clean (tree 08e3d6a62) clean (tree 111f3c238) #1178 c4beba1 clean (tree 646f02266) clean (tree eae3b5996) #1180 b316f41 clean (tree 25b3d4b6f) clean (tree 85a559171) #1186 07486a4 clean (tree d0fe77836) clean (tree 849f0ef41) #1189 a131fc2 clean (tree 5b1ab0620) clean (tree 9851d22b7) #1190 91a937e clean (tree aa83fae05) clean (tree 18101f50b) #1194 900f77a clean (tree 9d74efa35) clean (tree 46317beae) #1198 d38e4e3 clean (tree 87ca21b8c) clean (tree 7fc45a875) #1207 1c7ca5c clean (tree 1dbab659d) clean (tree 1e44c98f0) ``` ## Left for a follow-up (outside #1113) - **An unanswered entry beside an uncovered path still gives two first problems,** filed as #1197. `check` lists `plan-uncovered` first and `execute` refuses `plan-unresolved` first. Measured the same on #1125's head and on this head. `execute` refuses an unanswered entry before `_refuse_what_check_finds`, while `check` lists it in `_leg_findings`, after the paths and the coverage. Other entry findings ahead of it in `_entry_findings` are likely the same (`plan-unsafe-path` among them), and so is the taken split branch, but those were not measured. Closes #1113 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cter a terminal acts on in the command, its directory and what it printed, as `_first_said` spells gh's, so `--local-remote-dir` no longer reaches the terminal raw there; a NOTE before `NEXT:` says when the source's path in its clone command holds a character a terminal does not show as itself; and rows pin six sites #1177's 148 left to chance (#1195) (#1225) This PR was stacked on #1177, which landed as `8bcdb2c`. RS1225 then restacked it onto `main` with three merges (#1177's final head, its squash, then `main`), which carry exactly the reviewed change (#1225 issuecomment-6752574422). ## What #1195 follows RV1177's review of #1177 (for #1152). It names two raw routes, N3 and N4, and six mutants, K1, K2, K4, K5, K6 and K7, that all 148 rows of #1177's `tests/test_adopt_plan_and_remote_paths_spelled.py` let through. Three files change: `adopt-project.py`, that test file, and `tests/test_adopt_github_exits.py`. ### N3: a refused command no longer names `--local-remote-dir` raw `CommandFailed.loudly` belongs to `scripts/shape_materialize.py`, and every tool shares it. It printed the command, its `(in <cwd>)` line and its output as they were. So when a `pre-receive` hook refused a leg push under `--local-remote-dir <D>/re<U+202E>mote`, the raw U+202E was printed three times: in the command, and in git's own `To` and `error:` lines. The change is in `adopt-project.py` only. `shape_materialize.py` is unchanged. - `_unacted(text, kept="")` writes each character that `str.isprintable` refuses, other than those in `kept`, as `_spelled_character` spells it, with no backticks. That is what `_first_said` does to the line gh said. - `_loudly(exc, step)` builds the same REFUSED block from the command's arguments, its directory and its output, each passed through `_unacted`. - The output keeps its line breaks (`kept="\n"`), so it still reads one line per line. - This settles the point the issue left open: `loudly` prints the whole output, while `_first_said` keeps one line. - The leg push (`_push_the_leg`), `_failed_loudly` and `main`'s catch-all now print a refused command through `_loudly`, not `exc.loudly`. - A printable character prints as it is, so a command with plain paths prints the same block as before. I measured it with a real `execute --allow-empty-leg spec` and a global `pre-receive` hook that refuses every push: - **U+202E.** On `5208e874e`, U+202E in `--local-remote-dir` reaches stderr raw 3 times. On this head it reaches stderr 0 times. The command, the `(in ...)` line and git's `To` line each name it as `\u202e`, six ASCII characters. - **ESC.** ESC behaves the same way. git itself writes ESC as `?` in its `error:` line. - **Byte 0xff.** It was already printed as `\udcff` before this change, because stderr is `backslashreplace`. git's own lines hold U+FFFD for it, the character `run` decodes that byte to. ### N4: a NOTE says when `NEXT:`'s clone command holds a character a terminal does not show #1195 asks for a ruling: refuse such a source, as the README's clone line does, or add a note under `NEXT:`. The code settles it in favour of the note: - **Such a source is writable.** `_refuse_an_unwritable_clone_url` asks `unwritable_reason`. Since #255, that lets through a `Cf` character other than the nine bidi controls. Its docstring in `scripts/shape_materialize.py` says such a character "is invisible, not unwritable", and that a Persian name joins its parts with U+200C. - **The tests pin it.** `tests/test_adopt_template_values.py` lists `Ali<U+200C>Reza`, `a<U+200E>b`, `a<U+200F>b` and `a<U+061C>b` as `WRITABLE`. Refusing the source would reverse #255. - **Spelling breaks pasting.** Spelling the command instead would break `_branch_deletion`'s rule that a printed command pastes as it is. `sh` reads a quoted path, not a spelled one. I measured the old behaviour on `5208e874e`, with a full `execute --local-remote-dir` of a source under `<D>/so<c>urce/Thing`. U+200B, U+200F, U+200C and U+00A0 each pass, and the `split` line spells them. `NEXT:` prints each one raw, once, inside `shlex.quote`'s single quotes. On this head, the clone command is unchanged. When the url is not `str.isprintable`, this line prints just before the `NEXT:` block: ``` NOTE the path in the clone command below holds a character a terminal does not show as itself, so it can read as another path: it is `<D>/so\u200burce/Thing`, as the `split` line names it. ``` A url whose every character is printable gets no NOTE. A source name that ends in a space shows between the quotes, so it gets none either. **Where the NOTE goes differs from the issue's wording; please weigh it.** - The issue says a note *under* `NEXT:`. - #1121 and #1207 each add lines right after the `print` of the `NEXT:` block. With the NOTE printed under the block, this branch conflicted with #1121 (`git merge-tree` exit 1). - Printed just before the block, the NOTE merges cleanly with both. It is also read before the command a person would paste. - Mutant M14 (the NOTE after the block) fails 2 rows, so the placement is pinned. - Moving it under the block means moving one `print` and flipping the row's order check. It would then need a rebase over whichever of #1121 and #1207 lands first. ### The new rows The new file goes from 148 to 188 rows, 40 of them new. `tests/test_adopt_github_exits.py` keeps its 23 rows, with one assertion tightened. - **K1, K2.** `test_a_printable_plan_is_named_as_typed_unless_900_spells_it` runs `check` on the uncovered plan, in the plan's own directory, four ways. `run_adopt` now takes `cwd=` and `over=`. | `--plan` given | Named as | |---|---| | absolute, ending in a space | `` `<D>/plan.yaml ` `` | | `` `plan.yaml` ``, relative, between backticks | `` `\x60plan.yaml\x60` `` | | `plan.yaml`, relative | `plan.yaml` | | `./plan.yaml` | `plan.yaml` | - **K4, K5.** Two new `ROWS` entries. Each runs under `check` and `execute`, with the plan at each of the five path forms. - `plan-wrong-kind-a-placement-plan` expects `kind is 'placement-plan', expected 'adoption-plan'`. - `org-too-long`, with an `org:` of 40 `o`, expects `` `org:` is oooooooooooo...oooooooooooo, 40 characters ``. - The issue read both expected lines from the code. Both rows pass on `5208e874e`, which confirms them. - **K6.** `printable-U+00E9` is added to the remote-dir test. `re<U+00E9>mote` prints as typed, with no backticks. - **K7.** In `tests/test_adopt_github_exits.py`, the GitHub-mode `split` line must now match in full: ` split <12 hex> on adopt/three-repo-shape -> https://github.com/testorg/Northwind.git`. - **N4.** `SPLIT_SOURCES` gains `right-to-left-mark` (U+200F). Every split row checks either that the NOTE comes before `NEXT:`, or that there is no NOTE. - **N3.** Three tests: - `test_a_refused_command_names_the_remote_and_work_dirs_spelled` is the probe above as a test. The character goes in both `--local-remote-dir` and `--work-dir`, for U+202E, ESC, byte 0xff and a plain control. The line feed is left out, because git's own output names the path and the output's line breaks are kept. - `test_a_late_refused_command_is_said_spelled` checks `_failed_loudly`, in process, for all five `CHARACTERS`. - `test_a_refused_command_no_step_said_is_said_spelled` checks `main`'s own block, in process, with `cmd_check` replaced, for all five. ## Proof ### Before and after | Code | The new file (188 rows) and `tests/test_adopt_github_exits.py` (23 rows) | |---|---| | #1177's head `5208e874e` | 12 failed, 199 passed | | This head `73c9f57e8` | 211 passed | - **What fails on `5208e874e`.** The 12 failures are all N3 and N4 rows: - 2 split rows (U+200B and U+200F); - 2 refused-push rows (U+202E and ESC); - 4 `_failed_loudly` rows; - 4 `main` rows. - **The K rows** pass on `5208e874e`, because its code is right there. Each one fails on its mutant (below). - **Two N3 controls** pass on both heads: byte 0xff, because stderr already wrote `\udcff`, and the plain control. ### Mutants All 20 were killed. Each was applied to `adopt-project.py` on this head and restored with `git checkout`. After every restore, the file's sha256 was `163c692aff5b076410bde27740439070f2b9a4c62b4a7dc9510199cb0a7f890a`. - For K1 to K6, the whole new file was run (188 rows). For K7, `tests/test_adopt_github_exits.py` was run too (211 rows). - Under every K mutant, only new rows failed, so the old 148 stayed green. - M1 to M14 ran the N3 or N4 rows. | Mutant | What it does | Rows failed | |---|---|---| | K1 | `check`'s last count line names `--plan` by `isprintable()` alone | 2 of 188 | | K2 | that line names `plan.path.absolute()` | 3 of 188 | | K4 | the PLACEMENT plan's `plan-wrong-kind` names the plan raw | 8 of 188 | | K5 | `adopt-org-too-long` names the plan raw | 8 of 188 | | K6 | the extracted leg line spells every url outside ASCII | 1 of 188 | | K7 | the `split` line names the source's own path | 2 of 211 | | M1 | `_loudly` leaves the arguments raw | 10 of 14 | | M2 | `_loudly` leaves the working directory raw | 10 of 14 | | M3 | `_loudly` leaves the output raw | 8 of 14 | | M4 | the output's line breaks are spelled too | 14 of 14 | | M5 | the arguments are wrapped in backticks | 14 of 14 | | M6 | the leg push goes back to `exc.loudly` | 2 of 14 | | M7 | `_failed_loudly` goes back to `exc.loudly` | 4 of 14 | | M8 | `main` goes back to `exc.loudly` | 4 of 14 | | M9 | `_unacted` keeps every whitespace character | 2 of 14 | | M10 | no NOTE | 2 of 4 | | M11 | a NOTE wherever the `split` line spells the path | 1 of 4 | | M12 | the NOTE names the path raw | 2 of 4 | | M13 | the clone command is spelled, not quoted | 4 of 4 | | M14 | the NOTE prints after the block, not before it | 2 of 4 | ### Gate The gate ran on the scratch merge `37c65a609` of `origin/main` `961640c68` and this head `73c9f57e8`, in the landing gate's environment: | Shards | Tests | Result | |---|---|---| | g0 to g5: the 52 files listed below | 3,250 | 3,246 passed, 4 skipped | | d0 to d7: `tests/test_shape_doctor.py` | 448 | 445 passed, 3 skipped | - The shard lists hold each collected id exactly once. Every shard exited 0. - **The 7 skips are environment skips.** Three `tests/test_adopt_root_symlinks.py` rows need git 2.45 for the reftable backend. One `tests/test_stdout_encoding.py` row and the three doctor rows run only on Windows. - **g1 ran twice.** A host reboot killed its first run part way. It was re-run whole: 542 passed. <details><summary>The 52 files in g0 to g5</summary> `tests/test_adopt_attributes_eol_block.py`, `tests/test_adopt_c_locale_bytes.py`, `tests/test_adopt_clone_template.py`, `tests/test_adopt_contracts_dir.py`, `tests/test_adopt_e2e.py`, `tests/test_adopt_entry_paths.py`, `tests/test_adopt_execute_refusals.py`, `tests/test_adopt_filter_repo_failure.py`, `tests/test_adopt_git_reads.py`, `tests/test_adopt_git_rm_bytes.py`, `tests/test_adopt_github_exits.py`, `tests/test_adopt_late_failure.py`, `tests/test_adopt_leg_keys.py`, `tests/test_adopt_mount_ignored.py`, `tests/test_adopt_pins.py`, `tests/test_adopt_plan_and_remote_paths_spelled.py`, `tests/test_adopt_plan_escapes.py`, `tests/test_adopt_reencoded_files.py`, `tests/test_adopt_relative_submodule_url.py`, `tests/test_adopt_root_manifest.py`, `tests/test_adopt_root_shape_paths.py`, `tests/test_adopt_root_symlinks.py`, `tests/test_adopt_spec_only.py`, `tests/test_adopt_spelled_names.py`, `tests/test_adopt_split_branch.py`, `tests/test_adopt_submodule_url_answers.py`, `tests/test_adopt_submodules.py`, `tests/test_adopt_temp_work_roots.py`, `tests/test_adopt_template_values.py`, `tests/test_adopt_tracking_branch.py`, `tests/test_adopt_unsearchable_source.py`, `tests/test_adopt_values_as_typed.py`, `tests/test_adopt_verify.py`, `tests/test_doctor_next_is_a_command.py`, `tests/test_dry_run_asks_remotes.py`, `tests/test_family_add_refusals.py`, `tests/test_git_output_decoding.py`, `tests/test_github_limits_init_check_org.py`, `tests/test_github_name_limits.py`, `tests/test_non_utf8_name_callers.py`, `tests/test_path_guard_hygiene.py`, `tests/test_refusal_default_remediation.py`, `tests/test_repo_hygiene.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_scaffold_manifest_values.py`, `tests/test_scaffold_reuse_seeded_legs.py`, `tests/test_stdout_encoding.py`, `tests/test_template_values_single_pass.py`, `tests/test_update_shape.py`, `tests/test_update_shape_add.py`, `tests/test_update_shape_merged_away.py` </details> **`main` has moved since the gate's base, and it touched `adopt-project.py`.** - `origin/main` is now `50ce8ab15`. Of the 15 commits since `961640c68`, one touches `adopt-project.py`: `b7edd8816` (#1119, for #1083), with 279 lines added and 78 removed. None touches either test file. - `git merge-tree` of this head with `50ce8ab15` is clean. In the merged `adopt-project.py`: - the file parses; - `_unacted` and `_loudly` are each defined once; - `.loudly(` is called only inside `_loudly`, so #1119 adds no new `exc.loudly(` call. - The gate above did not run on `50ce8ab15`. The landing gate re-tests this head on its own merge. ### Merge-trees (2026-10-23T20:14Z) `git merge-tree --write-tree <X> 73c9f57` exits 0 (clean) for every `<X>` below: - `origin/main` at `50ce8ab15`; - #1177's head, `5208e874e`; - each of the 24 open PRs that touch `adopt-project.py`: #1210, #1207, #1198, #1194, #1190, #1189, #1186, #1180, #1178, #1171, #1170, #1169, #1154, #1153, #1143, #1141, #1125, #1123, #1122, #1121, #1120, #1118, #1106 and #1105. No other open PR touches either test file. #1121, which adds lines after `NEXT:`, has not landed, and it merges cleanly. Each of the 24 was also merged against a scratch merge of `origin/main` and this head: - 21 merge cleanly. - #1169, #1180 and #1190 conflict in `tests/test_adopt_check_throwaway_repos.py`, and #1169 also conflicts in `adopt-project.py`. - Those three conflict with `origin/main` alone, in the same hunks byte for byte: 6 in `adopt-project.py` for #1169, and 13, 8 and 8 in the test file. So this head adds no conflict. ## Not in this PR These are outside #1195, which covers `adopt-project.py`'s `--local-remote-dir`: - `scaffold-project.py`, `update-shape.py`, `scripts/family.py` and `scripts/bump-leg.py` still print `CommandFailed.loudly` as it is. - `_init_refused` prints `git init`'s argument list raw, and those arguments hold the `--work-dir` path. - The `adopt-remote-unreadable` and `source-unclonable` refusals print git's or gh's output raw. - `_unacted` and the NOTE test `str.isprintable`, as `_first_said` does. So the default-ignorable characters that #1171 (#1128) adds to `_shown` get no NOTE, and are not spelled in a REFUSED block. They belong with `_first_said` under #1165. Closes #1195 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Closes #1136
#1169 targets main, after #1119 squash-landed there as
b7edd881, through three merges: #1119's final head9b856443(ce1938a8), then its squashb7edd881(93a561c9), thenorigin/mainat50ce8ab1(fe25bb14, no conflict). The second merge took this branch's side (git checkout --ours) for the two files that conflicted,adopt-project.pyandtests/test_adopt_check_throwaway_repos.py, both #1119's;git diff 9b856443 b7edd881is empty over the whole tree, so that merge changes no file. The first merge had one conflict, in the module docstring oftests/test_adopt_check_throwaway_repos.py, where #1119's #1033 paragraph and this PR's #1136 paragraph stand at the same place: both stay, #1033 first. The restack is proved under "The restack" in Tests. It was left out of v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z. It is now on milestone v1.2.1, and lands under Brett Heap's v1.2.1 merge word in its held slot.What
#910's throwaways (the rehearsal,
_stored_as_written,_AttributesProbe) are made with_inherited_env(), which leaves outGIT_CONFIG_PARAMETERS(a parent git's-c),GIT_ATTR_SOURCE, and anyattr.treeinGIT_CONFIG_COUNT;_stored_as_writtenheld none of the source's objects; and the rehearsal chose the files it asks about withcheck-attr --cached, which reads noattr.tree. Socheckanswered as a git with none of them would, andexecutestopped after both legs existed, or refused a plan its own git would carry out. Measured with git 2.43; "base" isbba168bd(#1119's head when this PR was written), "head" is this branch before its restack (76c9b767, a commit that changes one comment only: with docstrings emptied, its AST is equal to1e958ca6's; the tests, mutants and gate below ran at1e958ca6, but for the one call marked at76c9b767; the restack's head,fe25bb14, carries the same changed lines, as "The restack" in Tests shows):-c core.protectHFS=trueover a source path.g+ U+200C +it/xcheckexit 0plan ok;executeexit 2REFUSED extracting the spec leg(invalid path,unable to checkout working tree); both legs existcheckexit 1plan-path-git-refuses;executeexit 2REFUSED plan-path-git-refuses;remotes/absentGIT_CONFIG_COUNT(control)plan-path-git-refuses; refused;remotes/absentplan ok;adoption verified-c filter.<name>.clean=false,smudge=cat,required=true, over a root.gitattributesthat names the filter for itselfplan ok;executeexit 2 extracting the spec leg (clean filter '<name>' failed); both legs existplan-attributes-reencoded; refused;remotes/absentGIT_CONFIG_COUNT(control)plan-attributes-reencoded; refusedattr.treenaming the empty tree, inGIT_CONFIG_PARAMETERSorGIT_CONFIG_COUNT, over a CRLFnotes.txtthe plan keeps under a root* text eol=lf(r1169's probe p8)checkexit 1plan-file-reencoded;executeexit 2REFUSED plan-file-reencodedbefore any leg, where its own git reads no rule eithercheckexit 0plan ok;executeexit 0adoption verifiedattr.tree(controls)plan ok,adoption verified; none:plan-file-reencoded, refused,remotes/absentattr.treenaming by its object id a tree the source holds on a side branch, whose.gitattributessays/.gitattributes filter=w1136, with that failing filter, over a root*.png binary(probes p2, p7), in either form or a global configcheckexit 0plan okin all three;executemade both legs and stopped in the spec leg,fatal: .gitattributes: clean filter 'w1136' failed, nothing rolled back (measured onb8e63bcfwithcheck's refusal stubbed, in all three)plan-attributes-reencodednamingclean filter 'w1136' failed, in all three;executerefuses before any leg;remotes/absentattr.tree(control)plan ok*.md textin place of*.png binary(r1169b's P7), in each formplan-file-reencodedalone, namingspecs/001-feature/spec.md; parameters and count:plan ok. On25805102:plan-attributes-reencodedand alsoplan-file-reencodednamingspecs/001-feature/spec.md, in all threeplan-attributes-reencodedalone, namingclean filter 'w1136' failed, in all threeattr.treenaming by its object id a side-branch tree whose.gitattributessays* text eol=lf, over a CRLFnotes.txtthat no rule of the source's own reaches, sent to the spec leg (r1169b's P3) or kept under a root*.png binary(P5), in the global, parameters and count formscheckexit 0plan okin all six;executeis not refused before any leg: P3 exit 2REFUSED mounting the legs(the following file has local modifications), P5 exit 1adopt-lostnamingnotes.txt; both legs pushed, nothing rolled back (r1169b's real runs, the same onbba168bdand25805102)checkexit 1plan-file-reencodednamingnotes.txt, in all six;executerefusesplan-file-reencodedbefore any leg;remotes/absentattr.tree(control)plan okGIT_ATTR_SOURCE=bogusplan ok;executeexit 2 extracting the spec leg (fatal: bad --attr-source or GIT_ATTR_SOURCE); both legs existadopt-git-unasked; refused;remotes/absentGIT_ATTR_SOURCE=(empty)plan ok;executeexit 2 extracting the spec leg; both legs existadopt-git-unasked; refusedGIT_ATTR_SOURCE=HEAD, code leg seededplan ok;executeexit 2REFUSED seeding the code leg; both legs existadopt-git-unasked; refusedGIT_ATTR_SOURCE=the source tree's abbreviated oid, code leg seededplan ok;executeexit 2REFUSED pushing the spec leg; both legs existadopt-git-unasked; refusedGIT_ATTR_SOURCE=HEAD, no seeded leg; or a full tree oid, seeded or not (KNOWN LIMIT, below)plan ok;adoption verifiedadopt-git-unasked; refusedGIT_ATTR_SOURCE(control)plan ok; verifiedThe first round (
b8e63bcf) answered theattr.treerows as base does (r1169's SHOULD-1). The second (25805102) still saidplan okfor P3 and P5, as base does, and gave P7 a second, misnamed finding (r1169b's SHOULD-1). Then1e958ca6answers r1169b's ruling X1, with item 4 below (one argument and the two docstrings that explain it, +16/-5 against25805102) and 17 new rows, (j) and (k) in Tests. The head was then76c9b767, which changes comments only: w1169b found that the comment above the rehearsal's attribute read still saidcheck-attr --cached, and it now says the read is made without it (+3/-2 against1e958ca6; with docstrings emptied, its AST is equal to1e958ca6's). The restack that followed makes the headfe25bb14and writes no line of its own. The change isadopt-project.py(+211/-57 againstbba168bd, and the same against main atfe25bb14; no--help, doc or code change), and the tests below:-c,attr.treetoo. The rehearsal (_SplitRehearsal) and_stored_as_writtentake_split_checkout_env(): this process's environment less_NOT_INHERITEDbutGIT_CONFIG_PARAMETERS, so a parent's settings inGIT_CONFIG_PARAMETERSandGIT_CONFIG_COUNTreach them as they stand,attr.treeamong them, as they reachexecute's checkout,git rmandgit add._stored_as_writtennow borrows the source's objects as the rehearsal does (_objects_borrowed, which both use), so a tree's object id names there the tree the split's clones read.GIT_ATTR_SOURCE._attr_source_unasked(source)raises_Unasked, the existingadopt-git-unasked, whatever the value is, the empty one included, with an ending of its own ("executerefuses it too, whatever it names, before it makes either leg. UnsetGIT_ATTR_SOURCE, ... and runcheckagain.") in place of the shared one, which saidexecuterefuses "while git cannot answer there" and offered another--work-dir(r1169's NIT-3). It runs first in_entry_findings'try, before the split's own clone is asked, socheckgives one finding and asks nothing after it, andexecuterefuses before either leg._AttributesProbe.__init__,_stored_as_writtenand_SplitRehearsal.__init__read their environment before_throwawaymakes the directory, so a read that raises leaves nothing behind, as adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119 made_resolvedand_answers._probe_envnow takes(nothing, inherited);_AttributesProbeis its only caller.--cached._SplitRehearsal.reached, which both_rules_reachingcalls use (the files a rule reaches as the source holds its rules, and as the split's root will), askscheck-attrwithout--cached, which reads the index's rules and noattr.tree. A file only a parent'sattr.treereaches is now asked about.Why
-cis handed to the rehearsal and to_stored_as_written.execute's checkout,git rmandgit addrun in the whole environment, so they take a parent's settings; the throwaways must answer as they do, as the same settings inGIT_CONFIG_COUNTalready did. A throwaway's own-cstill wins (git reads a command's own-cafter the parent's and takes a key's last value), so a parent'score.hooksPathorcore.fsmonitorruns nothing there; a row holds that.attr.treeis handed on too (r1169's SHOULD-1; lane 1's ruling: fix (b)). The first round dropped it from both forms, and r1169 measuredcheckanswering otherwise thanexecuteboth ways onb8e63bcf: under the empty treechecksaidplan-file-reencodedwhileexecute's own git read no rule either (probe p8), and under the side treechecksaidplan okwhile the split'sgit addof the root fails (probes p2, p7). A global config'sattr.treereached the rehearsal all along, but_stored_as_writtenheld none of the source's objects, so a tree id named nothing there, and the side tree in a global config wasplan oktoo. Measured with git 2.43 in a commitless throwaway: without the source's objects a full tree id read no rule and an abbreviated one was ignored; borrowing them, both read that tree's rules, as the split's clones do; the empty tree read no rule either way;HEAD, a branch name andboguswere ignored either way. So_stored_as_writtenborrows the source's objects as the rehearsal does, and nothing of a parent's settings is dropped, which retires the first round'sGIT_CONFIG_PARAMETERSreader (_parent_settings,_parameter_entries,_single_quoted), and with it r1169's NIT-2 and NIT-6.check-attris asked without--cached(r1169b's SHOULD-1; ruling X1). r1169b measured with git 2.43, in a repository whose side tree says* text eol=lf: underattr.tree,check-attr --cached text eol -- notes.txtgivesunspecifiedfor both, and without--cachedit givestext: set,eol: lf; the same in a commitless throwaway that borrows the source's objects, and in the parameters form. So a file only that tree's rules reach was left out of the files the rehearsal asks about, andchecksaidplan okwhere the split's checkout andgit add, which do readattr.tree, change the file (P3, P5). Without--cached, git's check-in direction reads a work-tree.gitattributesfirst;_rules_reachingasks before any checkout orplace()writes one, so git falls back to the index, and the answers with no setting do not change (the no-setting controls; the gate below).--cachedwas also the cause of P7's second, misnamed finding: with it restored, (k) fails in all three forms (M1 below), as it does on25805102.GIT_ATTR_SOURCEis refused, not inherited. Measured on 2.43, no throwaway can answer for it. An abbreviated source-tree oid gaveplan okandexecutestopped atpushing the spec legafter both legs existed. A commitless repository that borrows the source's objects, as the rehearsal does, takes that value (check-attrexits 0); one that does not borrow them refuses it (exit 128). So inheriting would still sayplan okwhereexecutestops.HEADresolves in the source yet stopsexecuteatseeding the code leg, so a "bad value" question put to the source would fail too. The existing codeadopt-git-unaskedcarries it, so there is no new code and no doc trio._AttributesProbetakes no parent settings. It models a clone on another machine, which has none of them. It still keepsGIT_CONFIG_COUNT: that predates this PR and is left alone (a known limit only).Tests
At head
1e958ca6(the head was then76c9b767, a comment-only commit that touches no test; the restack'sfe25bb14is under "The restack"). Intests/test_adopt_check_throwaway_repos.py(+630/-5 againstbba168bd, and the same against main atfe25bb14; +674/-5 with row (l) below, atd9224ecf), the section# #1136holds 56 ids, and the existingtest_an_environment_read_that_raises_leaves_no_throwawaygains 3 params: 59 new ids, 17 of them from this round, (j) and (k); row (l), added after r1169d's review, makes 62. Rows that need a feature of git skip, with a reason, where probing git itself shows it is missing (a git that reads noattr.tree; one that holds the U+200C path undercore.protectHFS); the real-executerows skip wheregit filter-repois not installed.core.protectHFS=trueover the U+200C path:plan-path-git-refusesfrom a parent's-c, andexecuterefuses before any leg; with the no-setting control.plan-attributes-reencoded, andexecuterefuses before any leg.test_a_parent_gits_attr_tree_is_read_as_the_split_reads_it: underattr.tree=<empty tree>,plan okin the global, parameters and count forms, andplan-file-reencodedwith no setting.test_execute_reads_a_parent_gits_attr_tree_as_check_does(p8) runs a realexecuteunder the same:adoption verifiedin the three forms, andREFUSED plan-file-reencodedwith no setting,remotes/absent. Each first asks the source's owncheck-attr eol:unspecifiedunder the setting,lfwithout.test_a_parent_gits_attr_tree_is_read_where_the_root_is_stored(global, parameters, count): oneplan-attributes-reencodednamingclean filter 'w1136' failed;test_execute_refuses_a_parent_gits_attr_tree_filter_before_any_leg(the three forms); and the controltest_the_failing_filter_alone_is_read_for_no_file,plan ok. Each first asks the source's owncheck-attr filter -- .gitattributes:w1136under the setting. The source's root says*.png binary, which names none of its files; (k) has*.md textthere.test_a_file_only_a_parent_gits_attr_tree_reaches_is_asked_about(no setting, global, parameters, count; each for a CRLFnotes.txtsent to the spec leg and kept under a root*.png binary; 8 ids):plan-file-reencodednamingnotes.txtunder the setting,plan okwith none.test_execute_refuses_a_file_only_a_parent_gits_attr_tree_reaches(the three forms, both cases; 6 ids):executerefusesplan-file-reencodedbefore any leg, andremotes/is absent. The side tree says* text eol=lf; each row first asks the source's owncheck-attr eol -- notes.txt:lfunder the setting,unspecifiedwithout..gitattributes,checkalso printsplan-file-reencodedfor a file the split stores as its blob, beside the rightplan-attributes-reencoded(#1136) #1192's case, r1169b's P7:test_a_parent_gits_attr_tree_filter_is_one_finding_over_an_own_rule(the three forms; 3 ids): the side tree of (h) over a root*.md text, oneplan-attributes-reencodednamingclean filter 'w1136' failedand noplan-file-reencoded.d9224ecf:test_a_pinned_copy_under_a_parent_gits_attr_tree_unsets_the_trees_rule(the three forms; 3 ids). A side tree says/scripts/*.py filter=w1136upper, an upper-casing clean filter, over a source root that says/scripts/*.py ident, andattr.treenames the tree. Each row first asks the source's owncheck-attr filter ident -- scripts/bootstrap.py:w1136upperandunspecifiedunder the setting. The copies finding (plan-file-reencoded, "store 5 pinned shape copies") then names-filterfor each of the 5 copies and no-ident: the split'sgit addof the copies reads the tree's rule and not the root's. Head: 3 passed, no skip; with M3 below, 3 failed (assert '/scripts/validate-pins.py -filter' in "... -ident..."). r1169d measured it on25805102, which names-identas M3 does: 3 failed; and onbba168bd: the global form failed the same way, the parameters and count forms saidplan ok.core.hooksPathandcore.fsmonitorrun 0 times where the rehearsal and_stored_as_writtenask (global config, parameters and count), with thegit addcontrol in the same row.GIT_ATTR_SOURCEis oneadopt-git-unaskedfinding that names it, for 7 values (bogus, empty,HEADseeded, the abbreviated tree seeded,HEADand the full tree as the known limit, and the empty tree), ending with its own words (ATTR_SOURCE_ENDS, r1169's NIT-3);executerefuses the first 3 before any leg, with the same ending.test_git_attr_source_is_refused_before_the_splits_own_clone_is_askedrecords_split_clone_asked: underGIT_ATTR_SOURCE,executerefusesadopt-git-unasked, the recorder stays empty, the work directory holds nothing, andremotes/is absent._AttributesProbe,_stored_as_writtenand_SplitRehearsal, with_inherited_envand_split_checkout_envboth made to raise.test_the_checkout_questions_take_the_settings_and_no_attr_source:_split_checkout_env()is this process's environment less the six variables ofANOTHER_REPOSITORY(GIT_DIR,GIT_WORK_TREE,GIT_INDEX_FILE,GIT_COMMON_DIR,GIT_OBJECT_DIRECTORY,GIT_ATTR_SOURCE), withGIT_CONFIG_PARAMETERSand theGIT_CONFIG_COUNTentries as they stand,attr.treein each; with noGIT_CONFIG_PARAMETERSthere is none. The first round's 13 parser rows went with_parent_settings. Every setting a row gives is appended after what the environment holds, never overwritten, sotests/test_no_auto_maintenance.pystill holds.Older rows changed in other files (outside the one test file the plan named; the change forces them):
GIT_ATTR_SOURCEparams (HEAD, and the empty tree's oid) fromtest_adopt_nested_attributes.py(two functions) andtest_adopt_reencoded_files.py(one): the head refusesGIT_ATTR_SOURCEfirst, so each foundadopt-git-unaskedwhere the older finding stood. Each docstring points totest_git_attr_source_is_one_finding_that_names_it, which holdsHEADand the empty tree.attr-tree-in-the-command-scoperows, which asked that the count-form empty tree NOT be read and found the file:test_adopt_reencoded_files.py's, nowtest_a_tree_this_machine_names_steers_the_question_as_it_steers_the_split, andtest_adopt_nested_attributes.py's split-question row, nowtest_a_tree_this_machine_names_steers_the_split_question_as_the_split. Each now expectsplan ok, with a no-setting control in the same row that finds the file. Measured on the head tool with a realexecute, under that setting each source givesadoption verified, and with noneexecuterefuses before any leg.test_command_scopes_built_at_the_test.py'sSITESnames the reencoded row by its new name. The nested-question row (test_no_tree_this_machine_names_steers_the_question) is unchanged:_AttributesProbestill takes noattr.tree.Base vs head, 67 ids: the 50 of the earlier rounds (the 42 new then, row (f)'s 3 older params, the clone row (its six variables are now the shared
ANOTHER_REPOSITORY), the 2 flipped rows andSITES' 2) and the 17 of (j) and (k). The head's tests ran over each tool in a scratch clone:bba168bd: 49 failed, 18 passed (101.9 s). Over the 50 ids: 34 failed, 16 passed (58.6 s). Failed: (f) all 6, at the newmonkeypatch.setattr(adopter, "_split_checkout_env", ...), which finds nothing to set there, sobba168bddoes not measure (f) (the first round measured its 3 new params leaving a throwaway behind there, and its 3 older ones passing); (a) and (b), the parameters form, check and execute (4); (c) and p8, parameters and count (4:plan-file-reencoded, refused); p2/p7 in all three forms (3:plan ok) and their execute rows (3: no refusal); (e) 7 + 3; (i); (g), with no_split_checkout_env; the two flipped rows (each found the file). Passed: the clone row; (a) and (b) count, check and execute (4), and the (a) control; (c) and p8, no-setting and global (4); the filter control; (d) all 3;SITES2. Over the 17 ids: 15 failed, 2 passed (the 2 no-setting controls of (j)); P7: globalplan-file-reencodedalone, parameters and countplan ok; P3 and P5:checkplan okin all six, andexecutenot refused (both legs pushed).b8e63bcf(the first round), over the 50 ids: 23 failed, 27 passed (56.0 s). Failed: (c) and p8, parameters and count (4, r1169's p8); p2/p7 and their execute rows (6, r1169's p2 and p7, the global form included); (e)'s 7 on the ending and its 3 execute rows on the same (r1169's NIT-3); (g), whoseGIT_CONFIG_PARAMETERSlackedattr.tree; the two flipped rows. Passed: (f) all 6, and (i), which is a guard there (b8e63bcfasks in this order already; M6 below is what it kills). The 17 were not run onb8e63bcf.25805102: 15 failed, 52 passed (92.9 s). The 50 pass. Of the 17, the same 15 fail and the 2 controls pass: P7 givesplan-attributes-reencodedand alsoplan-file-reencodedin all three forms; P3 and P5:checkplan okin all six, andexecutenot refused (both legs pushed).Path.exists.__qualname__read_quiet.<locals>.quiet, so the shim was active).Mutants at head
1e958ca6: 2 killed of 3, and M3 survived; with row (l), M3 is killed too, 3 of 3. The head was then76c9b767, which changes a comment and no code, and the restack'sfe25bb14writes no line of this PR, so they were not run again. Run in a scratch clone over the 67 ids, without-x; each file was restored in afinally, and its blob was HEAD's after each:--cachedrestored in_SplitRehearsal.reached(both calls): killed, 15 failed (the 15 rows of (j) and (k) with a setting).--cachedfor the first_rules_reachingcall only (the source's own rules): killed, 9 failed (P7 in three forms; P3, the file sent to a leg, in three forms,checkandexecute). P5, a kept file, is still found by the second call.--cachedfor the second call only (the split's root): survived the 67 ids, and it is not equivalent (r1169d's SHOULD-1). Under anattr.treethe first call already reads that tree for every file, soreached | (kept & at_the_root)can only grow, by files asked about needlessly, which the realgit addthen stores as their blobs; with noattr.treethe work tree is empty when_rules_reachingasks, so both forms read the index. But M3 does changeat_the_root, which_copies_findingreads for the unset line of a pinned copy under anattr.tree(r1169c's 33 probes per tool: 12 differ, 21 are byte-equal): with the side tree saying/scripts/*.py filter=...and the source's own root saying nothing or a neutral rule, head names/scripts/validate-pins.py -filterfor the copy and M3 says "a line that keeps the rule offscripts/validate-pins.py"; with the own root saying/scripts/*.py ident, head names-filter, the tree's rule, which the split'sgit addreads, and M3 names-ident, as25805102does, which git does not read there. Row (l) holds it: M3 fails its 3 ids of 3 (measured atfe25bb14plus the row, in a scratch clone, by adding acachedargument to_SplitRehearsal.reached, which puts--cachedin thecheck-attrcall, and passingcached=Trueat theAS_SPLITcall;adopt-project.py's blob was HEAD's after the restore).--cachedonly whereGIT_CONFIG_PARAMETERS, aGIT_CONFIG_KEY_<n>or a global config file namesattr.tree: killed by (k), in[git-config-parameters],[git-config-count]and[global-config]..gitattributeschecked out before_rules_reachingruns, which breaks the preconditionreached's docstring names: not equivalent, and killed by two rows, though it survived the 129 ids of the throwaway file. Over the nested and reencoded files it gave 2 failed, 124 passed:test_adopt_nested_attributes.py::test_an_encoding_the_root_attributes_is_not_in_is_found(assert set() == {'plan-file-reencoded'}: the root'sworking-tree-encoding=UTF-16LEcopy, checked out, gives git no rules) andtest_adopt_reencoded_files.py::test_every_file_is_read_again_whatever_the_clock(.gitattributes ctime).--cachedwhereattr.treeis unset: equivalent. It passed all 255 ids (129, and 126 over the nested and reencoded files). With no attribute source,check-attrwithout--cachedreads a.gitattributesin the work tree first, and the index otherwise; the work tree holds none when_rules_reachingasks (the precondition K7 breaks, which those two rows hold). The only other attribute source,GIT_ATTR_SOURCE, never reaches the rehearsal:_NOT_INHERITEDnames it,_split_checkout_envdrops it (row (g)), and_attr_source_unaskedrefuses it before any throwaway is made (rows (e) and (i)).948d1771, whose tool is25805102's, so with--cachedinreached; since then the tool changes by that argument, two docstrings and, at76c9b767, one comment.The 13 older mutants: 13 killed of 13, no survivor. Run in a scratch clone at
948d1771(the tests there differ from25805102in one row's name, one docstring and theATTR_SOURCEScomment) over the 50 ids with-x; each file was restored in afinally, and its blob was HEAD's after each. Each was killed by the first row named:_attr_source_unaskedafter_split_clone_asked: (i). r1169's M10 (_PARAMETER_SPACE) is moot: that reader is gone.attr.treedropped again (_split_checkout_envas_split_clone_env()): (c)[git-config-count];_objects_borrowedin_stored_as_written: p2/p7[global-config];GIT_CONFIG_PARAMETERSdropped from_split_checkout_env: (a)[git-config-parameters];_throwaway, in_stored_as_written,_SplitRehearsaland_AttributesProbe(three mutants): (f)[stored-as-written],[split-rehearsal],[attributes-probe];GIT_ATTR_SOURCE: (e)[bogus];_attr_source_unaskedcall: (e)[bogus]; an emptyGIT_ATTR_SOURCEallowed: (e)[empty]; no_objects_borrowedin the rehearsal: (a)'s control;_split_checkout_envkeepingGIT_ATTR_SOURCE, and returning the whole environment (two mutants): (g).The gate at
1e958ca6(the head was then76c9b767, a comment-only commit; the one call marked below ran at it; the calls at the restack'sfe25bb14are under "The restack"), in the worktree, CPython 3.12.3, git 2.43, in the probe environment (a stubgh,GH_TOKEN=bogus-f1169d, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBAL=/dev/null,GIT_SSH_COMMAND=false), each call under 9 minutes and at most two at once, node ids passed line by line. The last row ran in a scratch clone at the merge with #1180:test_adopt_check_throwaway_repos.pytest_adopt_nested_attributes.py,test_adopt_reencoded_files.py,test_command_scopes_built_at_the_test.py,test_adopt_printed_sentences.py(it imports from the reencoded file),test_path_guard_hygiene.py,test_no_auto_maintenance.py,test_validator_child_encoding.pytest_no_auto_maintenance.py:222: "the session began with GIT_CONFIG_GLOBAL set, and its file includes that one", which the probe environment'sGIT_CONFIG_GLOBAL=/dev/nullcauses; they do not come from this PRtest_scaffold_leg_path_collisions.pytest_adopt_root_symlinks.pytest_shape_doctor.py, shard 0 of 4 (112 ids)test_shape_doctor.py, shard 1 of 4 (112 ids)test_shape_doctor.py, shard 2 of 4 (112 ids)test_shape_doctor.py, shard 3 of 4 (112 ids)test_adopt_submodule_url_answers.py,test_adopt_manifest_values.py,test_adopt_relative_submodule_url.py(the three that call_entry_findingsdirectly)test_repo_hygiene.pyandtest_path_guard_hygiene.pyat76c9b767, in the same probe environment (GH_TOKEN=bogus-w1169c);test_repo_hygiene.pyis one of the doc trio, and the other two did not run, as no docstring that reaches--helpchangedtest_adopt_partial_clone_source.py(37 ids), on the merge of this head with #1180'sb316f415(Known limits)The doctor's 4 shards are 448 ids (from a fresh collect): 445 passed, 3 skipped.
The gate files named by what the change touches:
adopt-project.py(the doctor shards and the scaffold collisions file), the symbols it names (GIT_ATTR_SOURCE,_inherited_env,_AttributesProbe,_SplitRehearsal,plan-path-git-refuses: agit grepfinds only the files above), the three files that call_entry_findingsdirectly, before which_attr_source_unaskednow runs, the file that imports from a changed test file, and the hygiene and config gates. No doc, code or--helptext changed, so the doc trio did not run.The restack, at
fe25bb14(three merges and no line written; w1169e, Claude Sonnet 5.5). The real base isbba168bd(git merge-base 76c9b767 e0a98f13, wheree0a98f13is a later head of #1119, betweenbba168bdand its final head9b856443); the old head is76c9b767.git diff ce1938a8 93a561c9is empty, over adopt-project.py: check asks the split's own clone of the source in the work directory, so a clone this git will not make is adopt-git-unasked before any leg, and _resolved and _answers make their repositories as check's other throwaways do (#1083) #1119's four files (adopt-project.py,tests/test_adopt_check_throwaway_repos.py,tests/test_adopt_reencoded_files.py,tests/test_adopt_root_symlinks.py) and every other.git diff --name-only origin/main fe25bb14lists the same 5 files asgit diff --name-only bba168bd 76c9b767:adopt-project.py,tests/test_adopt_check_throwaway_repos.py,tests/test_adopt_nested_attributes.py,tests/test_adopt_reencoded_files.py,tests/test_command_scopes_built_at_the_test.py.--numstatis the same too: +211/-57, +630/-5, +26/-16, +24/-13, +2/-1.git diff -U0 origin/main fe25bb14and ofgit diff -U0 bba168bd 76c9b767are equal (the+and-lines, with the+++and---headers left out): 985 lines each, the same sha256.fe25bb14, in the probe environment (a stubgh,GH_TOKEN=bogus-w1169e, an emptyGH_CONFIG_DIR,GIT_CONFIG_GLOBAL=/dev/null,GIT_SSH_COMMAND=false), two calls at once, each under 9 minutes:test_adopt_check_throwaway_repos.py,test_adopt_nested_attributes.py,test_adopt_reencoded_files.py,test_command_scopes_built_at_the_test.pytest_on_a_disk_that_ignores_case_each_twin_is_held_or_named, "needs a disk that ignores case" (a row #1119 added; this bench's disk keeps case); it does not come from this PRtest_no_auto_maintenance.pyGIT_CONFIG_GLOBALones the gate above explainsThe doctor's shards,
test_scaffold_leg_path_collisions.py,test_adopt_root_symlinks.pyand the hygiene files did not run again atfe25bb14: their calls are above, at1e958ca6. Row (l) then makes the headd9224ecf, one commit onfe25bb14: +44/-0 intests/test_adopt_check_throwaway_repos.pyand no other file; its 3 ids ran atfe25bb14plus the row (and under M3, below), and R1 did not run again with it.The full suite did not run here, and CI's CPython 3.14 did not run (this bench has 3.12.3 only; the 3.14-like shim ran the 67 ids). A grep of the new rows finds no
ast.dump,repr(or Python version test; the git answers they compare with are read at run time (check-attr,git addin a repository of their own).Known limits
GIT_ATTR_SOURCEis refused too (lane 1's ruling: accepted).HEADwith no seeded code leg, and a full tree oid, tookexecutetoadoption verifiedon base (rows (e) hold them); nowchecksaysadopt-git-unaskedandexecuterefuses. It is the only rule with noplan okwhereexecutestops, for a variable the split has no use for; the way out is to run without it, which the finding says (and that agit --attr-sourcerunning this tool also sets it)._AttributesProbekeepsGIT_CONFIG_COUNT(lane 1's ruling: a known limit only). It models a clone on another machine, yet still readsGIT_CONFIG_COUNT; that predates adopt-project.py: #910's throwaways leave out a parent's -c settings and GIT_ATTR_SOURCE, so check says plan ok where execute stops after both legs, and three read the environment after making the throwaway (follow-up of #1083) #1136 and is not changed here.attr.tree.HEADor a branch names nothing in a throwaway, which holds no ref, so git reads the files' own rules there, as with no setting (measured with git 2.43). In the split's clonesHEAD, a tag and the split's own branch resolve; another source branch is held there only asorigin/<name>, soattr.tree=<that branch>names nothing there either (r1169b's NIT-1: measured in a plain local clone,sideread no rule andorigin/sideread them;HEADand a tag were not run through the tool). An object id, the empty tree included, is read as the split reads it (rows (c), (h), (j), (k)).attr.tree,plan-file-reencodedstill ends "Commit it in the source as git stores it ..., or change the rule that reaches it", where the rule is in the treeattr.treenames and the source holds none for the file (r1169b's NIT-2). The finding is right; the advice does not name the setting. Under a parent'sattr.treethe copies finding (plan-file-reencoded, "store 5 pinned shape copies") names the right attribute but puts the lines in the source's root.gitattributes, which git does not read there; following it gives the same finding again (r1169d's NIT-1: with the own root holding*.png binaryplus the five/scripts/<copy> -filterlines the finding asks for, and the side tree's filter rule read throughattr.tree, head givesexit=1with the same finding, asking for the same five lines, in all three forms; with the same rule in the own root, followed by the five lines, and noattr.tree,plan ok).25805102loops the same way, with the generic line, so it is not new here. Row (l) checks only which attribute is named, so it holds after any rewording of this ending. Not changed here..gitattributes, which says/.gitattributes filter=w1136and*.md text(either order), the failing filter in the global config,GIT_CONFIG_PARAMETERSorGIT_CONFIG_COUNT, and noattr.tree,checksays the rightplan-attributes-reencodedand alsoplan-file-reencodednamingspecs/001-feature/spec.md, a file the split stores as its blob. Measured on this head and on25805102, in all six (three forms, two orders); onbba168bdthe global and count forms gave the same two findings, and the parameters formplan ok. With the filter alone,plan-attributes-reencodedcomes alone everywhere (plan okonbba168bd's parameters form). The same comes when both rules are in the side treeattr.treenames (r1169d, measured at76c9b767with git 2.43: the source's own root says*.png binary, the side tree says/.gitattributes filter=w1136and*.md text, andattr.treenames that tree with the failing filter): in each of the three forms, head givesexit=1withplan-attributes-reencodedand alsoplan-file-reencodednamingspecs/001-feature/spec.md;25805102givesplan-attributes-reencodedalone;bba168bdgivesexit=0,plan ok. The control, the tree's*.md textalone with no filter, isplan okon head,25805102andbba168bdin all three forms, sospec.md(# The feature, LF only) is stored as its blob and the second finding names a file that is not reencoded. This case comes with ruling X1: the read without--cachednow reachesspec.mdthrough the tree's*.md text, so the file is asked about, where base saidplan ok. Either waycheckrefuses with the right first finding, andexecuterefuses before any leg, so it is no falseplan ok. The cause is traced (r1169c's expB, git 2.43): under the failing required filter, the rehearsal'sasked.git(["update-index", "-q", "--refresh"], TAKEN), whose exit code is never read, exits 128 withfatal: .gitattributes: clean filter 'w1136' failed; thediff-files --name-onlyafter it exits 0 and lists.gitattributes specs/spec.md, the files it did not refresh; with no filter, the refresh exits 0 anddiff-fileslists nothing. The own-rules case predates this PR, which only lets its parameters form reach it; the side-tree case does not predate it, because this PR's read without--cachedbrings it in. adopt-project.py: under a failing clean filter that a rule names for.gitattributes,checkalso printsplan-file-reencodedfor a file the split stores as its blob, beside the rightplan-attributes-reencoded(#1136) #1192'sattr.treecase (P7: the filter in the side tree and*.md textin the source's own root) is the two-places case, not this one:--cachedmade the check read the own root's*.md text, which git does not read underattr.tree, sospec.mdwas asked about and the refresh's list named it; without--cachedit is not asked about, and the one finding stays (row (k)).org/reposource not cloned yet (r1169's NIT-4).gh repo clonechecks out in the whole environment, so underGIT_ATTR_SOURCE=bogusor an empty value the clone is refused assource-unclonable, whose remediation namesgh auth status, before_entry_findingsasks anything;HEADclones (r1169's probe p5, with plaingit clone; the tool path read, not run). It is no falseplan ok.checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180 (fix/1145-partial-clone-source-no-fetch) was also stacked onbba168bd, at headb316f415; it has been restacked onto main since, and its head isa0e8cded. Before the restacks:git merge-tree --write-tree --merge-base=bba168bd 1e958ca6 b316f415exits 0 with tree1a9ec825(no conflict: adopt-project.py:checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180 touchesadopt-project.py,docs/cli.mdand its own test file). On that tree, in a scratch clone, this PR's 67 ids and adopt-project.py:checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180'stest_adopt_partial_clone_source.py(37 ids) gave 104 passed (115.0 s) (r1169b's NIT-3). The head was then76c9b767: the samegit merge-treecommand with it in place of1e958ca6exits 0 with treeee59fb02, which differs from1a9ec825only in the one comment ofadopt-project.pythat this commit changes (git diff --statof the two trees: 1 file, +2/-3); the 104 were not run again on it. After both restacks, with both branches on main50ce8ab1:git merge-tree --write-tree fe25bb14 a0e8cdedexits 0 with treeac442d4c(no conflict: adopt-project.py:checkandexecuterun withGIT_NO_LAZY_FETCH=1and refuse a partial-clone source that lacks an object its branches, tags or HEAD reach assource-partial-clone, so neither fetches into the source any more (#1145) #1180 touchesadopt-project.py,docs/cli.mdandtests/test_adopt_partial_clone_source.py); the 104 were not run again on it either.Not in this PR
_AttributesProbechange for a parent's settings (it models another machine), and no change to_split_clone_env, which hands the split's clone the settings whole and was only reworded in one docstring sentence.GIT_ATTR_SOURCEinto any throwaway, and no new code, doc,--helptext or doc-trio row.execute's own commands: they already run in the whole environment.attr.treenames, which this PR's read without--cachedbrings in).attr.tree(the limit above).Lane: openreposhape-1 (openRepoShape-1)
🤖 Generated with Claude Code
Summary by Sourcery
Make adoption planning match execution under inherited Git configuration and refuse unsupported attribute-source settings before any repository work begins.
Bug Fixes:
checkaccurately model the parent Git settings and attribute trees used byexecute, preventing plans that would fail during checkout or staging.GIT_ATTR_SOURCEsetting before asking further questions or creating adoption legs.Enhancements:
attr.treeobject IDs.Tests:
attr.tree,GIT_ATTR_SOURCE, environment-read ordering, and early execution refusal behavior.