Skip to content

shape-doctor.py: a path that is not ASCII is printed as that path in every NEXT, keep line and WHY, as itself where the report's encoding writes its bytes and escaped whole where it does not, so a line naming Projets/Équipe or a Latin-1 name runs as printed (#785) - #792

Merged
brettheap merged 8 commits into
mainfrom
l3-785-non-ascii-paths
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-3 (openRepoShape-3)
Claim: #785 (comment)

Closes #785.

Lands after the v1.1.0 tag.

Where the ? came from, and why it was there

What changed

  • quote_arg (shape-doctor.py:773, :897) and quoted_beyond_ascii (:917):
    • An ASCII value is quoted exactly as before, byte for byte.
    • A value that is not ASCII is quoted as itself, in the usual quoting, when every such character prints_as_itself: it is printable, it is not typography ascii_text flattens, and the report's encoding writes it. That encoding is stdout's, or the new encoding= keyword. On POSIX the bytes written must also be the bytes the path holds (path_bytes, :731).
    • Otherwise the whole value is escaped:
      • for sh, as $'...', every byte of such a character a three-digit octal escape. A surrogateescape byte (a name that is not UTF-8) is that byte. The apostrophe and backslash become \047 and \134.
      • for PowerShell, as ('...' + [char]0x00C9 + '...') (powershell_expression, :948). The first operand is always a string, because [char] + [char] adds two numbers. Its verbatim strings double all five of PowerShell's single quotes (powershell_verbatim, :940, with repo_shape.POWERSHELL_SINGLE_QUOTES imported at :428).
  • ascii_text (:704), with FLATTENED (:675), printed_encoding (:679) and prints_as_itself (:685):
  • Docs and comments:
    • the module docstring's ASCII rule (:336), quote_arg's docstring (:831) and the Row comment (:1002);
    • the docstrings of scripts/shape_materialize.py:162 (escape_what_stdout_cannot_encode) and scripts/shape_advisory.py:172 (printable), which described the doctor's old flattening.
    • No README, AGENTS.md, docs/cli.md or handbook text states the doctor's ASCII output (grep), and --help is unchanged.

What a reader sees (measured on this branch)

These lines are for a holder under Équipe, with its member detached and a commit made on it. Main printed ?quipe (and ??quipe under LC_ALL=C PYTHONUTF8=0) in every one of them.

  • Under a UTF-8 locale or LC_ALL=C (UTF-8 mode):
    • NEXT: python3 '/…/Équipe/InkRouter/scripts/bootstrap.py' --root '/…/Équipe/InkRouter'
    • WHY keep line: git -C '/…/Équipe/InkRouter/members/IRRS' branch NAME
  • Under LC_ALL=C PYTHONUTF8=0:
    • NEXT: python3 $'/…/\303\211quipe/InkRouter/scripts/bootstrap.py' --root $'/…/\303\211quipe/InkRouter'
  • With the directory named in Latin-1 (\xc9quipe), under every locale:
    • keep line: git $'--git-dir=/…/\311quipe/InkRouter/.git/modules/members/IRRS' branch NAME <commit>

In round 1, --json carried the same command as the table. Since round 2 it is written for UTF-8 whatever stdout's encoding is (see Round 2): a character shown as itself there is written as a \u escape that decodes to it.

Shells, and Windows

  • $'...' runs in bash, zsh, ksh and a POSIX.1-2024 sh. dash 0.5.12, Ubuntu's /bin/sh, reads the $ as itself. Measured on this host:
    • bash 5.2.21 and zsh 5.9 turn every spelling in the tests back into the path's bytes;
    • interactive bash 5.2 does not history-expand a ! inside $'...';
    • the existing paste tests run lines with sh -c over ASCII paths and are unaffected, and the new ones paste with bash.
  • Windows. Python writes a console as UTF-8, so in a console every printable character is quoted as itself ('C:\Users\Équipe'). Only a stdout redirected to a file or a pipe is in the ANSI code page. There, a character outside that code page, a typography character or a control gets the [char] expression. No PowerShell is installed here, so that form is asserted as text and read back by the test's own reader. It was not run in PowerShell.
  • A curly apostrophe in a path is now escaped on both platforms, as $'O\342\200\231Neill' or ('O' + [char]0x2019 + 'Neill'). It used to be flattened to ', which named another path. test_a_curly_apostrophe_is_normalized_before_it_is_quoted became test_a_curly_apostrophe_is_escaped_where_the_row_cannot_reach_it (tests/test_shape_doctor.py:10101).

Tests

New file, tests/test_doctor_non_ascii_paths.py (92 cases):

  • The spellings for sh and for PowerShell, asserted on every OS (:119, :124).
  • Row's second pass leaves each spelling whole (:130).
  • The PowerShell form stands for the value (:156).
  • What a sentence keeps and what it replaces (:162, :176).
  • bash and zsh turn each sh spelling back into the path's bytes (:185).
  • Run under a UTF-8 locale, under LC_ALL=C and under LC_ALL=C PYTHONUTF8=0, for a holder under a directory named in UTF-8 and one named in Latin-1:
    • the members row's keep line, for a detached member (git -C) and an emptied one (--git-dir), taken from the table's bytes and from --json, is pasted into bash and keeps the commit on a branch (:279);
    • the NEXT's --root is the root's own bytes, from the table and from --json (:308);
    • the table's NEXT, pasted, cures the row (:325).
  • Against main's shape-doctor.py: 78 failed, 14 passed (corrected in round 2). The 14 are the bash and zsh cases, which test the spellings alone.

Changed: tests/test_shape_doctor.py:10101 (the curly-apostrophe test, as above) and the _doctor docstring at tests/test_stdout_encoding.py:314.

Runs. Each was made with a stub gh first on PATH exiting 97, GH_TOKEN=bogus and an empty GH_CONFIG_DIR, on git 2.43.0 and Python 3.12.3:

  • /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt2 -o tmp_path_retention_policy=failed tests/test_doctor_non_ascii_paths.py tests/test_doctor_gitmodules_not_utf8.py tests/test_doctor_legs_members_next.py tests/test_doctor_members_spelled.py tests/test_doctor_gitlink_repairs.py
    → 274 passed in 204.38s (0:03:24)
  • /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt2 -o tmp_path_retention_policy=failed tests/test_doctor_next_is_a_command.py tests/test_family_printed_quoting.py tests/test_stdout_encoding.py tests/test_update_shape_next_quoting.py tests/test_readme_windows_quoting_sentence.py tests/test_doctor_validator_verdicts.py tests/test_doctor_recorded_answers.py tests/test_commit_trailers.py tests/test_shape_advisory.py tests/test_repo_hygiene.py tests/test_cli_reference.py
    → 602 passed, 1 skipped in 157.06s (0:02:37)
  • /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt -o tmp_path_retention_policy=failed tests/test_shape_doctor.py
    → 437 passed, 3 skipped, 1 xfailed in 319.47s (0:05:19)

A member NAMED café, and #774

The issue's own example is a member whose .gitmodules section is named café or caf\xe9; its keep line names .git/modules/members/café. On main, finding that name is #732's (#774's) job: a Latin-1 name gets no keep line at all, and LC_ALL=C PYTHONUTF8=0 ends in a traceback. So this PR tests the spelling through the holder's path, which every keep line, mount and kept repository sits under.

On a scratch merge of this branch with #774's head 93b9bac3, I ran a 12-case check. It uses #774's member_named, BYTE_NAMES and NAME_LOCALES, with each line taken from the table and from --json. All 12 passed, and each keep line pasted into bash kept the commit. On #774's head alone, all 12 failed. The check is not in this PR because its helpers are #774's. It can follow once #774 lands.

Overlaps

See Round 2 for the overlaps at the head.

Pinned copies

None touched. scripts/repo_shape.py, a pinned copy, still says in shell_quoted's docstring that the doctor flattens a value to ASCII. I left it, because any change to that file puts an upstream-changed row in front of every project. It can ride the next change to that file.

Left as it was

Round 2 (RV-a's review, issuecomment-6232864874)

Two commits on a1e2c9a8, pushed as a plain fast-forward to 4338345c. Main is not merged in.

S2: --json holds a UTF-8 name as itself, whatever stdout's encoding is (174efe80). This is RV-a's sketch, with its two caveats taken.

  • REPORT_ENCODING (shape-doctor.py:685): main sets it to "utf-8" for --json (:10146), before any row is built, and to None for the table.
  • printed_encoding (:688) reads it before stdout's encoding. json.dumps writes ASCII whatever stdout is, and every character as an escape its reader decodes.
  • quoted_beyond_ascii (:931, docstring :937) reads a POSIX name again as UTF-8 through surrogateescape (:949). Under LC_ALL=C PYTHONUTF8=0, é is read in as two escaped bytes; they become é again, and path_bytes gives the same bytes either way.
  • A surrogate surrogateescape never makes (outside U+DC80..U+DCFF) cannot be re-read like that. It is left as it is, and escaped, because it is not printable.
  • The docstring at :869 (the old :858-861) no longer says that --json and the table carry the same command.
  • A Latin-1 name is still $'…' in both.

S3 and N1: tests (tests/test_doctor_non_ascii_paths.py).

  • test_785_the_members_next_names_the_real_root (:343) runs under STDOUTS (:225): the three locales, plus a UTF-8 locale with PYTHONIOENCODING=cp1252 and with ascii. That is 20 cells.
  • test_785_json_holds_a_utf_8_name_as_itself_whatever_stdout_holds (:373) runs under the same five (S2).
  • test_785_dash_runs_a_next_that_shows_the_name_as_itself (:358) runs dash on the UTF-8 name, under the UTF-8 locale and C, from the table and from --json (N1). It is skipped where there is no dash.
  • Two new spellings: utf-8-read-byte-by-byte (:87) and stray-surrogate (:91).
  • That test's body moved into a helper, next_root (:296), which the dash test shares.

S1: the dash limit, documented (4338345c).

N3. Round 1's count against main's shape-doctor.py is corrected above to 78 failed, 14 passed, as RV-a measured. Round 2's file gives 99 failed, 18 passed; the 18 are the bash and zsh spelling cases. The #791 note is gone: #791 is clean.

Mutants. Each was run on tests/test_doctor_non_ascii_paths.py and tests/test_stdout_encoding.py:

mutant result
path_bytes check dropped 3 failed: the cp1252-escaped spelling twice, and the cp1252-stdout table --root cell
always escape 16 failed, 9 of them doctor runs: the 4 dash cells (N1) and --json under all five stdouts
never escape 50 failed
printed_encoding ignores stdout 6 failed: the cp1252, ascii and C-ascii-fs table --root cells (S3), 2 keep lines and the cure under C-ascii-fs
main leaves REPORT_ENCODING unset (--json follows stdout) 3 failed: --json under C-ascii-fs, cp1252 and ascii (S2)
no surrogateescape re-decode 3 failed: utf-8-read-byte-by-byte twice, --json under C-ascii-fs
no guard on the re-decode 2 failed: stray-surrogate (a UnicodeEncodeError)

Runs on 4338345c. Each was made with a stub gh first on PATH exiting 97, GH_TOKEN=bogus and an empty GH_CONFIG_DIR:

  • RV-a's 18 files: /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt2 -o tmp_path_retention_policy=failed tests/test_doctor_non_ascii_paths.py tests/test_stdout_encoding.py tests/test_doctor_next_is_a_command.py tests/test_doctor_validator_rows.py tests/test_doctor_validator_verdicts.py tests/test_doctor_validator_site.py tests/test_doctor_members_spelled.py tests/test_doctor_legs_members_next.py tests/test_doctor_gitlink_repairs.py tests/test_doctor_recorded_answers.py tests/test_family_printed_quoting.py tests/test_readme_windows_quoting_sentence.py tests/test_update_shape_next_quoting.py tests/test_shape_advisory.py tests/test_doctor_gitmodules_not_utf8.py tests/test_cli_reference.py tests/test_repo_hygiene.py tests/test_validator_child_encoding.py
    → 862 passed, 1 skipped in 763.71s (0:12:43)
  • The handbook and README tests: … tests/test_repo_hygiene.py tests/test_readme_windows_quoting_sentence.py tests/test_readme_sections.py tests/test_agents_md_codes.py tests/test_setup_by_hand_commands.py tests/test_update_add_codes_documented.py
    → 214 passed in 4.56s
  • … tests/test_shape_doctor.py (once) → 437 passed, 3 skipped, 1 xfailed in 540.84s (0:09:00)

Overlaps at 4338345c. git merge-tree is clean with main (20004887) and with every open lane-3 PR (#802, #794, #791, #790, #789, #788, #782, #781, #780, #779, #778, #775, #774, #773, #772, #771, #770, #769) but #630, which conflicts in README.md and docs/handbook.html as above.

Left as RV-a said:

  • N2: the stderr REFUSED block, which is reasoned only.
  • N4: the pinned scripts/repo_shape.py docstring.

Round 3 (RV-a's re-verification, issuecomment-6234017777)

One commit, 26d61255, pushed as a plain fast-forward on 4338345c. Main is not merged in.

M1: under a legacy multibyte filesystem encoding, the re-decode named another directory.

  • The failure. Under EUC-JP, Big5 or EUC-KR, a directory named 日 in UTF-8 (\xe6\x97\xa5) is read in as three escaped bytes. Round 2 joined them into 日, and path_bytes wrote that as EUC-JP \xc6\xfc, the twin directory, in the table and in --json.
  • The fix. quoted_beyond_ascii (shape-doctor.py:957-963) keeps the joined text only where its path_bytes equal the value's own. This is RV-a's measured fix, 5bac3ea1.
  • The docstring (:947-954) no longer says the bytes are the same either way, and names the case.
  • The test. test_785_a_utf_8_name_under_a_legacy_filesystem_keeps_its_bytes (tests/test_doctor_non_ascii_paths.py:144, cases LEGACY at :137) patches sys.getfilesystemencoding. It checks that the escaped UTF-8 bytes of 日 under EUC-JP and Big5, and of € under EUC-KR, stay $'\346\227\245' (or $'\342\202\254'). It checks the table, written in that encoding, and --json, in UTF-8. That is 6 cases.
  • Mutant: the byte-equality check removed (text = joined always, which is round 2's code): 6 failed, 167 passed, 1 skipped. All 6 are the new cases. The head passes all 6.

N1. test_785_dash_runs_a_next_that_shows_the_name_as_itself (:392) now runs over DASH_CELLS (:384): the table under the UTF-8 locale and C, and --json under all five stdouts. That is 7 cells, and all pass in dash.

N2. AGENTS.md:268-269: "each row with its id, to act on a row. A next with $'…' needs bash or zsh, not sh." This is in the same two lines, and AGENTS.md stays at 492.

Runs on 26d61255. Each was made with a stub gh first on PATH exiting 97, GH_TOKEN=bogus and an empty GH_CONFIG_DIR:

  • /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt2 -o tmp_path_retention_policy=failed tests/test_doctor_non_ascii_paths.py tests/test_stdout_encoding.py
    → 173 passed, 1 skipped in 69.67s (0:01:09)
  • … tests/test_repo_hygiene.py tests/test_readme_windows_quoting_sentence.py tests/test_readme_sections.py tests/test_agents_md_codes.py tests/test_setup_by_hand_commands.py tests/test_update_add_codes_documented.py
    → 214 passed in 4.09s

Merge round (main 2e476a7)

One merge commit, 1ccb3dd7, on 26d61255: parents 26d61255 (this branch) and 2e476a77 (main, after #804). It was pushed as a plain fast-forward. It brings in #630's v1.1.0 docs (190133d5) and everything else main landed since bf1f099b. Lane 2's #821 is not on main and none of its changes are taken; lane 2 resolves on #821's side (#473 issuecomment-6234162871).

Conflicts. Two files, as the planner predicted: one README hunk and seven handbook marker hunks.

Markers. The seven § Starting a project in a new organisation markers were recomputed from the merged README.md with tests/test_repo_hygiene.py's own readme_segments and readme_fingerprint, to blob 3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3. handbook_marker_problems reports none, and 23 of 23 markers are current. Every other marker is main's, unchanged.

Cap. README.md is 1476 lines, at its cap: 1422 + the ledger (460: +2, 550: +5, 563: +12, 564: +22, 606: +10, 683: +1, 785: +2) = 1476. That is main's 1474 plus this branch's 2, and tests/readme-cap.d/785.txt stays at +2. AGENTS.md is 494 lines, at main's cap of 494; #630 raised it from 492, so round 3's "stays at 492" was measured before that.

The delta is the reviewed one. Outside README.md, the handbook and tests/readme-cap.d/, the -U0 changed lines of git diff origin/main HEAD equal those of the reviewed git diff bf1f099b 26d61255: 772 lines each, cmp identical. The cap file is byte-identical too. In README.md and the handbook, the prose lines are the same; only the seven marker hashes differ.

Runs on 1ccb3dd7. Each was made with a stub gh first on PATH exiting 97, GH_TOKEN=bogus and an empty GH_CONFIG_DIR:

  • /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<W>-scratch/bt-docs -o tmp_path_retention_policy=failed tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_readme_sections.py tests/test_readme_windows_quoting_sentence.py tests/test_release_workflow.py tests/test_repo_hygiene.py tests/test_setup_by_hand_commands.py tests/test_update_add_codes_documented.py
    → 300 passed in 41.67s
  • … --basetemp=<W>-scratch/bt-gate … on shape-doctor.py: a path that is not ASCII is printed as that path in every NEXT, keep line and WHY, as itself where the report's encoding writes its bytes and escaped whole where it does not, so a line naming Projets/Équipe or a Latin-1 name runs as printed (#785) #792's gate set, 22 files (tests/test_shape_doctor.py once, inside it): test_doctor_non_ascii_paths, test_shape_doctor, test_readme_sections, test_setup_by_hand_commands, test_readme_windows_quoting_sentence, test_update_add_codes_documented, test_doctor_legs_members_next, test_doctor_next_is_a_command, test_doctor_validator_rows, test_doctor_validator_verdicts, test_doctor_recorded_answers, test_doctor_gitmodules_not_utf8, test_doctor_gitlink_repairs, test_doctor_plan_overrides, test_doctor_pin_restore_merges, test_doctor_partial_clone, test_doctor_validator_site, test_doctor_unfinished_merge, test_doctor_members_spelled, test_doctor_machine_row, test_shape_advisory, test_validator_child_encoding
    → 1206 passed, 3 skipped, 1 xfailed in 2071.76s (0:34:31)
  • scripts/render-cli-reference.py --check → [ok] docs/cli.md is what the generator produces
  • git diff --check origin/main...HEAD → no output, exit 0

Main moved during the gate run, to c100843b: #752's conflicted-pin NEXT in shape-doctor.py, 677f9bfa's quoting-sentence inventory, and 2c6543e8's release/v<major>.<minor> push runs. None touches README.md, the handbook, AGENTS.md or the cap ledger, and git merge-tree of this head with c100843b is clean. A predicted merge, built locally with merge-tree and commit-tree (82c944ab, never pushed), passes the files main changed plus this branch's own and the docs checks: tests/test_doctor_non_ascii_paths.py tests/test_stdout_encoding.py tests/test_readme_windows_quoting_sentence.py tests/test_doctor_pin_restore_merges.py tests/test_validator_child_encoding.py tests/test_release_workflow.py tests/test_workflow_concurrency.py tests/test_readme_sections.py tests/test_repo_hygiene.py → 572 passed, 1 skipped, 1 xfailed in 334.47s (0:05:34).

🤖 Generated with Claude Code

Summary by Sourcery

Make shape-doctor print non-ASCII paths faithfully or escape them safely so generated commands continue to target the correct files across output encodings and shells.

New Features:

  • Preserve non-ASCII paths in shape-doctor commands when the report encoding can represent their bytes, and provide shell-specific escaping otherwise.
  • Support encoding-aware PowerShell expressions and POSIX shell byte escapes for paths that cannot be printed directly.
  • Keep JSON command paths UTF-8-safe regardless of stdout encoding.

Bug Fixes:

  • Prevent pasted NEXT and WHY commands from referring to different paths when repository names contain non-ASCII, legacy-encoded, or typography characters.
  • Avoid changing path bytes when reinterpreting escaped filesystem names under UTF-8 or legacy filesystem encodings.

Enhancements:

  • Retain printable non-ASCII characters in report prose when the output encoding supports them while continuing to flatten unsupported characters safely.
  • Document shell requirements for escaped commands and update related internal guidance.

Documentation:

  • Document the $'…' quoting used for non-ASCII paths and its bash, zsh, and ksh compatibility limitations with dash.
  • Update handbook content and synchronized section markers for the new command-printing behavior.

Tests:

  • Add comprehensive coverage for non-ASCII path quoting across POSIX shells, PowerShell, stdout encodings, JSON output, locales, filesystem encodings, and pasted doctor commands.
  • Update curly-apostrophe quoting expectations and stdout-encoding documentation tests.

…every NEXT, keep line and WHY, as itself where the report's encoding writes its bytes and escaped whole where it does not, so a line naming `Projets/Équipe` or a Latin-1 name runs as printed and `--json` carries the same command (#785)

`ascii_text` ended in `encode("ascii", "replace")`, and `quote_arg` and
`Row` both ran it, so every character of a path that is not ASCII printed
as `?` on every console, in the table and in `--json`: a keep line under
`Projets/Équipe` named `Projets/?quipe`, which is not there.

- `quote_arg` keeps an ASCII value's spelling byte for byte. A value that
  is not ASCII is quoted as itself where each such character is
  printable, is not typography `ascii_text` flattens, and is written by
  the report's encoding (stdout's, or `encoding=`) as the bytes the path
  holds; otherwise it is escaped whole: `$'...'` with three-digit octal
  escapes for `sh` (a `surrogateescape` byte as that byte), and a
  `('...' + [char]0x00C9 + '...')` expression for PowerShell.
- `ascii_text` still flattens the typography, and replaces with `?` only a
  character the report cannot show (and U+FFFD, `?` since #385), so a
  UTF-8 terminal reads `café` in a sentence and an ASCII one still reads
  ASCII.
- Docstrings of `shape_materialize.escape_what_stdout_cannot_encode` and
  `shape_advisory.printable`, which described the doctor's old flattening.
- tests/test_doctor_non_ascii_paths.py: the spellings on every OS, and the
  members row's keep lines and NEXT, from the table and from `--json`,
  pasted into bash under a UTF-8 locale, `LC_ALL=C` and `LC_ALL=C
  PYTHONUTF8=0`, for a holder under a directory named in UTF-8 and in
  Latin-1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR makes shape-doctor preserve the identity of non-ASCII paths in generated commands: values are shown literally when the report can faithfully represent them and escaped as shell- or PowerShell-safe whole expressions otherwise, while ordinary report prose remains encoding-safe. Extensive tests validate quoting, JSON/table consistency, locale and legacy-byte paths, and pasted command round trips.

Sequence diagram for encoding-safe command reporting

sequenceDiagram
    participant V as Validator
    participant Q as quote_arg
    participant E as Encoding checks
    participant R as Row
    participant O as Report
    V->>Q: quote_arg(value, platform, encoding)
    Q->>E: prints_as_itself(char, encoding)
    E-->>Q: Show literally or escape
    Q-->>V: Shell or PowerShell-safe argument
    V->>R: Build next_command or WHY
    R->>R: ascii_text(report text)
    R-->>O: Table and JSON command
    O-->>V: Command preserves path identity
Loading

Flow diagram for encoding-aware path quoting

flowchart TD
    A[quote_arg receives value] --> B{Value is ASCII?}
    B -->|Yes| C[Existing ASCII quoting]
    B -->|No| D[quoted_beyond_ascii]
    D --> E{Every character prints as itself?}
    E -->|Yes| F[Quote path as itself]
    E -->|No| G{Windows?}
    G -->|No| H[Emit sh $'...' byte escapes]
    G -->|Yes| I[Emit PowerShell expression]
    F --> J[Row ascii_text preserves quoted value]
    H --> J
    I --> J
    C --> J
Loading

File-Level Changes

Change Details Files
Preserve non-ASCII path identity in generated commands while retaining safe, readable report output.
  • Added encoding-aware character preservation and replacement rules to sentence/report text.
  • Changed POSIX quoting to emit literal path bytes when displayable or whole-value $'...' octal escapes otherwise.
  • Added PowerShell verbatim-string and [char] expression generation for displayable and unrepresentable characters.
  • Ensured Row’s second normalization pass does not alter already-safe command spellings.
shape-doctor.py
Updated documentation to distinguish report prose from path values and reflect the new output behavior.
  • Revised module and helper docstrings describing encoding-aware output and escaping.
  • Updated advisory and materialize documentation to reference the doctor’s new behavior.
shape-doctor.py
scripts/shape_advisory.py
scripts/shape_materialize.py
tests/test_stdout_encoding.py
Added comprehensive coverage for non-ASCII path quoting, round trips, locales, encodings, and JSON/table parity.
  • Tested POSIX and PowerShell spellings, including typography, surrogateescaped bytes, controls, bidi marks, apostrophes, and supplementary characters.
  • Verified bash/zsh round trips and pasted doctor NEXT/WHY commands under UTF-8, C, and non-UTF-8 filesystem conditions.
  • Updated the curly-apostrophe regression test for escaped rather than flattened paths.
tests/test_doctor_non_ascii_paths.py
tests/test_shape_doctor.py

Assessment against linked issues

Issue Objective Addressed Explanation
#785 Print runnable NEXT, keep, and WHY commands whose named paths retain the exact filesystem path, including UTF-8 and non-UTF-8 names, across output encodings and locales. ✅
#785 Preserve real path characters in --json command values (or use equivalent escapes that decode to them), rather than replacing them with ?. ✅
#785 Keep report output safe to print when the output encoding cannot represent a path character, using a quoted representation that the target shell can restore to the original path bytes. ✅

Possibly linked issues


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-3, 2026-10-17T10:33:54Z) Independent review of #792 by RV-a (opus), on head a1e2c9a8 over main bf1f099b: READY, with three SHOULDs. No MUST: no line that worked on main changes, and dash fails with an error rather than acting on another path.

1. dash is not a regression. The members NEXT was run through each shell for 3 locales × UTF-8/Latin-1 parent × table/json:

dash bash zsh
main 0/12 0/12 0/12
head 4/12 12/12 12/12
  • dash 0.5.12 reads $'…' as a literal $ followed by a single-quoted string. There is no expansion or injection: ' is \047 and \ is \134.
  • Every printed command names an absolute path, so in dash the path becomes a relative one starting with $, and the command fails with nothing created.
  • bash with set -H does not history-expand a ! inside $'…'.

2. Windows consoles.

3. ASCII paths are byte-identical to main. 24 doctor runs across three locales, with a plain parent and O'Brien sp ace$x, as table and --json.

4. Merges: clean with main and with all 17 open lane-3 PRs. On committed merges, #792's tests plus each PR's own pass:

PR result
#774 254 passed
#775 399 passed
#791 136 passed
#773 122 passed
#780 224 passed

SHOULDs:

  • S1: the dash limit is not documented.
    • README.md:155-156 and handbook:729 say "Printed commands are quoted for a POSIX shell". AGENTS.md:268-269 tells agents to act on --json rows, and sh -c "$next" is dash on Debian and Ubuntu.
    • Add a sentence after the pinned one; tests/test_readme_windows_quoting_sentence.py:32's regex still matches. For example: "A path the report cannot print as itself is spelled $'…', which bash, zsh and ksh read and dash (/bin/sh on Debian and Ubuntu) does not."
    • An optional code alternative: "$(printf '…')" gives the same bytes in dash, bash and zsh. It needs % doubled, and a trailing newline is stripped.
  • S2: what --json holds depends on stdout's encoding, though issue shape-doctor.py: every non-ASCII character in a path it prints becomes ?, so a NEXT or keep line naming such a path (a member café, a holder under Projets/Équipe) cannot be pasted as printed #785 asks for real characters in JSON.
    • Measured with an Équipe holder: PYTHONIOENCODING=ascii or latin-1 gives "python3 $'…/\303\211quipe/…'", while utf-8 gives the real character.
    • RV-a's four-edit sketch:
      • a module REPORT_ENCODING set to "utf-8" under --json;
      • printed_encoding reads it first;
      • on POSIX, a surrogateescape re-decode.
    • With the sketch, every encoding gives the real characters in JSON, and the C-locale JSON cell now works in dash too (196 passed, 1 skipped).
    • Caveats: guard surrogates outside U+DC80..U+DCFF, and reword the docstring at :858-861. A Latin-1 name still needs $'…'.
  • S3: no test runs the doctor with a stdout encoding that differs from the filesystem's (Linux's stand-in for Windows' redirected cp1252 stdout). The mutant "printed_encoding ignores stdout" survives every test; under it, bash is sent to another root. Add PYTHONIOENCODING=cp1252 and ascii cases to test_785_the_members_next_names_the_real_root. RV-a's probe does this: the head passes, and that mutant fails.

NITs:

Mutants:

mutant result
drop the path_bytes check 2 failed
always escape 5 failed, spelling tests only (N1)
never escape 45 failed
printed_encoding ignores stdout survives (S3)

Runs:

set result
18 targeted files 837 passed, 1 skipped
test_shape_doctor.py (once) 437 passed, 3 skipped, 1 xfailed
9 more files 714 passed
the new test file against main's doctor 78 failed, 14 passed

Next: lane 3 takes S1-S3 with N1 and N3 in one round after 13:50Z. S1 makes #792 a README PR, so it lands in lane 3's README turn.

Lane: openreposhape-3 (openRepoShape-3)

This was referenced Oct 17, 2026
Brett Heap and others added 2 commits October 17, 2026 11:58
…t's encoding is, and a name read in byte by byte under `LC_ALL=C` is read as the UTF-8 it spells (#785, review round 2)

Review S2 and S3 on PR #792 (RV-a). What `--json` held followed stdout's
encoding: `PYTHONIOENCODING=ascii` or `latin-1` gave `$'...\303\211...'`
where `utf-8` gave `É`, though `json.dumps` writes ASCII whatever stdout is.

- `REPORT_ENCODING`, which `main` sets to UTF-8 for `--json` before any row
  is built, and `printed_encoding` reads first: the table still follows
  stdout's encoding.
- `quoted_beyond_ascii` reads a POSIX name again as UTF-8 through
  `surrogateescape`, so the two bytes `LC_ALL=C PYTHONUTF8=0` reads `é` in
  as are `é` again; a surrogate `surrogateescape` never makes is left, and
  escaped. A Latin-1 name is still `$'...'`.
- tests: the members NEXT's `--root` with stdout in cp1252 and in ASCII
  (S3), `--json` holding the name as itself under all five (S2), dash
  running the lines that show a UTF-8 name as itself (N1), and the two new
  spellings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rint as itself is written `$'…'` off Windows, which bash, zsh and ksh read and dash (`/bin/sh` on Debian and Ubuntu) does not (#785, review round 2)

Review S1 on PR #792 (RV-a): the sentence that printed commands are
quoted for a POSIX shell did not say that the form #785 writes is not
read by dash, and `sh -c` is dash on Debian and Ubuntu.

- README.md, "Windows, natively": two lines after the pinned sentence,
  which `tests/test_readme_windows_quoting_sentence.py` still finds once;
  `tests/readme-cap.d/785.txt` raises the cap by those two.
- docs/handbook.html: the same sentence in the Windows panel. The seven
  markers reading README § Starting a project in a new organisation move
  to its new blob. Of the seven sections that read it, only `start` (the
  Windows panel) says how printed commands are quoted; `masthead`,
  `partway`, `ci`, `legs`, `doublepin` and `park` say nothing the new
  sentence changes, so for them the marker alone moves.
- AGENTS.md point 7 of "Checking": a `next` with `$'…'` needs bash, in the
  two lines the point had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T12:43:26Z) Re-verification of #792 round 2 by RV-a (opus), on head 4338345c: NOT READY, with one MUST. The fix is three lines and has been measured. Everything else checked holds.

M1: under a legacy filesystem encoding, the re-decode names another path, in the table and in --json (shape-doctor.py:949-953; the docstring's "same bytes either way" at :937-947 is false there).

  • Scenario:
    1. The locale is EUC-JP, Big5 or EUC-KR, so Python's filesystem encoding is that encoding. UTF-8 mode is automatic only under C/POSIX.
    2. A directory is named in UTF-8, for example 日 = \xe6\x97\xa5.
    3. os.fsdecode escapes those bytes, and the re-decode joins them into 日.
    4. path_bytes('日') then encodes as EUC-JP \xc6\xfc, a different name.
  • Measured with sys.getfilesystemencoding patched, both directories on disk, and each line pasted into bash: round 1 named the right directory in 8/8 cells, round 2 the other one in 8/8.
  • Property check over 20,000 random names per encoding (re-joined names whose bytes changed): EUC-JP 471, Big5 170, EUC-KR 227. UTF-8 and ASCII gave 0, so the C locale with UTF-8 mode off is safe.
  • Fix (measured, 5bac3ea1 in RV-a's clone): keep the joined text only when b"".join(map(path_bytes, joined)) == b"".join(map(path_bytes, text)).
    • The legacy probe then gives 8/8 right.
    • The 20-cell matrix and every dump are byte-identical to 4338345c.
    • 164 passed, 1 skipped.
  • Also: correct the docstring, and add a unit case. The bytes \xe6\x97\xa5 with the filesystem encoding euc_jp must stay $'\346\227\245'.

Checked and holding:

  • Shells: dash, bash and zsh × five stdouts × UTF-8/Latin-1 parents × table/JSON.
    • bash and zsh: 20/20.
    • dash: 7/20, with --json now working for every UTF-8 name (5/5; round 1 managed 2/5).
    • The 13 dash failures are exactly the $'…' cells, so the new README sentence states what was measured. ksh is not installed here.
  • The surrogate guard: a surrogate outside U+DC80..U+DCFF skips the join and stays escaped.
  • Output vs round 1: all 10 tables are byte-identical. JSON changes only in the three cells where a UTF-8 name meets a stdout that cannot show it.
  • ASCII: 24 runs are byte-identical to main.
  • Docs: README:159-160 and handbook:733-735 match the matrix. README is 1461 lines (785.txt +2), and test_readme_sections.py gives 76 passed on the merge with main.
  • Mutants: all four killed: REPORT_ENCODING unset (3), stdout read first (3), no re-decode (3), and stdout's encoding ignored (6). None kills M1, and no test covers a legacy filesystem encoding.
  • Merges: clean with main's tip and with 18 open lane-3 PRs. It conflicts with README, AGENTS.md, docs/cli.md and the handbook: pipx install openreposhape is the second way in and the first on native Windows, git-filter-repo is installed in install.sh's order and never with a bare pip install, and a first draft of the v1.1.0 release notes (#563) #630 in README.md and the handbook, as expected for the README turn.

NITs:

  • N1: run test_785_dash_runs_a_next_that_shows_the_name_as_itself's JSON source under all five stdouts, to pin S2's gain in dash.
  • N2: AGENTS.md:269 says "needs bash"; say "bash or zsh, not sh".

Runs, on the committed merge 4338345c + main 741679e7:

  • the 18 files: 870 passed, 1 skipped;
  • test_shape_doctor.py (once): 437 passed, 3 skipped, 1 xfailed.

Lane: openreposhape-3 (openRepoShape-3)

…TF-8 it spells only where the joined name has the same bytes, so a UTF-8 directory under an EUC-JP, Big5 or EUC-KR filesystem encoding is never written as its twin (#785, review round 3)

Review M1 on PR #792 (RV-a, round 2). Under a legacy multibyte filesystem
encoding a directory named `日` in UTF-8 (`\xe6\x97\xa5`) is read in as three
escaped bytes; round 2 joined them into `日`, which `path_bytes` writes as
EUC-JP `\xc6\xfc`, so both the table and `--json` named another directory.

- `quoted_beyond_ascii` keeps the joined text only where its `path_bytes`
  are the value's own (RV-a's measured fix), and its docstring no longer
  says the bytes are the same either way.
- tests: the escaped UTF-8 bytes of `日` (EUC-JP, Big5) and `€` (EUC-KR),
  with `sys.getfilesystemencoding` patched, stay `$'...'` in the table and
  in `--json`; dash runs `--json` under all five stdouts (N1).
- AGENTS.md point 7: a `next` with `$'…'` needs bash or zsh, not `sh` (N2),
  in the same two lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T12:55:11Z) Re-verification of #792 round 3 by RV-a (opus), on head 26d61255 (a fast-forward of 4338345c): READY. M1 is fixed. No MUST, SHOULD or NIT.

1. No name changes its bytes.

  • The legacy probe: 8/8 cells (table and --json; EUC-JP 日 and Ж, Big5 日, EUC-KR €) now name the UTF-8 directory. Round 2 named the other one in all 8.

  • The end-to-end property check: 20,000 random names per encoding, each spelled by quote_arg and read back by one bash run. Names the doctor spells wrong:

    encoding round 3 round 2
    UTF-8, ASCII, Shift_JIS, GBK, cp1252 0 0
    EUC-JP 0 471
    EUC-KR 0 227
    Big5 5 175

    The 5 Big5 names are ones Python's own big5 codec cannot round-trip (fsdecode then fsencode). Round 1 shows the same 5.

2. Nothing else changed. The 20-cell shells matrix is unchanged line for line from 4338345c, and the table and --json dumps are byte-identical.

3. The new test really exercises the check. test_785_a_utf_8_name_under_a_legacy_filesystem_keeps_its_bytes (tests/test_doctor_non_ascii_paths.py:144) fails all 6 cases against round 2's doctor, and passes here.

4. Only the expected lines moved:

  • shape-doctor.py:957-963 is RV-a's sketch; only the docstring (:947-954) is worded differently;
  • AGENTS.md:268-269 reads "bash or zsh, not sh" (N2);
  • DASH_CELLS (:384) runs 7 dash cells (N1).

Merge-tree is clean with main cbff780c. #630 still conflicts in README and the handbook; #792 lands in lane 3's README turn after #630 and #790.

Runs:

  • test_doctor_non_ascii_paths.py + test_stdout_encoding.py: 173 passed, 1 skipped;
  • test_repo_hygiene.py, test_readme_sections.py, test_readme_windows_quoting_sentence.py and test_cli_reference.py: 178 passed.

Lane: openreposhape-3 (openRepoShape-3)

This was referenced Oct 17, 2026
…side #785's `$'…'` sentence (#785)

README.md conflicted in one hunk, the "Windows, natively" paragraph: main's
#630 rewording (the two-liner is the Windows way in that installs nothing,
pipx's `openRepoShape` runs the same flow) is kept whole, and #785's two
lines on `$'…'` follow it, as they followed the old text.

docs/handbook.html conflicted in seven hunks, each one § Starting marker
(masthead, start, partway, ci, legs, doublepin, park). The prose merged
cleanly: #630's pipx text and #785's `$'&hellip;'` sentence stand together in
`start`. The seven markers were recomputed from the merged README.md with
tests/test_repo_hygiene.py's own readme_segments and readme_fingerprint
(3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3), and handbook_marker_problems
reports none. Every other marker is main's and still current.

README.md is 1476 lines, at its cap: main's 1474 + 2 from
tests/readme-cap.d/785.txt. AGENTS.md is 494 lines, at its cap of 494.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 17, 2026
…nd what they tell git to the `GIT_CONFIG_COUNT` entries the environment holds, so a pasted line keeps the session's maintenance settings and the Windows job's two (#758) (#818)

Against main. #748 landed as `431c8cf8`; this branch merged #748's final head and main (see Fix round 1). After that merge, #723 (`bad05d21`) and #746 (`fc867207`) landed. The branch is refreshed over main `fc867207` as `4a553e17`, one merge commit. Its only conflict was `tests/test_non_utf8_git_reads.py`'s `from conftest import` block, resolved by keeping both sides' names, as rv818 asked. The merged tree differs from main in #818's own 27 files, and #818's count scan flags no file. Tests only: 25 files under `tests/`, no shipped code, no pinned file, no README, handbook, `AGENTS.md` or `docs/cli.md` change.

Two new commits on `b6e15591`: the retarget merge `2908e46f`, then the fix commit `eaa72d9e`. Tests only, as before.

### The retarget: merge commit `2908e46f`
#748 landed as `431c8cf8` from its final head `8368d1f8`, and GitHub moved this PR's base to main. The merge commit has three parents: `b6e15591`, `8368d1f8` and main `fa04d2cd`. #748's head goes in first, so git sees the history the squash dropped.

- **#748's head did not merge clean**, as rv818 found. `tests/test_adopt_empty_symlinks.py` conflicts, because `8368d1f8` holds main `741679e7`, which carries #388.
  - I took w758's earlier resolution of that file, which rv818 checked. Main's side of the file is the same at `974d4827`, `8368d1f8` and `fa04d2cd`, so it still fits.
  - Both sides stay: #388's two targets (`with_each_target`, `SAID`, the `target` parameters) and #758's `symlinks_off()`, built at the call.
  - `test_the_link_given_a_target_adopts_end_to_end` calls `symlinks_off()` too. Git does not flag it, and left as it was the file fails at collection.
- **Main then conflicted in three files** (main was `d17564d9` when I resolved them): `tests/test_clone_just_pushed.py`, `tests/test_doctor_legs_members_next.py` and `tests/test_family_bump_remove_rollback.py`.
  - Main's copy of each was `8368d1f8`'s byte for byte.
  - This branch's copy is `8368d1f8`'s plus #758's own hunks, so this branch's side is taken.
  - Main's next two landings, #809 and #816, merged with no conflict.
- **rv818 MUST 1.** `tests/test_local_remote_dir_urls.py` came with #812 and imported `ALLOW_FILE_PROTOCOL` from `test_family`.
  - Without a change, the file fails at collection: `ImportError: cannot import name 'ALLOW_FILE_PROTOCOL' from 'test_family'`, and none of its 27 cases run.
  - It now imports `allow_file_protocol` and calls `pasted(lines, paste, allow_file_protocol())`.
  - `git grep` finds no other file on main that uses a name #758 removes (`SYMLINKS_OFF`, `SYMLINKS_ON`, `CONFIG_COUNT`, `CONFIG_VALUE`, `PASTE_ENV`, `ALLOW_FILE_PROTOCOL`). `NO_MAINTENANCE`, `QUOTE_PATH_OFF` and `tool_env` change shape, and each is used only in its own file.
- **Checked:**
  - Against main, the merged tree differs in #758's 25 files and in `tests/test_local_remote_dir_urls.py`.
  - Each of the 25 differs by exactly #758's own added and removed lines, `tests/test_adopt_empty_symlinks.py` aside.
  - Collecting the whole suite at the head finds 12,320 tests and no error.

### The fix commit `eaa72d9e`
- **SHOULD 2:** `configure` in `tests/test_adopt_reencoded_files.py`, the seventh append from #758's line 2, now sets what `appended_to_command_scope(os.environ, pairs)` returns. It gives the same entries in the same order, so no test can fail on the old form.
- **SHOULD 3:** the scan also reads a name the module binds to `"GIT_CONFIG_COUNT"` (`CONFIG_COUNT = "GIT_CONFIG_COUNT"`, annotated or not) as the count. It does so in a dict key, a setter's first argument and a subscript.
  - rv818's repro: append the base's two lines to `tests/test_adopt_root_symlinks.py`.
  - The old scan passes. The new one names line 2137.
- **SHOULD 4:** `counts_set_to_a_number` is split into one reader per node type: `in_a_dict`, `in_a_call` and `by_subscript`. The walk takes each node's reader from `READERS`. By hand, no function scores more than 5 on S3776. I could not run Sonar.
- **NIT 5:** `conftest.py` is no longer skipped. It scans clean, at the head and on today's main.
- **NIT 6:**
  - New "sees" cases: `os.putenv("GIT_CONFIG_COUNT", "1")` and a bare `setenv("GIT_CONFIG_COUNT", "1")`.
  - The walk is now `files_off_their_count(directory)`. A new test runs it over a folder of its own, and checks three things:
    - `conftest.py` and a helper module are read;
    - a `.txt` file is not;
    - a file `OWN_COUNTS` allows is named for fewer counts as well as for more.
  - Four of rv818's six surviving mutants now fail a case: `putenv` dropped, the bare-name setter dropped, `!=` changed to `>`, and `glob("test_*.py")`.
  - The fifth, `conftest.py` scanned, is now how the code works, and putting the skip back fails the new test.
  - The sixth, the count's value taking any string, still survives: it is a stricter scan. Git refuses a count that is not a number (`bogus count in GIT_CONFIG_COUNT`, exit 128), so a test that holds one fails on its own.
- **NIT 7, in part:**
  - An empty count (`""`) is now a count of its own. Git 2.43 and 2.47.1 read it as no entries at all. Measured: `GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0=a.b GIT_CONFIG_VALUE_0=c git config --get a.b` exits 1.
  - The docstring says the scan reads the count's value as a constant only.
  - Left out:
    - a value built by a call (`str(len(pairs))`) or held in a variable;
    - an int;
    - `dict()`, or `environ={}` by keyword, handed to the helper;
    - `setenv` with keyword arguments;
    - an annotated subscript.

    rv818 judged only `str(len(entries))` likely, and telling it from a count worked out needs what the call means, not how it is spelled.
- **NIT 8:** the paste case deletes `GIT_CONFIG_PARAMETERS` first. Measured on the merge before the fix commit, with `GIT_CONFIG_PARAMETERS="'probe.param'='p'"`, the case failed (`Left contains one more item: 'probe.param=p'`). It passes now.
- **NIT 9, left as it is:** main's three appends built by hand each work the count out right: `tests/test_family_head_lost_gitmodules.py:97`, `tests/test_family_bootstrap_gate_order.py:135` and `tests/test_family_add_spellings_remedies.py:137`. Each of those files is also changed by another open PR (#828 `fix/761-add-move-back-gitmodules`, #820 `fix/762-validate-family-remedies-texts`, `fix/763-holder-docs-agree`), so converting them here would only add conflicts.
- **NIT 10:** the comment on `NO_MAINTENANCE` now says its `maintenance.auto` and `gc.auto` repeat the session's two, which reach these gits in the command scope. Only `gc.autoDetach` adds a setting. The two stay, so the constant alone still keeps maintenance off for these gits.
- **NIT 11:** the count was in lane 5's #473 summary, not in this body. The seven sets held 2,401 distinct passes, not 2,609: the 208-test run repeats three files of the 256-test run.

### Tests at `eaa72d9e`
All runs went through `run9.sh`, with the real `gh` unreachable. All passed, none skipped:
- **The 26 test files the PR changes against main**, in one full-file run each: 2,206 passed (1,108 + 1,098).
  - These are #758's 24 test files, `tests/test_local_remote_dir_urls.py` and `tests/test_adopt_reencoded_files.py`.
  - The count matches the collection exactly.
- **The 43 files that import any of those 26, with `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`:** 1,646 passed (825 + 821).
- **The same merge made on main `d17564d9`** (a local commit, `c16a7e1a`, never pushed), before the fix commit: `tests/test_local_remote_dir_urls.py` and `tests/test_no_auto_maintenance.py`, 55 passed.

### Fails on the base
The new tests were run against the base's scan and walk, as `b6e15591` has them, with `GIT_CONFIG_PARAMETERS` set: 6 failed, 30 passed. The six:
- the paste case;
- the `nothing`, `a-name-bound-to-it`, `an-annotated-name` and `a-name-by-subscript` cases;
- the walk test.

The three other new cases (`a-bare-setenv`, `putenv`, `a-name-bound-to-another-key`) are there to kill mutants, and they pass on any correct scan. MUST 1 fails on main's form at collection, as above. SHOULD 2 and NIT 10 change no behaviour.

### Mutants
I made 26 mutants. 25 are caught, and one survives as rv818 said it would:
- **22 on the scan and the paste case**, each run against `tests/test_no_auto_maintenance.py`:
  - the delenv dropped;
  - `putenv` dropped from `SETTERS`, or the bare-name setter;
  - the name branch of `is_the_count` dropped, or any name taken as the count;
  - `""` no longer a count;
  - the `Assign` names dropped, or the `AnnAssign` names;
  - no names collected;
  - each reader ignoring the names;
  - each of the three readers dropped from `READERS`;
  - `conftest.py` skipped;
  - `!=` changed to `>` or to `<`;
  - `glob("test_*.py")`, or `glob("*")`;
  - `by_subscript` taking any value;
  - the walk pointed at an empty folder.

  The last passes on a clean tree, like any change to an assertion that nothing is found. It is caught by rv818's SHOULD 3 repro, where the real walk fails and the mutant passes.
- **The survivor:** the count's value taking any string. It makes the scan stricter (see NIT 6).
- **`configure`:** with `appended_to_command_scope({}, pairs)`, the scan names the file. With the pairs dropped, three of its tests fail.
- **MUST 1:** main's import put back fails collection with `ImportError`.

### Merge checks at `eaa72d9e`
I ran `git merge-tree --write-tree` against `origin/main` (`fa04d2cd`) and the 53 other `fix/*` and `l3-*` heads on origin:
- **Clean:** `origin/main` and 43 heads.
- **#792 (`l3-785-non-ascii-paths`)** merges clean, but `tests/test_doctor_non_ascii_paths.py` imports `PASTE_ENV`. Whichever PR lands second changes `:50` (the import), the `:266` docstring and `:268` to `paste_env()`.
- **#723 (`fix/663-non-utf8-name-tests`), #746 (`fix/672-bump-leg-branch-and-push-line`, which now holds #723) and #806 (`fix/757-non-utf8-test-dirs`)** conflict only in the `from conftest import` block of `tests/test_non_utf8_git_reads.py` (and of `tests/test_adopt_git_reads.py`, for #806). Both sides add names, so keep them all.
- **Seven heads conflict with main itself, not with this PR:**
  - `l3-732-latin1-section` and `l3-733-ignored-sections` (`tests/test_doctor_legs_members_next.py`);
  - `fix/712-validate-pins-contracts`, `fix/766-spelled-paths` and `fix/798-adopt-legs-asked-first`;
  - `l3-677-pins-hand-step-wip` and `l3-678-naming-checkout-wip`.
- I scanned each clean merged tree's changed test files for a name #758 removes, or a count the scan flags. Only #792 shows up.
- **Main moved after the push.** #723 landed as `bad05d21`, and the two now conflict in one place: the `from conftest import` block of `tests/test_non_utf8_git_reads.py`. The fix is to keep both sides' names: `ORG, PROJECT, REPO, appended_to_command_scope, cloned_just_pushed, git, run_script, skip_message, skip_where_fsencode_gives_a_surrogate_no_byte, windows_fsencode`. Every other change merges in. On that merged tree, with the block resolved that way, the scan flags no file, and no file uses a name #758 removes.

On `deb029c9` a probe test imported `PASTE_ENV` and pasted `git config --show-origin --list` through `paste()`:
- `PASTE_ENV` held `GIT_CONFIG_COUNT=1` (`protocol.file.allow`), a stale `GIT_CONFIG_KEY_1=user.email` left from collection, and no `GIT_CONFIG_GLOBAL`.
- `os.environ` at the call held a count of 4 and the session's global file.
- The pasted git listed one command-line entry, `protocol.file.allow=always`, and `git config --get maintenance.auto` exited 1. So a pasted line ran with git's automatic maintenance on, in both scopes, as the issue said.

`paste_env()` replaces it. It reads `os.environ` at each call (minus `PYTHONPATH`), adds the identity and `PYTHONDONTWRITEBYTECODE`, and appends `protocol.file.allow` with `appended_to_command_scope`. The six uses call it. The import-time copy also predated the session's `gh` guard on `PATH`, as `test_doctor_next_is_a_command.py`'s docstring says of its own environment; a pasted line now gets the `PATH` of the call. That docstring now says `paste_env()` reads at the call too.

- **The seven.** Six are on this base, each now one call to `appended_to_command_scope`: `test_adopt_git_reads.py`, `test_adopt_nested_attributes.py`, `test_adopt_ignored_shape_files.py`, `test_adopt_root_symlinks.py`, `test_bootstrap_after_bump.py`, `test_family_bump_remove_rollback.py`. The seventh, `test_adopt_reencoded_files.py`, came with #743 and is converted in fix round 1. `test_clone_just_pushed.py:96` stays, as the issue says: it is the helper's own test.
- **The other literals.** Each sets a count of its own and is laid over `os.environ`, where the session's two settings and the Windows job's two sit, so each replaced them (`NO_MAINTENANCE` put the two maintenance keys back and dropped the job's two). I read each; each is converted, none left:
  - `test_family.py`: `ALLOW_FILE_PROTOCOL` is now `allow_file_protocol()`; its two importers (`test_siblings_family_name.py`, `test_family_siblings_placement.py`) and the four uses in `test_family.py` call it.
  - `test_family_dry_run.py`: `NO_MAINTENANCE` is a tuple of entries, appended by `isolated()`.
  - `test_non_utf8_git_reads.py`: `QUOTE_PATH_OFF` is a tuple of entries, which `tool_env()` appends; `user_name()` appends too.
  - `test_adopt_empty_symlinks.py`: `SYMLINKS_OFF` is `symlinks_off()`.
  - `test_adopt_root_symlinks.py`: `SYMLINKS_OFF` and `SYMLINKS_ON` are gone; the four tests that set the first call `links_as_files` and the two that set the second call `told_git`, which already appended.
  - one-off environments: `test_scaffold_reuse_seeded_legs.py`, `test_adopt_split_branch.py`, `test_scaffold_free_text_values.py`, `test_adopt_plan_yaml_quoting.py`, `test_setup_project_py.py`, `test_setup_sh.py`, `test_doctor_next_is_a_command.py`.
  - The two tests that build a held environment on purpose to see it kept (`test_adopt_git_reads.py`, `test_clone_just_pushed.py`) now build it with `appended_to_command_scope({}, ...)`.

Measured with a probe on a throwaway bare remote after a push (18 loose objects): the hand-built options and `clone_just_pushed`'s give the same clone, one pack and no loose object, with `upload-pack` and `pack-objects` in `GIT_TRACE`, and `origin` keeps the plain path. A plain local clone of the same remote keeps all 18 loose objects. For a clone that is not recursive the helper's `insteadOf` rewrite is inert, because no submodule is cloned; the helper adds one place for the options, not a different read.
- `test_bump_leg_dry_run_truth.py:98`, the private bare copy of the session's spec remote: a session remote. `cloned_just_pushed(..., bare=True)`; the fixture now asserts the copy is bare.
- `test_bump_leg_dry_run_truth.py:110`, `private_work`, a clone of that private remote, which tests push tags into: `cloned_just_pushed(..., recurse=False)`.
- `test_doctor_legs_members_next.py:1828` and `:1929`, `Ghost`, a clone of the first member's checkout in the test's own holder copy: not a remote, and the hand-built `-c protocol.file.allow=always` was dead weight on a direct clone. Both call `clone_member`, one helper over `cloned_just_pushed(..., recurse=False)` that asserts the legs stay empty.
- `test_family_bump_to_checks.py:235`, a clone of `a_private_remote`, a bare repository filled by a push: `cloned_just_pushed(remote, work, "main", recurse=False, env=env)`.
- `test_doctor_pin_restore_merges.py:714` is **left as it is**: a `--filter=blob:none` clone of a file URI. The helper takes a path, and has no way to pass `--filter`.

- `test_family_dry_run.py`: the comment on `NO_MAINTENANCE` says what the entries are for, the gits `isolated()` hands an environment to, whose global file `excluding()` replaces, and that the session fixture covers the module's own setup commits, which pass no `env=`.
- `tests/conftest.py`: the two i645 replacements, "repacks once its estimate, taken from `objects/17` alone, is above 256 loose objects, so two loose objects there are enough", and the `objects changed` sentence. **#713's body holds the matching sentence and needs the same two replacements; I cannot edit a body.**

`tests/test_no_auto_maintenance.py` has 17 new cases (28 in the file, 11 before):
- `test_a_pasted_line_keeps_the_entries_the_environment_holds_at_the_call`: the environment holds two entries as the Windows job does, then the two settings as the session fixture appends them, set after the doctor tests were imported. `git config --show-origin --list` through `paste()` lists the four, in order, then `protocol.file.allow`, and reads both settings from the session's global file.
- `test_no_test_sets_a_git_config_count_of_its_own`: reads every `tests/*.py` but `conftest.py` with `ast` and fails a `GIT_CONFIG_COUNT` set to a number (dict, keyword, `setenv`/`setdefault`/`putenv`, subscript) and an `appended_to_command_scope` call handed a dict written out. Three files are allowed the ones that build a held environment on purpose, with a count each in `OWN_COUNTS`.
- 15 cases that hold the scan itself: seven spellings it must see, eight it must leave (`env.get("GIT_CONFIG_COUNT", "0")` is a read).

Runs at the head `b6e15591` through `run9.sh` (the real `gh` unreachable), all passed, none skipped:
- `test_no_auto_maintenance.py`, `test_doctor_legs_members_next.py`, `test_doctor_next_is_a_command.py`: 208 passed.
- `test_clone_just_pushed.py` and `test_doctor_validator_rows.py` (it imports the doctor file), with those three: 256 passed, run one commit earlier (the later commit only moved two blank lines and reworded one failure message).
- The sweep files: `test_adopt_git_reads.py`, `test_adopt_nested_attributes.py`, `test_adopt_root_symlinks.py`, `test_adopt_ignored_shape_files.py`, `test_bootstrap_after_bump.py`, `test_family_bump_remove_rollback.py`, `test_adopt_empty_symlinks.py`, `test_adopt_split_branch.py`: 445 passed.
- `test_non_utf8_git_reads.py`, `test_family.py`, `test_siblings_family_name.py`, `test_family_siblings_placement.py`, `test_scaffold_reuse_seeded_legs.py`, `test_scaffold_free_text_values.py`, `test_adopt_plan_yaml_quoting.py`: 999 passed.
- `test_family_dry_run.py`, the five files that import its `isolated` (`test_family_bump_to_a_tag.py`, `test_family_bump_to_checks.py`, `test_family_bump_to_local_only.py`, `test_family_refuse_up_front.py`, `test_family_remove_gitmodules.py`) and `test_bump_leg_dry_run_truth.py`: 216 passed.
- `test_setup_project_py.py`, `test_setup_sh.py` and their importers (`test_adopt_shape_pin_and_work_dir.py`, `test_dry_run_asks_remotes.py`, `test_clone_url_bytes.py`, `test_setup_by_hand_commands.py`): 313 passed.
- `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`, `test_doctor_pin_restore_merges.py`, `test_git_helper_identity.py`: 172 passed. `render-cli-reference.py --check` is ok.

**On the base (`deb029c9`)**, with the new `test_no_auto_maintenance.py` copied in: 2 of the 17 fail, the paste case and the scan (it finds 12 files). The other 15 hold the scan itself and pass on any tree.

Each of the new hunks was broken in a throwaway and run against its tests:
- `paste_env`: a literal count of 1, a snapshot taken at import, and `appended_to_command_scope({}, ...)`: each fails the paste case, and the first and third the scan too.
- The 18 sites that call `appended_to_command_scope(os.environ, ...)` (or `env`), each with `{}` in place of the environment: each fails the scan. Sites: the six of line 2, `allow_file_protocol`, `isolated`, `tool_env`, `user_name` (non-UTF-8), `symlinks_off`, `git_reads_github_from`, `github_at`, `user_name` (free text), `test_adopt_plan_yaml_quoting.py`, the two setup tests, `test_doctor_next_is_a_command.py`.
- The scan: eight changes (ignore dicts, keywords, `setenv` calls, subscripts, `appended_to_command_scope({}`; flag `os.environ`; flag a read; allow fewer in `OWN_COUNTS`) each fail a scan case.
- The clone sites: dropping `bare=True` from the private remote, `recurse=False` from `clone_member`, and `"main"` from the checks clone each fail. Two **survive as equivalents**: `recurse=False` in `private_work` (the spec leg has no `.gitmodules`, so a recursive clone of it is the same clone) and `env=env` in the checks clone (a clone reads no config the isolated environment changes).
- The two comments cannot be mutated.

`git merge-tree --write-tree` of this head against `origin/main` (`974d4827`), #748 (`deb029c9`), #713 (`979bb394`) and every `fix/*` and `l3-*` head on origin (46 refs):
- **Clean:** #748, #713, and 24 of the 46 heads: 21 `fix/*` heads (every lane-5 PR but #723 and #756, and most of lane 4's) and `l3-677`, `l3-678`, `l3-rehearsal-v1.1.0`.
- **`origin/main`: one file, `tests/test_adopt_empty_symlinks.py`** (four hunks), against #388's landing (`5c990708`). The parent #748 merges clean, so this arrives with the retarget. Resolve by keeping main's lines and my `symlinks_off()`. Two more spots need the same change and git does not flag them:
  - `tests/test_adopt_empty_symlinks.py::test_the_link_given_a_target_adopts_end_to_end` uses `SYMLINKS_OFF`, which this change removes: give it the `off` parametrization.
  - `tests/test_adopt_reencoded_files.py::configure`, the seventh hand-built append, goes to `appended_to_command_scope(os.environ, pairs)`.
  - `tests/test_family_head_lost_gitmodules.py::on_disk` (on main) is one more hand-built append: `{**env, **appended_to_command_scope(env, [("core.ignorecase", ignorecase)])}`. The scan does not read for hand-built appends, so it is not required, but it is the same thing.

  I made that merge in a scratch tree (main at `bf1f099b`), resolved it as above, and ran the files this change touches that main also changed, with `test_adopt_empty_symlinks.py`, `test_adopt_reencoded_files.py`, `test_family_head_lost_gitmodules.py`, `test_no_auto_maintenance.py` and the files that import `isolated` or the doctor helpers: 653 and 245 passed, in two runs. The scan finds no other file of main's with a count set by hand.
- **#723 (`fix/663-non-utf8-name-tests`):** `tests/test_non_utf8_git_reads.py`, the `from conftest import` block, where both add names.
- **`l3-732`, `l3-733`:** `tests/test_doctor_legs_members_next.py` (the `from conftest import` block) and `tests/test_adopt_empty_symlinks.py`.
- **19 more** (`fix/664`, `fix/729` (#756) and 17 `l3-*` heads): `tests/test_adopt_empty_symlinks.py` only, because they carry main's #388 as main does.
- **Not a textual conflict, but it breaks on merge:** `l3-785-non-ascii-paths` imports `PASTE_ENV` (`tests/test_doctor_non_ascii_paths.py:47` and `:223`); it must call `paste_env()`. #812 (`fix/693-local-remote-dir`) landed first, with `tests/test_local_remote_dir_urls.py` importing `ALLOW_FILE_PROTOCOL` (`:51`, `:349`). This PR's retarget merge makes it call `allow_file_protocol()` (rv818 MUST 1).

- A pasted `git submodule update --init` still clones a local path: `paste()` takes no remotes, so no `insteadOf` rewrite reaches it (rv662's "build it from `read_through_git`"). It runs under the session's maintenance settings now, which is what the issue asks.
- Three more appends built by hand beside `appended_to_command_scope`, in files this base does not hold: `tests/test_family_head_lost_gitmodules.py:97` (main), `tests/test_family_bootstrap_gate_order.py:135` (#682), `tests/test_family_add_spellings_remedies.py:137` (#767). The scan reads for counts set to a number, not for appends worked out by hand.

Review: lane 2's independent review (issuecomment-6236874291) and its re-verification of eaa72d9, READY (issuecomment-6238781272); its new SHOULD is taken in #848, and 4a553e1 merges main over eaa72d9. CI (Brett Heap's ruling 27): H0 is the head 4a553e1 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base fc86720; and the local gate passed on its predicted squash tree 9d6c099 over main d8b8eb3: the adopt_empty_symlinks, adopt_git_reads, adopt_ignored_shape_files, adopt_nested_attributes, adopt_plan_yaml_quoting, adopt_reencoded_files, adopt_root_symlinks, adopt_split_branch, bootstrap_after_bump, bump_leg_dry_run_truth, clone_just_pushed, doctor_legs_members_next, doctor_next_is_a_command, family, family_bump_remove_rollback, family_bump_to_checks, family_dry_run, family_siblings_placement, local_remote_dir_urls, no_auto_maintenance, non_utf8_git_reads, scaffold_free_text_values, scaffold_reuse_seeded_legs, setup_project_py, setup_sh, siblings_family_name, repo_hygiene, agents_md_codes, cli_reference sets, 2359 passed. Landed on Brett Heap's word of 2026-10-17T21:47:28Z, in lane 5's session (posted on #473 as issuecomment-6241758069): "merge lane 5's v1.1.1 PRs once reviewed and green".

Closes #758

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T23:46:16Z) RV792m's re-check of #792's merge round (26d61255..1ccb3dd7): READY, provided CI tests passes (it was still running). No MUST, SHOULD or NIT. The README and handbook resolution is the stated one, all 23 handbook markers are current, the delta outside README, the handbook and tests/readme-cap.d/ equals the reviewed one under cmp, and lane 2's #821 did not come in.

Main has moved since, to 112eeed, and lane 5's #738, a README-turn PR, is landing now. So #792 is predicted and gated again over the main of its README turn before it asks.


RV792m re-check of #792 (#785) at head 1ccb3dd7

RV792m verdict: READY for the merge round 26d61255..1ccb3dd7. I found no MUST, SHOULD or NIT item. Every claim in the brief held when I measured it. I made no GitHub write.

#792's head is still 1ccb3dd7391c29e426fd4488d3557cbb151ed6a8. gh pr view 792 reports state OPEN, but mergeable and mergeStateStatus were both UNKNOWN when I last read them.

Items

1. The merge tree matches git merge-tree --write-tree 26d61255 2e476a77.

  • That merge conflicts, as predicted, in README.md and docs/handbook.html only. The conflicted tree is 4032594c2f45.
  • The head's tree is c920cf35b14e689a448c30eac6a883cb31589a84. git diff --name-only 4032594c c920cf35 lists only README.md and docs/handbook.html. Every other file, including the auto-merged AGENTS.md, shape-doctor.py and tests/test_shape_doctor.py, is byte-identical to git's own merge.
  • The README resolution is the stated one. git diff 2e476a77 1ccb3dd7 -- README.md is exactly +2 lines, the two $'…' lines.
  • In the handbook, git diff -U0 2e476a77 1ccb3dd7 shows 7 marker lines changed and the 3-line $'&hellip;' sentence added at docs/handbook.html:741-743, nothing else. The other 16 markers are unchanged from main.

2. The README hunk reads correctly.

3. All 23 handbook markers are current. I ran handbook_marker_problems, _handbook_markers, readme_segments and readme_fingerprint from the head's tests/test_repo_hygiene.py:

  • problems: 0, markers: 23, current: 23 of 23.
  • The 7 § Starting markers are all 3d4a01ffadc322f88dfffd9cef50b1c48c58b9b3, at docs/handbook.html lines 475, 615, 1092, 1129, 1191, 1436 and 1735, which matches the writer's list.
  • Cross-check without the helpers: an awk cut of that README segment, passed through git hash-object --no-filters, gives 3d4a01ff…. Main's own segment hashes to 45a3f886…, which is what main's 7 markers carried.

4. The delta equals the reviewed one.

  • Outside README.md, docs/handbook.html and tests/readme-cap.d/, git diff -U0 2e476a77 1ccb3dd7 and git diff -U0 bf1f099b 26d61255 are identical under cmp.
  • The 772 figure counts the diff text including file headers (grep -v '^@@\|^index'). It is 750 +/- lines without them. Both measures are identical, and git patch-id --stable gives 2d43f4c1… for both.
  • The only differences are two hunk-header offsets, from lines main added above them: AGENTS.md 268 vs 270, and shape-doctor.py +9972 vs +9964.

5. Lane 2's #821 did not come in.

6. Tests on 1ccb3dd7, under a stub gh on PATH exiting 97, with GH_TOKEN=bogus and an empty GH_CONFIG_DIR:

  • The 8 docs files, via /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=…/bt-docs -o tmp_path_retention_policy=failed tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_readme_sections.py tests/test_readme_windows_quoting_sentence.py tests/test_release_workflow.py tests/test_repo_hygiene.py tests/test_setup_by_hand_commands.py tests/test_update_add_codes_documented.py:
    300 passed in 59.23s
  • tests/test_doctor_non_ascii_paths.py with the same flags, --basetemp=…/bt-nonascii: 126 passed in 108.87s (0:01:48).
  • git diff --check 2e476a77 1ccb3dd7 exits 0, and so does git diff --check origin/main 1ccb3dd7.

Extra runs on the predicted squash (scratch commit 1bb5d656, tree b7048467…, on main c100843b). I added these because the merge auto-merged shape-doctor.py against #730, #752 and #759, and the extra files below are the ones those commits and the workflow changes touched:

  • The 8 docs files, tests/test_doctor_non_ascii_paths.py, tests/test_stdout_encoding.py, tests/test_doctor_pin_restore_merges.py, tests/test_validator_child_encoding.py and tests/test_workflow_concurrency.py: 653 passed, 1 skipped, 1 xfailed in 313.43s (0:05:13).
  • tests/test_shape_doctor.py, run once: 437 passed, 3 skipped, 1 xfailed in 812.11s (0:13:32).
  • The marker check on that tree also reports 23 of 23 current, with 0 problems.

7. Merge with current origin/main.

  • Fresh fetch, git merge-tree --write-tree origin/l3-785-non-ascii-paths origin/main is clean, exit 0. Predicted squash tree: 2d171f1e2ef046d618ef4104ab31e668629b9eb4, with origin/main at d8b8eb339ad90e2c229134ee7d320dfd713117e.
  • Main moved twice while I worked. The last move is d8b8eb33, #family.py's reftable refusal, which touches only scripts/family.py and two tests. Against that tree, README.md, docs/handbook.html, AGENTS.md, tests/readme-cap.d/ and shape-doctor.py are identical to the c100843b prediction I tested.
  • Against c100843b the predicted squash tree is b7048467e05f90e53fdcedf54d0880e4e25587b9. It is clean and differs from main in exactly the 10 PR files, with the same patch-id as the PR's net diff.
  • I did not re-run tests on 2d171f1e itself.

Checks, gh pr checks 792:

  • SonarCloud Code Analysis: pass (58s).
  • tests-git-bash: pass (1m57s).
  • package: pass (30s).
  • tests: pending.

I did not wait for CI.

Scratch paths

  • Clone, no checkout: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV792m
  • Scratch root: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV792m-scratch/
    • head worktree at 1ccb3dd7: RV792m-head
    • predicted-squash worktree at local commit 1bb5d656, never pushed: RV792m-pred
    • marker script: RV792m-markers.py
    • test logs: RV792m-pred-tests.log, RV792m-sd-tests.log
    • basetemps: bt-docs, bt-nonascii, bt-pred, bt-sd
    • stub gh: RV792m-stub/gh
    • delta files: RV792m-delta-*.txt, RV792m-full-*.diff

Lane: openreposhape-3 (openRepoShape-3)

Brett Heap and others added 2 commits October 18, 2026 00:11
…and the PRs since 2e476a7 (#785)

This brings in #818's (#758) `paste_env()` in tests/test_doctor_legs_members_next.py,
which replaced the module-level dict `PASTE_ENV`, and the PRs on main since
2e476a7. No conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… from paste_env(), the function #818 (#758) made of PASTE_ENV, so the file collects on main again

#818 replaced the module-level dict `PASTE_ENV` in
tests/test_doctor_legs_members_next.py with `paste_env()`, built at the call.
This file still imported `PASTE_ENV`, so over any main since aec3b38 it
failed to collect (ImportError) and pytest exited 2; #792's CI was green only
because its run merged a main from before #818. `environ()` now builds from
`paste_env().items()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T00:24:44Z) RV792p's re-check of #792's fix round (1ccb3dd7..4948c7a4): READY, provided CI tests passes (run 39082233093 was pending). The merge of bee84d55 is exact, and the fix is the 3 lines that move tests/test_doctor_non_ascii_paths.py from PASTE_ENV to #818's paste_env(). Each git the tests run now gets the session's no-maintenance settings and the file protocol. On the predicted squash over 7b21059 the file runs 126 passed, test_no_auto_maintenance.py passes, and the handbook markers are current with README at 1476 of 1477.

#792 asks for its README turn once tests is green, gated again over the main of that time.


RV792p re-check of #792 (#785) at head 4948c7a4

VERDICT: READY. No MUST items.

Items

  • SHOULD: none.
  • NIT 1. tests/test_doctor_non_ascii_paths.py:50 lists paste_env before commit_in. Every other import of this module sorts its names. A sorted line fits in 79 columns: commit_in, detached_member, emptied_with_its_repository_kept, paste_env,. No linter in the repo checks it, and no test failed on it.

Checks 1 to 4

1. The merge is exact.

  • git merge-tree --write-tree 1ccb3dd7 bee84d55 exits 0 with tree c5694c966170d10c85379350d638a7c36a6476b0.
  • 86d4c708^{tree} is the same tree.
  • The parents of 86d4c708 are 1ccb3dd7 and bee84d55.
  • bee84d55 is an ancestor of origin/main (exit 0).

2. The fix is only that.

3. Semantics.

4. #818's rule.

Check 5: tests on the predicted squash

I fetched fresh. origin/main is 7b210593 and the PR head is 4948c7a4. The prediction is git merge-tree --write-tree origin/main 4948c7a4, exit 0, then commit-tree. That gives scratch commit 9274238a2f3aeb210b5ee3ba135959f5573bc0b4 with tree 4a4f1bf00e2e19108053c1c242b9bd1ca924e6c4. I never pushed it, and the worktree stayed clean.

I ran each command under the stub gh (exit 97) with GH_TOKEN=bogus and an empty GH_CONFIG_DIR. GIT_ALLOW_PROTOCOL was unset. I used /usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed --basetemp=<scratch>/bt-RV792p-<name>, with timeout 1500.

Files Result line
tests/test_doctor_non_ascii_paths.py 126 passed in 76.02s (0:01:16)
tests/test_no_auto_maintenance.py 36 passed in 5.90s
tests/test_stdout_encoding.py 47 passed, 1 skipped in 11.85s
the 8 docs files 331 passed in 42.22s
  • The one skip in tests/test_stdout_encoding.py is tests/test_stdout_encoding.py:286: the default this is about is Windows' own.
  • The 8 docs files were test_agents_md_codes.py, test_cli_reference.py, test_readme_sections.py, test_readme_windows_quoting_sentence.py, test_release_workflow.py, test_repo_hygiene.py, test_setup_by_hand_commands.py and test_update_add_codes_documented.py.
  • I did not run tests/test_shape_doctor.py, as you said.

Marker check, using the predicted tree's tests/test_repo_hygiene.py helpers:

Check 6: CI and the tree

  • The predicted squash tree 4a4f1bf00e2e19108053c1c242b9bd1ca924e6c4 is over main 7b2105937567da30231db6a42749f258af48d9f9.
  • gh pr checks 792 -R opensoft/openRepoShape showed this, read at the time:
Check State
SonarCloud Code Analysis pass (1m11s)
package pass (29s)
tests-git-bash pass (2m16s)
tests pending
  • The pending tests job is the Linux job, run 39082233093, which I did not wait for. gh pr view 792 showed headRefOid 4948c7a4, state OPEN, mergeable and mergeStateStatus UNKNOWN.
  • origin/main is 2 commits past bee84d55: 5fc049b7 and 7b210593. They merge into the branch cleanly in the prediction.

Scratch paths

  • Clone: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV792p
  • Scratch root: /workspace/projects/openRepoShape-worktrees/_lane3-scratch/RV792p-scratch
  • Predicted squash worktree: .../RV792p-scratch/RV792p-wt
  • Worktree of 86d4c708: .../RV792p-scratch/RV792p-wt-merge
  • Probe worktree, holding the untracked probe test: .../RV792p-scratch/RV792p-wt-probe
  • Probe scripts: .../RV792p-scratch/RV792p-probe.py and .../RV792p-scratch/RV792p-probe2.py
  • Stub gh: .../RV792p-scratch/RV792p-stub/gh
  • Run scripts: .../RV792p-scratch/RV792p-runA.sh and .../RV792p-scratch/RV792p-runB.sh
  • Logs, all in .../RV792p-scratch/: RV792p-nonascii.log, RV792p-nam.log, RV792p-stdout.log, RV792p-docs.log
  • Basetemps: .../RV792p-scratch/bt-RV792p-*
  • I made no GitHub write and ran no rm -rf. No test processes of mine are still running.

Lane: openreposhape-3 (openRepoShape-3)

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 01:55
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

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

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

@brettheap
brettheap merged commit 5cbec22 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the l3-785-non-ascii-paths branch October 18, 2026 01:56
brettheap added a commit that referenced this pull request Oct 18, 2026
…book: under Git Bash (`MSYSTEM` set) the lines that follow the host are quoted for `sh`, through one question, `host_shell_dialect`, so the doctor's lines, setup's family lines and `family.py`'s `NEXT`, `switch -c`, `add` and `cd` lines paste whole in the shell that printed them, and README's Windows sentence says so (#711) (#821)

The tools that quote a printed line for the host chose PowerShell's
dialect wherever `os.name` was `nt`, and it is `nt` under Git Bash too,
which runs the Windows Python. So Git Bash was handed
`'C:\Users\Jane O''Neill'`, which it reads as `C:\Users\Jane ONeill`, a
bare `C:\work\Atlas`, which it reads as `C:workAtlas`, and `Copy-Item`
and `$env:PYTHONPATH = ...;`, which it has no command for. Brett Heap
chose option (a) of #532's Git Bash question at 2026-10-16T13:07:34Z.

- scripts/repo_shape.py: `host_shell_dialect(name=None, environ=None)`
  answers "nt" (PowerShell) on a Windows host whose environment has no
  `MSYSTEM`, or an empty one, and "posix" (`sh`) everywhere else, Git
  Bash and every MSYS2 shell included. `shell_quoted` and `shell_line`
  ask it when they are given no `platform`; an explicit one keeps its
  meaning. Its docstring names the limit: a PowerShell started from
  inside Git Bash inherits `MSYSTEM` and is answered `sh`'s.
- setup-project.py carries a copy, as it does of the quoting, because it
  runs before any checkout of the standard exists. `SHELL_BARE`, decided
  at import from `os.name`, is gone: `shell_word` asks the question at
  each call and takes the alphabet and the escape from one answer, which
  the old code read twice. Setup's family lines follow it (L2-29).
- shape-doctor.py: `quote_arg`, `restore_from_head`, `validator_command`,
  `bump_leg_command`, `checkout_notes`, `submodule_update_next` and
  `copy_command` ask it, so Git Bash gets `sh`'s quoting, `cp` and
  `env PYTHONPATH=...`. `scaffold_command` keeps its entry point and
  interpreter on the machine (`python setup-project.py` on Windows, from
  either shell) and takes only its quoting from the question. The
  interpreter is `PYTHON`, the machine's, on every line.
- scripts/family.py is unchanged: its host-dialect lines go through
  `shell_quoted` and `shell_line`, `_print_next_steps`'s `cd` included.

Tests: tests/test_git_bash_dialect.py emulates PowerShell, Git Bash, an
empty `MSYSTEM` and POSIX on any host (each module under test gets an
`os` whose `name` is the host's; the real `os.name` is never touched)
and reads each line back with `shlex.split` or `powershell_words`: the
question's answers, setup's copy held to them, `shell_quoted` and
`shell_word` held to one answer on every host, and the lines of setup,
family.py and the doctor. #724's inventory in
tests/test_readme_windows_quoting_sentence.py now fails a function that
quotes for the host and reads `os.name` itself.
tests/test_family_printed_quoting.py reads the host through the
question, and tests/test_setup_project_py.py unsets `MSYSTEM` where it
patched the gone `SHELL_BARE`.

Fix round 1 (rv711, no MUST): no pinned script may import
`host_shell_dialect` (siblings.py's older-library test strips it too, and
the skew test reads all seven pinned scripts that import from repo_shape);
the guard counts the doctor's nine line builders, the functions that hand a
`platform` on to a quoter, as quoters, so `copy_command(s, d, os.name)` in a
new row fails it, and names a quoter's body by `Class.method`; the Git Bash
rows read back a kept repository's line and a lane trailer spelled by the
line's own quoter. A lane name cannot hold an apostrophe (`LANE_NAME_RE`),
so that trailer test hands the quoter one through a stand-in.

README and the handbook: README's Windows sentence keeps "Printed
commands are quoted for a POSIX shell, so paste them into Git Bash",
names `init`'s `cd` line among `family.py`'s, and says the exceptions
are "quoted for the shell they are printed in, PowerShell or Git Bash, on
Windows." #785's sentence after it says a path `shape-doctor.py` cannot
print as itself is written `$'...'` "Off Windows and in Git Bash": its
`quote_arg` now asks `host_shell_dialect` too, so Git Bash is handed
`sh`'s spelling of such a path. The handbook's Windows panel says the
same, and adds that a shell started from inside Git Bash inherits
`MSYSTEM`, so a PowerShell started there is handed Git Bash's quoting,
and a cmd.exe started anywhere else is handed PowerShell's. The seven
markers of README's § Starting a project in a new organisation move to
its new blob, e0d2b672, and tests/readme-cap.d/711.txt files the two
lines this adds (README 1476 -> 1478, the cap 1477 -> 1479).

NIT 6 of lane 2's review of #808, which landed first: the one-question
guard's `calls_to` counts a quoter handed on as a value (`host_values`)
as well as a call, so `map(shell_quoted, w)` beside a branch on
`os.name` is seen.

Stacked on #724, whose final head 949fd4d is merged here and which has
landed as 2000488, merged here with main. #630, which rewrote the
README and handbook around this sentence, has landed too (190133d), and
so has #792 (#785, 5cbec22), which wrote into the same paragraph and
into `quote_arg`; main d7befb9 is merged here with both. The copied
`scripts/repo_shape.py` changes, so it rides in v1.1.1's shape bump.

Closes #711

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant