Skip to content

adopt-project.py: check's compare for the mount's git rm skips the .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

Draft
brettheap wants to merge 6 commits into
mainfrom
fix/1192-refresh-exit-read

Conversation

@brettheap

@brettheap brettheap commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1192

This branch starts from main at 93d29314, #1169's squash, as one more commit series on top of it: e0105903 (the change and 16 rows), af48d8de (2 more rows, for the mutants) and 18c2aaef (the stop the new finding names, and its execute row; see "Why" for the run that made it change), then the review round's 845666aa, 898e9a5e and ad70474d (see "Review round"). The 18 rows, mutants M1-M10 and the gate under "Tests" ran at 18c2aaef; the review round's rows, mutants and gate ran at ad70474d.

Outside v1.2.0 by Brett Heap's ruling of 2026-10-21T10:32:04Z

What

check's rehearsal asks 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. Measured on base 93d29314 with git 2.43, in-process, wrapping _SplitRehearsal.git, in each of the three forms:

  • update-index -q --refresh: exit 128, fatal: .gitattributes: clean filter 'w1136' failed;
  • diff-files --name-only -z: exit 0, .gitattributes specs/001-feature/spec.md;
  • _reencoded_files_findings: one plan-file-reencoded.

At head (measured at e0105903, whose _reencoded_files_findings is 18c2aaef's: the later commits change one constant the finding names, comments and rows), the same probe: update-index -z --assume-unchanged --stdin exit 0, the refresh exit 0, diff-files lists nothing, and _reencoded_files_findings gives nothing.

Measured with check, git 2.43, CPython 3.12.3; "base" is 93d29314, "head" is 18c2aaef. "The filter" is filter.w1136 with clean=false, smudge=cat, required=true. "The three forms" are a global config, GIT_CONFIG_PARAMETERS and GIT_CONFIG_COUNT.

Case Base Head
#1192's case: the source's root says *.png binary; attr.tree names, by its object id, a side-branch tree whose .gitattributes says /.gitattributes filter=w1136 and *.md text; the filter; in each of the three forms exit 1: plan-attributes-reencoded (clean filter 'w1136' failed) and plan-file-reencoded "the plan sends 1 file to a leg or drops it ... specs/001-feature/spec.md", a file the split stores as its blob exit 1: plan-attributes-reencoded alone
the same, with specs/001-feature/spec.md committed with CRLF (control: the compare runs to its end and a real file is named) plan-attributes-reencoded and plan-file-reencoded naming the spec same
the tree's *.md text alone, no filter (control) plan ok same
the root says /notes.txt filter=w1136, notes.txt sent to the spec leg, the code leg or dropped; the filter in GIT_CONFIG_PARAMETERS plan-file-reencoded "the plan sends 1 file ... would store it as other bytes than its blob ...: notes.txt", from the list of files the dead compare never reached plan-file-reencoded: "this machine's git refuses to compare what the plan sends to a leg or drops with its blob, as the mount's git rm does (git said fatal: notes.txt: clean filter 'w1136' failed), so execute would stop after the leg repositories exist, 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. Change the rule or the setting git names, and re-run plan." (head at ad70474d)
the same, execute REFUSED plan-file-reencoded before any leg, with base's wording REFUSED plan-file-reencoded before any leg, with head's
the same, execute with check's refusal patched out (the measurement behind the stop it names) both leg repositories made; stopped REFUSED extracting the spec leg at git checkout -q -B adopt/three-repo-shape <commit>, fatal: notes.txt: clean filter 'w1136' failed, in 47 of 48 runs here, wherever the plan sent the file; once REFUSED extracting the code leg, the same way, "and the spec leg is pushed"; with the filter kept out of both extractions, REFUSED cloning the source for the split at the same git checkout -q -B, "and both legs are pushed", in 24 of 24 (r1253) and 24 of 24 here same: execute is not changed
the root keeps a FILE shape, where shape/.gitattributes goes, and says * text eol=lf and /notes.txt filter=w1136; notes.txt sent to the spec leg (the rehearsal cannot write the split's root) plan-root-blocks-shape-copy and "the plan sends 4 files to a leg or drops them", three of them LF files the compare never reached plan-root-blocks-shape-copy and the compare's refusal
the root says *.md filter=w1136 (every Markdown file) plan-file-reencoded naming the spec from the stale list, then the store's refusal (README.md), then the copies' (AGENTS-shape.md) the store's refusal, the compare's (the spec), the copies', in the groups' order
a CRLF docs/.gitattributes sent to the spec leg under the root's * text eol=lf (control: only the rules git rm leaves are skipped) plan-file-reencoded naming docs/.gitattributes same

Why

  • The compare asks what git rm asks. The mount's git rm -r -q -- <paths> compares only what it removes. Each .gitattributes it leaves now stays in TAKEN, so the rules are read as before, and is marked --assume-unchanged, which update-index --refresh and diff-files both skip. Holding them out of the index instead would leave git only the work tree's copies to read the rules from, where today it falls back to the index's; the bit keeps the index as it was and changes only the compare. Their compare result was never used (taken keeps only paths in moved), so the bit changes only that their own filter no longer stops the compare. A .gitattributes the plan sends to a leg or drops, which a rule reaches, is in moved and still compared (the docs/.gitattributes control; mutant M2).
  • The exit code is read. -q makes a file that needs an update no error, so a nonzero exit is git refusing the compare (a filter that fails on the way in). Then its list is not read, and the finding is git's last line, as _refused_finding gives every refusal of the rehearsal; COMPARE_REFUSED is the step. This mirrors the split's side, where a refused git add -u already gives stored = [] and STORE_REFUSED. It is also why the brief's 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 (the notes.txt rows), base refused with the stale list, execute with the check patched out made both legs and stopped extracting one, and naming nothing would have made it a false plan ok.
  • Where the root's own rules fail, D5(a) names it. The root's kept .gitattributes is adopt-project.py: a nested .gitattributes setting eol or -text for a pinned copy outranks #471's root LF block, so Windows writes CRLF #530's D5(a) question (_stored_as_written, plan-attributes-reencoded), and the split's side already leaves it out of STORED for that reason; a kept nested .gitattributes whose filter fails is named by the split's git add -u (STORE_REFUSED; the review round's row, a folder named in UTF-8 or not, and mutants R2 and R9). So skipping them in the compare loses no finding.
  • The stop it names. The first round said "at its first clone of the source". A row that ran execute with the check patched out stopped extracting the spec leg in 47 of 48 runs here and once extracting the code leg, after the spec leg was pushed: that time the spec leg's git checkout -B trusted the file's stat data. Which step reads the file again is the clock's to decide (READ_AGAIN), so STOPS_AT_A_READ names the steps in order, and that row now asks only that execute refuses before any leg. Each leg's extraction, and then the split's own clone once both legs are pushed (cloning the source for the split), clones the whole source and runs git checkout -q -B in it; the mount's git rm comes after all three. The first round of this text left the split's clone out (r1253's SHOULD-1): with the filter kept out of both extractions, execute stopped there in 24 of 24 runs. So the text names four steps: 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; and a row now runs execute with the check patched out and asks that it stops at one of them, none in particular.
  • Order. The refusals follow the groups' order: the store's (what the root keeps), the compare's (what goes to a leg or is dropped), the copies'.
  • Every other exit code on this path is read. In _reencoded_files_findings and _SplitRehearsal, each git command goes through asked (raises _Unasked) or refused (returns git's line): refused_paths, _rules_reaching's read-tree, update-index, hash-object and check-attr, hold, checkout, diff-files, add -u, staged, and _copies_stored's add -f and hash-object; _throwaway_made and _objects_borrowed check theirs. The refresh was the one raw asked.git(...) call; none is left outside the class.

Tests

The 18 new rows (tests/test_adopt_check_throwaway_repos.py, section "#1192: the mount's compare, under a filter that fails"), base against head, in the probe environment, node ids passed one per argument:

Row Ids Base 93d29314 Head 18c2aaef
test_a_side_trees_failing_filter_over_its_text_rule_is_one_finding (the three forms) 3 3 failed: two findings 3 passed
test_a_file_the_compare_reaches_is_still_named_beside_that_filter (control, three forms) 3 3 passed 3 passed
test_the_side_trees_text_rule_alone_is_plan_ok (control, three forms) 3 3 passed 3 passed
test_a_compare_git_refuses_is_that_finding_in_gits_words (spec, code, drop) 3 3 failed: the stale list's wording 3 passed
test_execute_refuses_a_compare_git_refuses_before_any_leg (spec, code, drop) 3 3 failed: refused with base's wording 3 passed
test_a_compare_git_refuses_is_named_where_the_root_is_not_laid_out 1 failed: "sends 4 files" passed
test_a_gitattributes_the_mount_removes_is_still_compared (control) 1 passed passed
test_each_refusal_is_named_in_the_groups_order 1 failed passed
total 18 11 failed, 7 passed (26.40 s) 18 passed (25.30 s)

The base column ran in a scratch clone at 18c2aaef with adopt-project.py replaced by 93d29314's (blob 5e8cfd75), and restored to 18c2aaef's (blob 698ce8e5) afterwards. The 7 that pass on base are the controls, as a control must.

Mutants: 10 killed of 10, no survivor. Run in that scratch clone at 18c2aaef over the 18 ids; each mutant replaced one exact text of adopt-project.py (asserted to occur once), and the file was restored in a finally; after each, git hash-object adopt-project.py was 698ce8e5, equal to git rev-parse HEAD:adopt-project.py.

Mutant Change Killed by (failed ids)
M1 no --assume-unchanged call 6: #1192's case and the CRLF control, three forms each
M2 every rule marked, the moved ones too 1: test_a_gitattributes_the_mount_removes_is_still_compared
M3 the refresh's exit code unread (asked.git, compare_said = None) 8: the compare refusal rows, execute rows, the no-root row, the order row
M4 diff-files' list read after a refused compare 7
M5 no compare refusal among the findings 7
M6 no compare refusal where the root is not laid out 1: test_a_compare_git_refuses_is_named_where_the_root_is_not_laid_out
M7 the compare's stop said as STOPS_AT_A_CLONE 8
M8 the compare's refusal before the store's 1: test_each_refusal_is_named_in_the_groups_order
M9 the refresh without -q 4: the CRLF control (three forms) and the docs/.gitattributes control
M10 --really-refresh, which ignores the bit 6: #1192's case and the CRLF control

The same ten (M7 then against STOPS_AT_THE_SPLIT) also killed 10 of 10 at af48d8de, over that commit's 18 ids.

The gate at 18c2aaef, in the worktree, CPython 3.12.3, git 2.43, in the probe environment (a stub gh first on PATH, GH_TOKEN=bogus-w1192, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, EMAIL set to the task's .invalid address, no LANES_LANE), each call under 9 minutes, at most two at once:

Call Files Result
G1 test_adopt_check_throwaway_repos.py 169 passed, 1 skipped (185.5 s); the skip is the row that needs a disk that ignores case
G2 test_adopt_reencoded_files.py 51 passed (79.4 s)
G3 test_adopt_single_repository_record.py 39 passed (16.6 s)
G4 test_scaffold_leg_path_collisions.py 104 passed (4.2 s)
G5 test_path_guard_hygiene.py, test_no_auto_maintenance.py 179 passed, 2 skipped (26.2 s); both skips are test_no_auto_maintenance.py:222, "the session began with GIT_CONFIG_GLOBAL set, and its file includes that one", which the probe environment's GIT_CONFIG_GLOBAL=/dev/null causes
G6 the doc trio: test_agents_md_codes.py, test_cli_reference.py, test_repo_hygiene.py 171 passed (36.9 s)
D0 test_shape_doctor.py, shard 0 of 4 (113 ids) 112 passed, 1 skipped (178.9 s), the PowerShell skip: "the PowerShell line is printed, and powershell.exe exists, only on a Windows host"
D1 test_shape_doctor.py, shard 1 of 4 (112 ids) 111 passed, 1 skipped (163.3 s), the same skip
D2 test_shape_doctor.py, shard 2 of 4 (112 ids) 112 passed (172.8 s)
D3 test_shape_doctor.py, shard 3 of 4 (112 ids) 111 passed, 1 skipped (171.5 s), the same skip
G7 test_adopt_nested_attributes.py (beyond the brief: it asks D5(a) and the rehearsal) 75 passed (77.3 s)
G8 test_adopt_printed_sentences.py (beyond the brief: its STOPS rows run execute with the check patched out) 54 passed (36.9 s), no skip

Earlier rounds of the same calls, before the stop text changed: at e0105903, G1 167 passed, 1 skipped (183.8 s), G2 51 passed, G3 39 passed; at af48d8de, G4 104 passed, G5 179 passed, 2 skipped, G6 171 passed, and the four shards 112 passed, 1 skipped; 111 passed, 1 skipped; 112 passed; 111 passed, 1 skipped.

Review round (r1253: two SHOULDs, two NITs)

r1253 reviewed 18c2aaef (issuecomment-6723519114): no MUST, two SHOULDs, two NITs. Three commits answer it, each pushed with an explicit refspec and read back with git ls-remote. Main is not merged here: lane 1 refreshes the branch at landing.

  • 845666aa: SHOULD-1 and NIT-1, in STOPS_AT_A_READ and its comment block in adopt-project.py, and compare_refused() in the test file.
  • 898e9a5e: SHOULD-2's row.
  • ad70474d: SHOULD-1's optional row.

SHOULD-1: the stop left out "cloning the source for the split". Between the extractions and the mount, the split's own clone runs the same git clone and git checkout -q -B an extraction runs. With the filter kept out of both extractions, r1253's probe stopped there in 24 of 24 runs.

  • The fix: STOPS_AT_A_READ now says "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". Its comment says each extraction, and then the split's clone, runs that checkout, and that the mount's git rm comes after all three. compare_refused(), the "What" table, Why and Known limits follow.
  • The optional row is in: test_execute_stops_at_a_step_the_refused_compare_names runs execute in-process with _refuse_what_check_finds patched out, for notes.txt sent to the spec leg, the code leg or dropped. It has two forms:
    • the extractions as they run;
    • as if each extraction trusted the file's stat data: the filter is out of the environment while _extract_leg runs, as in r1253's probe.
  • What it asks: no step in particular. execute must exit 2 at a step the finding names, with git's fatal: notes.txt: clean filter 'w1136' failed, and its notice must say the legs are pushed that the finding names for that step ("neither leg", "the spec leg", "both legs"). In the second form, both legs must be pushed.
  • Why it is reliable: where no step read the file again, execute exits 0 and the row skips, as the finding's own limit says execute may go on. Any other exit fails the row.
  • Measured here with git 2.43, over 72 runs at ad70474d's tool (the row run 8 times per id):
    • as they run: 47 stopped extracting the spec leg, and 1 extracting the code leg once the spec leg was pushed;
    • as if trusting: 24 of 24 stopped cloning the source for the split, both legs pushed;
    • all 72 passed, and none skipped.
    • A row that named one step would have failed the code-leg run. That is why the first round's row was cut back to "refuses before any leg".

SHOULD-2: no row covered a kept nested .gitattributes, or a name that is not UTF-8.

  • The row: test_a_kept_nested_gitattributes_whose_filter_fails_is_the_stores_alone keeps .github/<folder>/.gitattributes in the assembly root. Three ids:
    • the-roots-rule: the root says .github/*/.gitattributes filter=w1136 and *.md text; <folder> is kept;
    • the-roots-rule-not-utf-8: the same, with <folder> caf and the byte 0xE9. plan asks about it as ".github/caf\udce9/" with leg: null, answered root;
    • its-own-rule, r1253's kept-own variant: the kept file says .gitattributes filter=w1136, and the root says *.md text.
  • What it asks: codes(result) == [REENCODED_FILES], and that the one finding is the split's store's refusal: the whole line for the UTF-8 names, and every word but the folder's name for the other (NIT-2).
  • On 93d29314 each id gave a second finding, a false plan-file-reencoded naming specs/001-feature/spec.md: a second form of adopt-project.py: under a failing clean filter that a rule names for .gitattributes, check also prints plan-file-reencoded for a file the split stores as its blob, beside the right plan-attributes-reencoded (#1136) #1192, which the change fixed but no row held.
  • It kills R2 and R9 (below).
  • The id that is not UTF-8 skips where such a name cannot be made (skip_where_a_name_cannot_be_not_utf_8), and its folder is built with chr(0xDCE9), so the module imports on Windows.

NIT-1: STOPS_AT_A_CLONE's comment said both legs are extracted "before either is pushed". It now says the refusal comes extracting the spec leg, before either is pushed, since each leg is extracted and then pushed in turn, the spec leg first. The Known limits bullet and the "Not in this PR" line about it are gone.

NIT-2: no code change. It is in Known limits as a follow-up candidate, with follow-up text.

The 9 new ids, base against head. They ran in a scratch clone at ad70474d, with adopt-project.py swapped to the named commit's. After each run it was restored to blob 5778d0e3, equal to git rev-parse HEAD:adopt-project.py.

Row Ids Base 93d29314 (blob 5e8cfd75) 18c2aaef, before this round (blob 698ce8e5) Head ad70474d
kept nested .gitattributes 3 3 failed: a second finding, specs/001-feature/spec.md 3 passed 3 passed
execute stops, extractions as they run 3 3 failed: base's finding is the stale list's, which names no step 3 passed 3 passed
execute stops, as if trusting the stat 3 3 failed: the same 3 failed: the finding left out the split's clone 3 passed
total 9 9 failed (20.58 s) 3 failed, 6 passed (20.68 s) 9 passed (21.42 s)

The kept row passes at 18c2aaef because the marking it holds was already there: it is the coverage r1253 asked for, and R2 and R9 show it holds the marking.

Mutants: 11 killed of 11, no survivor. They ran in that scratch clone at ad70474d over the #1192 section's 27 ids. Each replaced one exact text of adopt-project.py (asserted to occur once), and the file was restored in a finally. After each, git hash-object adopt-project.py was 5778d0e3, equal to git rev-parse HEAD:adopt-project.py.

  • T1-T7 mutate the round's changed lines, the text of STOPS_AT_A_READ.
  • R2, R5 and R9 are r1253's, run again; R2 and R9 survived every row at 18c2aaef.
  • K1 is a control for the marking.
Mutant Change Ids failed The new rows among them
T1 18c2aaef's text: the split's own clone left out 11 the 3 trusting execute ids
T2 the split's clone and the mount without "once both legs are pushed" 11 the 3 trusting execute ids
T3 the mount left out 8 none; the exact-text rows kill it
T4 the code leg's extraction left out 8 none; the exact-text rows kill it
T5 the spec leg's extraction left out 10 2 of the 3 execute ids as they run (code, drop). The spec one passed. Under T5 it can pass only where execute stopped at a later step than the spec leg's extraction, one T5 still names, so in that run the clock passed the spec leg by, as it did once in 48 runs above. A row that asks for no step in particular must let that pass
T6 the legs' order swapped 11 the 3 execute ids as they run
T7 the split's clone said once the spec leg alone is pushed 11 the 3 trusting execute ids
R2 only the root's own .gitattributes marked 3 the 3 kept ids
R5 the compare's stop said as STOPS_AT_THE_SPLIT 14 the 6 execute ids
R9 the marked paths encoded as strict UTF-8, not _nul 1 the-roots-rule-not-utf-8
K1 the nested rules marked, the root's own not 6 none: #1192's case and its CRLF control, three forms each

"The exact-text rows" are the 8 ids that hold compare_refused() whole: the compare refused in git's words (3), execute refuses before any leg (3), the root not laid out (1) and the groups' order (1). Every T mutant fails those 8.

The gate at ad70474d, in the worktree, CPython 3.12.3, git 2.43, in the probe environment (as under "Tests", with GH_TOKEN=bogus-w1253f), each call under 9 minutes, at most two at once:

Call Files Result
S the #1192 section, 10 functions 27 passed (61.3 s)
G1 test_adopt_check_throwaway_repos.py 178 passed, 1 skipped (303.8 s); the skip is the row that needs a disk that ignores case
G2 test_adopt_reencoded_files.py, test_adopt_nested_attributes.py 126 passed (242.3 s)
G3 test_adopt_printed_sentences.py, test_adopt_single_repository_record.py 93 passed (84.8 s)
G4 test_scaffold_leg_path_collisions.py 104 passed (6.6 s)
G5 test_path_guard_hygiene.py, test_no_auto_maintenance.py 179 passed, 2 skipped (27.8 s); both skips are test_no_auto_maintenance.py:222, caused by the probe environment's GIT_CONFIG_GLOBAL=/dev/null
G6 the doc trio 171 passed (43.6 s)
D0 test_shape_doctor.py, shard 0 of 4 (113 ids) 112 passed, 1 skipped (196.4 s), the Windows-only PowerShell skip
D1 shard 1 of 4 (112 ids) 111 passed, 1 skipped (179.3 s), the same skip
D2 shard 2 of 4 (112 ids) 112 passed (201.1 s)
D3 shard 3 of 4 (112 ids) 111 passed, 1 skipped (198.0 s), the same skip

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 (STOPS_AT_A_READ). Measured here with the check patched out: the spec leg's extraction in 47 of 48 runs, the code leg's, after the spec leg was pushed, in one, and again 47 and 1 of 48 at ad70474d; with the filter kept out of both extractions, the split's own clone in 24 of 24 (r1253) and 24 of 24 here. 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; CI's git was not measured here.
  • 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 in the review round's row git's words name the folder as caf and U+FFFD where _spelled's escape (adopt-project.py: a kept or dropped name reaches the split commit and the pull request body unspelled, and _spelled, _shown and plan's report print text that reads as another name, or raw #900) would print caf\udce9; the row holds every word of that finding but the folder's name. It reaches every refusal the rehearsal names (checkout, store, copy, and now compare). 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 \udcNN escape (measured on adopt-project.py: check's compare for the mount's git rm skips the .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's branch with git 2.43: fatal: .github/caf<U+FFFD>/.gitattributes: clean filter 'w1136' failed)".

Not in this PR

  • No change to D5(a)'s question (_stored_as_written), the split's side (git add -u), the pinned copies' question, or execute's own commands.
  • No change to what TAKEN holds, or to which files are asked about.
  • No new code, doc, --help text or doc-trio row: no doc names plan-file-reencoded's sentences (git grep -n 'file-reencoded' -- ':!tests' ':!adopt-project.py' is empty).
  • No change to how a refusal decodes git's words (the U+FFFD limit above).

Lane: openreposhape-1 (openRepoShape-1)

🤖 Generated with Claude Code

Brett Heap and others added 3 commits November 8, 2026 14:23
…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)

The rehearsal's `update-index -q --refresh` over its `TAKEN` index ran the
clean filter of every `.gitattributes` it held, the root's own kept one
too, which the mount's `git rm` never compares. Under a required clean
filter that fails for that file (a side tree `attr.tree` names saying
`/.gitattributes filter=w1136` and `*.md text`, in each of the three
configuration forms), the refresh died there, exit 128, nothing read that,
and `diff-files` listed every file it had not reached, so `check` also
printed `plan-file-reencoded` for `specs/001-feature/spec.md`, which the
split stores as its blob.

Now each `.gitattributes` the mount leaves stays in the index for its
rules and is marked `--assume-unchanged`, so the compare asks what `git
rm` asks; and the refresh's exit code is read: where git refuses the
compare (a filter failing for a file the plan sends to a leg or drops),
that is the finding, `COMPARE_REFUSED` in git's words, and no list is
read, as the split's side already does for `git add -u`.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…git rm` removes is still compared, and the store's, compare's and copies' refusals come in the groups' order (#1192)

Two more rows for the mutants of #1192's change: only the `.gitattributes`
the mount leaves are marked unchanged (a CRLF `docs/.gitattributes` sent
to the spec leg under `* text eol=lf` is still named), and a filter that
fails for every Markdown file gives the split's store refusal, then the
mount's compare refusal, then the copies' refusal.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… step that reads the file again, not at its first clone, and its row asks only that `execute` refuses before any leg (#1192)

The row that patched `check` out and ran `execute` stopped extracting the
spec leg in 47 of 48 runs here, and once extracting the code leg, after
the spec leg was pushed: the spec leg's `git checkout -B` trusted the
file's stat data that time and never read it. Which step reads the file
again is the clock's to decide (`READ_AGAIN`), so `COMPARE_REFUSED` now
names `STOPS_AT_A_READ`: extracting the spec leg, extracting the code leg
once the spec leg is pushed, or mounting the legs. The row now checks
only that `execute`, unpatched, refuses with that finding before any leg.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap added this to the v1.2.1 milestone Nov 8, 2026
@sourcery-ai

sourcery-ai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Mark only the .gitattributes files left by the mount comparison as unchanged, read the refresh result, and report a refused compare in the store/compare/copies finding order.

  • adopt-project.py L12546-12556, L13042-13058 (+2 small edits)

Describe the execution steps at which a refused comparison may be read again, and give the compare refusal its finding text and recovery guidance.

  • adopt-project.py L12603-12610, L12616-12630, L12638-12642

Tests

Add regression coverage for failing filters, comparison controls, refusal reporting and execution timing, including nested and non-UTF-8 .gitattributes paths and finding order.

  • tests/test_adopt_check_throwaway_repos.py L76-86, L104, L2330-2669

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T15:43:14Z) Independent review r1253 of #1253 at 18c2aaef: NOT READY, for two SHOULDs; no MUST. Lane 1 starts a fix round for SHOULD-1, SHOULD-2 and NIT-1 now.

Review of PR #1253 (for #1192) at 18c2aaef53ebfd8c882c68396d370eaf8a041192, by r1253

Verdict: NOT READY

There is no MUST. The core change works:

Two SHOULDs hold it:

  1. The new stop text leaves out a step that execute actually reaches.
  2. No row covers a kept nested .gitattributes, or a name that is not UTF-8. Two mutants on changed lines survive every row.

Findings

SHOULD-1: STOPS_AT_A_READ leaves out "cloning the source for the split"

  • Where: adopt-project.py:12615-12626 (the constant and its comment), and compare_refused() in the new section of tests/test_adopt_check_throwaway_repos.py.
  • The claim: execute stops at "extracting the spec leg, extracting the code leg once the spec leg is pushed, or mounting the legs". The comment says the mount's git rm "comes after both" extractions.
  • The code: between the extractions and the mount, the split's own clone runs the same pair of commands each extraction runs:
    • extraction: adopt-project.py:14584-14585 (clone, then checkout -q -B), then the push at :14604;
    • split's clone: :15935 (step = "cloning the source for the split") and :15957-15959;
    • the mount only starts at :15961.
  • Evidence: my probe clone/tests/test_r1253_probe.py runs execute in-process on the notes.txt case, with _refuse_what_check_finds patched out.
    • As is: 24 of 24 runs stopped at REFUSED extracting the spec leg.
    • With the filter taken out of the environment only while each _extract_leg runs (as if both extractions trusted the stat data): 24 of 24 runs stopped at a step the text does not name:
      REFUSED cloning the source for the split. THE EXACT COMMAND WAS:
          git checkout -q -B adopt/three-repo-shape f0edc6d6...
          exit 128   (fatal: notes.txt: clean filter 'w1136' failed)
      ... and both legs are pushed.
      
    • Summary line: 48 passed in 58.73s. Record: logs/probe-stops.log.
  • Why it matters: that step comes before the git rm, which the text does list. The body's "Why" and "Known limits" bullets ("The mount's git rm comes after both extractions") repeat the omission.
  • Fix:
    • New text: "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".
    • Add the split's clone to the comment, and update compare_refused() and the body.
    • Optional: a measurement row like test_adopt_printed_sentences.py's STOPS. It should assert that the patched execute stops at one of the named steps, not at a particular one. Today no row checks this stop text against execute at all.

SHOULD-2: no row covers the marking of a kept nested .gitattributes, or a name that is not UTF-8

  • Where: adopt-project.py:13047-13049 (the --assume-unchanged call), and the new section of the test file.
  • Evidence: two mutants survive all 18 rows.
    • R2 marks only the root's own .gitattributes: 18 passed.
    • R9 encodes the marked paths as strict UTF-8 instead of using _nul: 18 passed.
  • My probe clone/tests/test_r1253_kept.py:
  • Fix: add a row with two ids (the UTF-8 name and os.fsdecode(b".github/caf\xe9/")). It should assert that codes(result) == [REENCODED_FILES] and that the single finding is the store's refusal. plan asks about the folder as ".github/caf\udce9/" with leg: null, so answer root. This kills R2 and R9, and fails on base.

NIT-1: the comment above STOPS_AT_A_CLONE is wrong, and was wrong before this PR

NIT-2: a non-UTF-8 path in git's words prints as U+FFFD (pre-existing, not this PR's)

The brief's checks

1. The --assume-unchanged marking

  • TAKEN is a throwaway index. It is .git/index-taken inside _SplitRehearsal, which is removed on exit. grep -n '\bTAKEN\b' adopt-project.py shows it only at lines 13045-13054, so nothing after diff-files reads it.
    • STORED is a separate index that hold builds fresh.
    • _as_the_split_writes only touches the work tree.
    • execute never sees the throwaway, so the bit cannot reach it.
  • What git rm compares (git 2.43, every entry's stat data made stale, probes/probe-a.sh):
    • A1: git rm -r -q -- notes.txt under a failing filter on every .gitattributes: exit 0.
    • A2: the same with a logging clean filter: clean ran on: [].
    • A3/A4: git rm -r -q -- docs: the filter runs on docs/.gitattributes, and fails with fatal: docs/.gitattributes: clean filter 'w1136' failed, exit 128.
    • A5 (base's compare): the refresh gives exit 128; diff-files lists every entry.
    • A6 (the PR's marking): the kept rules are skipped, and the moved docs/.gitattributes still refuses.
    • So the writer's claim holds: git rm never compares a .gitattributes it leaves, and does compare one it removes.
  • The marked set is right for each case the brief names.
    • A .gitattributes sent to a leg or dropped, with a rule reaching it, is in moved and is compared. test_r1253_moved.py, with docs/.gitattributes saying .gitattributes filter=w1136 and docs/ sent to spec, code, or dropped:
      • head: exactly compare_refused('docs/.gitattributes') for all three;
      • base: the misnamed "would store it as other bytes".
    • A kept nested one is marked, and named by the store (SHOULD-2).
    • A name that is not UTF-8 works, through _nul (SHOULD-2).
    • A moved one that no rule reaches has no filter to fail. A symlink is never filtered.
  • No false plan ok found. The bit is set only on rules outside moved, and taken keeps only moved paths, so the bit can change taken only by letting the refresh finish. A marked rule whose filter fails is still named somewhere:
    • the root's own, by D5(a);
    • a kept nested one, by STORE_REFUSED.

2. The exit code

  • asked.refused works as the store does (stored = [] if stored_said is not None) and as the copies do ((said, [])).
  • When -q --refresh exits non-zero (probes/probe-b.sh):
    • B1: a CRLF blob under eol=lf: exit 0, and diff-files lists it.
    • B2: the same without -q: exit 1.
    • B3: a file missing from the work tree: exit 0.
    • B4: an unmerged entry: exit 1. hold writes only stage-0 entries, so this cannot happen in TAKEN.
    • So a moved file that only changed is still plan-file-reencoded, never a refusal. The CRLF controls (3 ids) and the docs/.gitattributes control pass at head.
  • The refusal order (store, compare, copy) follows the order of the findings groups, and is pinned:
    • R6 (store, copy, compare) is killed by test_each_refusal_is_named_in_the_groups_order;
    • the writer's M8 makes the same point.

3. The writer's argument holds

  • R10 reads the exit code but drops the refusal, in both places. Over the three notes.txt cases (spec, code, drop), check gives exit=0 stdout-plan-ok=True findings=[].
  • execute, unpatched, then makes both leg repositories and stops at REFUSED extracting the spec leg with fatal: notes.txt: clean filter 'w1136' failed (logs/mut-R10-probe.rec).
  • At head, check names the refusal: the rows pass, and execute refuses before any leg.

4. STOPS_AT_A_READ and the docs

  • The wording is covered in SHOULD-1.
  • git grep -n 'file-reencoded' -- ':!tests' ':!adopt-project.py' is empty (rc=1).
  • The only doc that quotes the stop texts is docs/release-notes/v1.2.0.md:478-480. It describes the tagged v1.2.0 release, and this PR is outside v1.2.0, so no doc needs the new text.
  • The STOPS_AT_A_CLONE comment limit is covered in NIT-1.

5. Rows and repeat runs: see the tables below. No flake: 6 runs at head, 1 under the 3.14-like shim, and 1 on the merge with #1189 all passed 18/18.

6. Mutants: 13 of my own (table below).

7. Merges: clean (below).

9. The body:

Rows: base 93d29314 against head 18c2aaef

The base column ran in my clone with adopt-project.py swapped to blob 5e8cfd75, then restored to 698ce8e5 (equal to HEAD:adopt-project.py).

Row Ids Base Head
side tree's failing filter over *.md text (three forms) 3 3 failed: two findings 3 passed
CRLF spec control (three forms) 3 passed passed
text rule alone control (three forms) 3 passed passed
compare refused, in git's words (spec, code, drop) 3 failed: wording of the stale list passed
execute refuses before any leg (spec, code, drop) 3 failed: base's wording passed
root not laid out 1 failed: "sends 4 files" passed
docs/.gitattributes control 1 passed passed
groups' order 1 failed passed
total 18 11 failed, 7 passed in 25.57s 18 passed in 27.27s

Repeat runs at head:

Run Result
1 18 passed in 27.27s
2 18 passed in 27.11s
3 18 passed in 25.76s
4 18 passed in 26.54s
5 18 passed in 27.17s
6 18 passed in 28.71s
under the 3.14-like pathlib shim 18 passed in 28.03s

Mutants: 13 run, 8 killed by the PR's rows

Every restore read back as blob 698ce8e5, equal to HEAD's.

Mutant Change 18 rows Result
R1 marked set inverted (only moved rules) 7 failed killed
R2 only the root's own .gitattributes marked 18 passed survives; my kept probe kills it (2 failed)
R3 taken read only where git refused 11 failed killed
R4 not-laid-out branch's test inverted 1 failed killed
R5 stop said as STOPS_AT_THE_SPLIT 8 failed killed
R6 refusal order store, copy, compare 1 failed killed
R7 --skip-worktree instead of --assume-unchanged 18 passed survives; equivalent (refresh and diff-files skip both bits)
R8 --no-assume-unchanged 6 failed killed
R9 marked paths encoded as strict UTF-8 18 passed survives; my kept probe kills it (traceback)
R10 exit code read, refusal dropped 8 failed killed; check gives plan ok, then execute fails after both legs
R11 rules held out of TAKEN, no mark (the alternative the writer rejected) 18 passed survives; equivalent here (the checkout writes every rule to the work tree)
R12 refused compare raised as _Unasked 8 failed killed
R13 the mark's exit code unread 18 passed survives; equivalent (A7: the mark fails only for a path not in the index)

Merges

Gate and other summary lines (probe environment, git 2.43, CPython 3.12.3)

Call Result
throwaway file, whole 169 passed, 1 skipped in 218.44s (skip: the disk that ignores case)
test_adopt_reencoded_files.py + test_adopt_nested_attributes.py 126 passed in 182.25s
test_adopt_printed_sentences.py + test_adopt_single_repository_record.py 93 passed in 61.57s
test_scaffold_leg_path_collisions.py 104 passed in 5.77s
test_path_guard_hygiene.py + test_no_auto_maintenance.py 179 passed, 2 skipped in 34.54s (test_no_auto_maintenance.py:222, caused by GIT_CONFIG_GLOBAL=/dev/null)
doc trio 171 passed in 40.36s
execute-stop probe 48 passed in 58.73s
kept, kept-own and moved probes head 2, 1 and 3 passed; base runs restored to 698ce8e5

What I did not cover

  • test_shape_doctor.py and the full suite (the brief forbids both), CI's git, real CPython 3.14 (only the pathlib shim), Windows and macOS.
  • The writer's own 10 mutants, which I did not re-run.
  • Whether "mounting the legs" is ever the first read in a run that is not patched.
  • User configs such as core.ignoreStat or split-index, and a failing filter.<x>.process filter.

Two process notes:

  • My first head run hung because the command line used a zsh ${(@f)} flag. Lane 1 stopped it, and I re-ran through run-ids.sh.
  • For about 4 seconds, three of my pytest calls ran at once: the moved probe overlapped two gate calls.

Files

  • Review worktree: /workspace/projects/openRepoShape-worktrees/l1-r1253, clean at 18c2aaef.
  • Scratch folder: /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1253/
    • logs: logs/
    • mutants: mutants.py, with results in logs/mutants.tsv
    • git-level probes: probes/probe-a.sh, probes/probe-b.sh
    • probe tests, untracked in my clone only: clone/tests/test_r1253_probe.py, test_r1253_kept.py, test_r1253_kept_own.py, test_r1253_moved.py
    • merge worktree: merge-1189/

Nothing was pushed or posted.

Lane: openreposhape-1 (openRepoShape-1)

Brett Heap and others added 3 commits November 8, 2026 15:51
… the steps `execute` can stop at once both legs are pushed, and `STOPS_AT_A_CLONE`'s comment says each leg is extracted and pushed in turn (#1192)

r1253's SHOULD-1: between the extractions and the mount, the split's own
clone runs the same `git clone` and `git checkout -q -B` an extraction
runs. With the filter kept out of both extractions, `execute` stopped
there (`REFUSED cloning the source for the split`) in 24 of 24 runs, a
step `STOPS_AT_A_READ` left out. It now reads "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", and
its comment no longer says the mount's `git rm` comes right after the
extractions. `compare_refused()` follows.

r1253's NIT-1: `STOPS_AT_A_CLONE`'s comment said both legs are extracted
before either is pushed; `_build_the_legs` extracts and pushes each leg
in turn, the spec leg first.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s under `.github/`, whose filter fails, its folder named in UTF-8 or not, is the split's store's refusal alone (#1192)

r1253's SHOULD-2: no row covered the marking of a kept nested
`.gitattributes`, or a name that is not UTF-8, and two mutants of the
marking survived every row: only the root's own `.gitattributes` marked
(R2), and the marked paths encoded as strict UTF-8 rather than by `_nul`
(R9). The new row keeps `.github/<folder>/.gitattributes`, the root's
`.github/*/.gitattributes filter=w1136` and `*.md text` naming the
filter for it, with `<folder>` `kept` and `caf` plus the byte 0xE9; and,
a third id, the kept file's own rule naming it. Each gives one finding,
the split's store's refusal. On 93d2931 each also gave a false
`plan-file-reencoded` for `specs/001-feature/spec.md`.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d out, stops at a step the refused compare names, with the legs pushed it names for that step, and at the split's own clone where neither extraction reads the file (#1192)

r1253's optional row for SHOULD-1. No row checked `STOPS_AT_A_READ`
against `execute`. This one runs `execute` in-process with
`_refuse_what_check_finds` patched out, in two forms: the extractions as
they run, and as if each trusted the file's stat data (the filter out of
the environment while `_extract_leg` runs, as r1253's probe did). It
asks for no step in particular, since the clock decides which step
reads the file again: only that the step is one the finding names, that
the notice says the legs pushed that the finding names for it, and, in
the second form, that both legs are pushed. Where no step read the
file, `execute` exits 0 and the row is skipped, as the finding warns.

Measured here with git 2.43 over 72 runs: as they run, 47 stopped
extracting the spec leg and 1 extracting the code leg once the spec leg
was pushed; as if trusting, 24 of 24 stopped cloning the source for the
split, both legs pushed. Every run passed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T16:34:08Z) #1253's fix round for r1253 (issuecomment-6723519114) is pushed at head ad70474d. A fresh independent re-review starts now.

  • SHOULD-1: STOPS_AT_A_READ now names every step that can read the 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". The comment and compare_refused() match.
  • SHOULD-2: test_a_kept_nested_gitattributes_whose_filter_fails_is_the_stores_alone covers a kept .github/<dir>/.gitattributes in three ids: the root's rule, the same with a name that is not UTF-8, and the file's own rule. It kills r1253's R2 and R9.
  • NIT-1: STOPS_AT_A_CLONE's comment now says each leg is extracted and then pushed in turn.
  • NIT-2: U+FFFD in git's words for a name that is not UTF-8 is a Known limit, with follow-up text. No code changed for it.
  • Optional row: test_execute_stops_at_a_step_the_refused_compare_names runs execute with check's refusal patched out. It asserts a stop at one of the named steps, not at a particular one.
    • It passed 72 of 72 runs on git 2.43: as they run, 47 stopped at the spec leg and 1 at the code leg; trusting the stat data, 24 of 24 stopped at the split's clone.
  • Rows and mutants: the 9 new ids fail at base 93d29314 and pass at head (3 of them fail at 18c2aaef too). 11 of 11 mutants are killed.
  • Gate at ad70474d:
    • the throwaway file: 178 passed, 1 skipped;
    • 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;
    • the two scans: 179 passed, 2 skipped;
    • the doc trio: 171 passed;
    • tests/test_shape_doctor.py, in 4 shards: all pass.

Lane: openreposhape-1 (openRepoShape-1)

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-11-08T17:13:44Z) Independent re-review r1253b of #1253 at ad70474d: READY, with no MUST or SHOULD and two NITs. Lane 1 takes NIT-A's sentence into the body and the squash text, leaves NIT-B's optional rewrite of the row as a follow-up idea (it would also steady lane 4's test_execute_stops_where_the_finding_says[the-common-case]), and lands #1253 once CI is green on ad70474d, in the slot lane 3 gives it.

Re-review of PR #1253 (for #1192) at ad70474d2b6bab225b13c91ddfd1bbd3d70fc186, by r1253b

Verdict: READY

There is no MUST and no SHOULD. The round fixes every point r1253 raised:

  • SHOULD-1 is fixed. STOPS_AT_A_READ names every step execute can stop at, in order. The comment and compare_refused() say the same.
  • SHOULD-2 is fixed. The kept-row fails on base, passes at head, and kills R2 and R9. Its non-UTF-8 id skips only where such a name cannot be made.
  • NIT-1 is fixed. NIT-2 is left as a pre-existing Known limit, with follow-up text.
  • The optional row is safe for CI. Every timing outcome either passes or skips. I could not make it fail:
    • 144 real runs under load: all passed, none skipped;
    • 30 runs that force each step to be the first read: all passed;
    • 6 runs where no step reads the file again: all skipped, with exit 0.

There are two NITs, one about a sentence in the body and one optional strengthening of the row.

Findings

NIT-A: "that it can be is read off the code" (Known limits) overstates what the code shows

  • Where: PR body, ## Known limits, bullet 2: "The mount's git rm was never the first to read the file in any run, so that it can be is read off the code, not measured."
  • Evidence:
    • The mount's git rm runs in the same clone, after the split's git checkout -q -B (adopt-project.py:15957-15966).
    • For the git rm to read the file, git must see an index that was written in the same second as the file. With a clock that only moves forward, the clone's own index would then be from that second too. So the checkout reads the file first, and the stop is "cloning the source for the split".
    • I could reach "mounting the legs" first only by setting the index's mtime by hand. Probe clone/tests/test_r1253b_branches.py, scenario mount-first, set the index mtime equal to notes.txt's after the checkout. Result: 6 of 6 steps=['mounting the legs'], and the real row passed.
    • In 144 real runs (mine), 72 (the writer's) and 48 (r1253's), the mount was never the first read.
  • Why it doesn't matter much: naming the mount in STOPS_AT_A_READ does no harm. It lists more than can happen, never less, and it is the step the compare models. Only the body's sentence is worth changing.
  • Fix (optional): "…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."

NIT-B (optional, not needed for CI): the row could force each read instead of waiting for the clock

NIT-1 (r1253): fixed

  • git show 18c2aaef:adopt-project.py | grep -n 'before either is pushed' gives 12603:#: leg\, where both legs are extracted, before either is pushed).`
  • At head it gives 12603:#: leg\, before either is pushed: each leg is extracted and then pushed in` (continued: "turn, the spec leg first").
  • That matches _build_the_legs (:15397), where each _extract_leg pushes before it returns (:14604).

NIT-2 (r1253): left as a Known limit, which is right

  • It is pre-existing. The base run of the kept row shows git's words naming .github/caf\ufffd/.gitattributes (log logs/new-base.log).
  • The head row checks every word but the folder's name, as the body says.
  • The follow-up text in Known limits is accurate.

The brief's checks

1. SHOULD-1

  • The text names every step, in order:
    • extracting the spec leg (_extract_leg's clone and checkout -q -B, :14588-14589);
    • extracting the code leg, once the spec leg is pushed (the loop at :15397; the push at :14604);
    • the split's clone (:15939, :15957-15960), once both legs are pushed;
    • the mount (:15965).
  • No later step reads the file. The mount's git rm removes notes.txt in all three cases, _verify reads only trees, and the legs carry no rule for the file.
  • A seeded spec leg is pushed too, so "once the spec leg is pushed" still holds.
  • The comment (:12616-12626) and compare_refused() say the same. The 8 exact-text rows pass at head, and every text mutant fails them.

2. SHOULD-2's row

  • It fails at base: 3 failed. Each id gets a second finding, a false plan-file-reencoded naming specs/001-feature/spec.md (assert ['plan-file-r...le-reencoded'] == ['plan-file-reencoded']).
  • It passes at head: 3 passed.
  • It kills R2 (3 failed) and R9 (1 failed, UnicodeEncodeError: 'utf-8' codec can't encode character '\udce9').
  • The non-UTF-8 skip is correct. skip_where_a_name_cannot_be_not_utf_8 is called only for the id whose folder is not ASCII. It skips on Windows (the os.fsencode check) and where mkdir fails with EILSEQ (macOS), and it raises on any other error. Here the id ran: the section had 27 passed and 0 skipped. The folder is built with chr(0xDCE9), so the module imports everywhere.

3. The optional row: flakiness. See the measurements below.

  • Verdict: safe for CI. Which step reads the file is timing-dependent. A run where no step reads it exits 0 and skips. Every reachable step is in READ_AGAIN_STOPS, with the right "pushed" notice. Exit 2 at any other step fails the row, and none was seen.
  • check's side does not depend on timing. hold writes an index with no stat data (:12760-12765), so the compare always reads the file.
  • CI runs one serial python3 -m pytest tests -q per job, on its own ubuntu-24.04 runner (.github/workflows/tests.yml:74, :130). "26 jobs at once" means 26 machines, not 26 suites on one.
  • Lane 4's row asserts exit 2 for one racy re-read, so it fails where the clone straddles a second. This row skips there instead.
  • Mutant kill power depends on timing. Under T5, the writer's spec id passed once because the clock skipped the spec step. That limits what a mutant run shows. It does not make the row flaky.

4. NIT-1 and NIT-2: see above.

5. Mutants: 14 of 14 killed (table below).

6-7. Gate and merges: see the tables below.

8. The body

  • Exactly one closing form. Line 1 is Closes #1192, and closingIssuesReferences is [1192]. A grep for other close, fix and resolve forms before an issue number finds only line 1.
  • The title is 243 characters.
  • The stacking statement is right: merge-base HEAD main is 93d29314.
  • The section's claims match what I measured:
    • base 9 failed; 18c2aaef 3 failed and 6 passed (the trusting ids); head 9 passed;
    • gate counts S 27, G1 178/1, G2 126, G3 93, G4 104, G5 179/2, G6 171;
    • the writer's 72 runs: repeat-1.rec 23 spec and 1 code, repeat-2.rec 24 spec and 24 split clone.
  • "Why it is reliable" ("exits 0 and the row skips… any other exit fails the row") is confirmed by my no-read probe.
  • The one inaccuracy is NIT-A.

Flakiness measurements (git 2.43, CPython 3.12.3, 32 cores, other lanes' pytest running)

The real row, called unchanged 8 times per id (clone2/tests/test_r1253b_repeat.py):

Batch Ran beside load1 Result As they run Trusting the stat
A my mutant runs 37.5-44.7 48 passed in 123.91s 24 × extracting the spec leg 24 × cloning the source for the split
B my mutant runs 39.0-63.7 48 passed in 134.10s 24 × spec leg 24 × split clone
C my merge-main gate 51.2-60.1 48 passed in 126.11s 24 × spec leg 24 × split clone
Total 144 passed, 0 skipped, 0 failed 72 spec leg 72 split clone

Forced branches (clone/tests/test_r1253b_branches.py, final version):

  • How it works: after each clone, the index mtime is set equal to notes.txt's ("R": the next command reads it) or 3 s later ("T"). A "T" clone is set again after its checkout -q -B.
  • Result: 30 runs, and the real row passed in all 30. The pytest summary was 6 failed, 24 passed in 82.72s; the 6 failures are only my probe's "expected a skip" check in the no-read case.
    • spec-first: 3 × extracting the spec leg;
    • code-first: 3 × extracting the code leg ("the spec leg is pushed");
    • split-first: 3 × cloning the source for the split as they run; 9 more in the trusting form, where the R extraction clones cannot fail;
    • mount-first: 6 × mounting the legs.
  • Why the no-read case still read in this probe: a later git rm rewrote the index in the second the file was written, and git read it again there. That is a weakness of the emulation, and it is why I wrote the next probe.

No-read emulation (clone/tests/test_r1253b_noread.py):

  • After each clone it waits past the second notes.txt was written, then touches the index, as a clone that straddles a second does.
  • Result: 6 skipped in 42.18s. Every run had codes=[0], steps=[], reason "no step read notes.txt again in this run (READ_AGAIN)".

Mutants: 14 of 14 killed (#1192 section, 27 ids; clone at ad70474d)

Every restore read back as blob 5778d0e3, equal to HEAD:adopt-project.py.

Mutant Change Section result New-row ids failed
X1 spec leg; in place of spec leg, 11 failed 3 STOPS as they run
X2 cloning the split 11 failed 3 STOPS trusting
X3 mounting the leg repositories 8 failed none (exact rows kill it)
X4 code leg "once the spec leg is made" 8 failed none (exact rows kill it)
X5 mount before the split's clone 11 failed 3 STOPS trusting
X6 comma, not colon, after "again" 8 failed none (exact rows kill it)
X7 stop said as STOPS_AT_A_CLONE 14 failed all 6 STOPS
X8 comma before "or mounting the legs" 8 failed none (exact rows kill it)
X9 rules whose path is not ASCII left unmarked 1 failed KEPT not-utf-8
X10 marked paths encoded with replace 1 failed (adopt-git-unasked) KEPT not-utf-8
X11 only rules at most one folder deep marked 3 failed 3 KEPT
R2 (r1253's) only the root's own .gitattributes marked 3 failed 3 KEPT
R5 (r1253's) stop said as STOPS_AT_THE_SPLIT 14 failed all 6 STOPS
R9 (r1253's) marked paths encoded as strict UTF-8 1 failed (UnicodeEncodeError) KEPT not-utf-8

Merges

Summary lines (probe environment, git 2.43, CPython 3.12.3, at most 2 calls of mine at once)

Call Result
#1192 section (27 ids) 27 passed in 49.67s
throwaway file, whole 178 passed, 1 skipped in 246.01s (:493, disk that ignores case)
test_adopt_reencoded_files.py + test_adopt_nested_attributes.py 126 passed in 223.60s
test_adopt_printed_sentences.py + test_adopt_single_repository_record.py 93 passed in 79.28s
test_scaffold_leg_path_collisions.py 104 passed in 6.26s
test_path_guard_hygiene.py + test_no_auto_maintenance.py 179 passed, 2 skipped in 38.04s (test_no_auto_maintenance.py:222, caused by GIT_CONFIG_GLOBAL=/dev/null)
doc trio 171 passed in 47.12s
section under the 3.14-like pathlib shim 27 passed in 65.53s
9 new ids, base 93d29314 (blob 5e8cfd75) 9 failed in 31.41s
9 new ids, 18c2aaef (blob 698ce8e5) 3 failed, 6 passed in 26.77s (the trusting ids)
9 new ids, head 9 passed in 21.77s
repeats A / B / C 48 / 48 / 48 passed
forced branches real row 30 of 30 passed
no-read emulation 6 skipped in 42.18s
merge with main 178 passed, 1 skipped in 294.99s
merge with #1189 47478d51 221 passed, 2 skipped in 329.76s

What I did not cover

  • tests/test_shape_doctor.py and the full suite (the brief forbids both), CI's own git build, real CPython 3.14 (only the pathlib shim), Windows and macOS.
  • The writer's T1-T7 and K1, which I did not re-run; I wrote my own X1-X11 instead.
  • A file system with whole-second mtimes, or a git built with USE_NSEC. By reading the code, there the row would always skip, never fail.

Process note: I had two watches running on the #1189 run for a short time. They were read-only, and I stopped the extras.

Files

  • Review worktree: /workspace/projects/openRepoShape-worktrees/l1-r1253b, clean at ad70474d.
  • Scratch folder: /workspace/projects/openRepoShape-worktrees/_lane1-scratch/r1253b/
    • logs: logs/
    • mutants: mutants.py, with results in logs/mutants.out
    • repeat records: logs/repeat-{A,B,C}.rec
    • branch records: logs/branches.rec (final), logs/branches-v1.rec
    • no-read records: logs/noread.rec
  • Probe tests, untracked in my clones only: clone/tests/test_r1253b_branches.py, clone/tests/test_r1253b_noread.py, clone2/tests/test_r1253b_repeat.py.
  • Merge worktrees: merge-main/ (cb1c4e5f) and merge-1189/ (c434aebd). Both clones' tools are at blob 5778d0e3.

Nothing was pushed or posted.

Lane: openreposhape-1 (openRepoShape-1)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant