Skip to content

bump-leg.py and update-shape.py hand the project's own validators an explicit PYTHONIOENCODING, so a real bump or apply on a project named 日本 is no longer refused as bump-leg-validators-red or update-validators-red under a cp1252 stdout (#254) - #281

Merged
brettheap merged 3 commits into
mainfrom
fix/validator-child-encoding
Oct 12, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-5 (openRepoShape-5)
Claim: #254 (comment)

What was wrong

A real bump-leg.py (not --dry-run) and update-shape.py apply run the
PROJECT'S own scripts/validate-pins.py and scripts/validate-manifest.py as
child processes after they rewrite the pin. A child takes its stdout
configuration from its environment (PYTHONIOENCODING, then the locale), not
from the reconfigure its parent did in #214, and the validators are
digest-pinned shape copies that print the display name with a plain print.
So on a stdout whose code page cannot hold the name (PYTHONIOENCODING=cp1252,
or the cp1252 with surrogateescape that Windows gives any stdout that is not
the console), validate-manifest.py raised UnicodeEncodeError at
manifest ok: <name>, exited 1, and the tool refused and rolled back a bump
the pins had no quarrel with. The refusal blamed the validators.

What changed

Decisions, and the evidence for each

Captured case: the child writes UTF-8, and the tool reads UTF-8. #230 gave
run_validator errors="replace" and no encoding=, "so that codec is the
one that reads it back". That was right only while the child wrote in the
locale's codec. Once the child is told UTF-8, the locale's codec misreads it:
'日本'.encode('utf-8') read as ascii is six U+FFFD, and read as cp1252 is
'\xe6\u2014\xa5\xe6\u0153\xac'. So encoding="utf-8" is
explicit. Dropping it fails
test_a_real_bump_reads_the_validators_as_utf_8_whatever_the_locale_is
(mutant bump-decode-with-locale). The words then reach the person through
the tool's own stdout, which #214 made safe: manifest ok: \u65e5\u672c on
a cp1252 stdout, manifest ok: 日本 on UTF-8 and cp932.

An operator's PYTHONIOENCODING is replaced for the captured pipe and
respected for the inherited one.
The operator's value describes the stream
their own words land in. The captured pipe is the tool's, so respecting
cp1252:strict there would put the failure back one pipe away from the
console. The inherited fd IS the operator's stream, so the child is told what
that stream is. The tests run under cp1252:strict and cp1252:surrogateescape
(the brief's two), plus cp1252:replace (an operator's own lenient handler:
?? before and after), cp932:strict (a Japanese Windows) and utf-8:strict.

Inherited case: the stdout's own encoding, never UTF-8, and built-in handler
names only.
UTF-8 bytes on a cp1252 pipe would be a different mistake. A
child cannot use a handler only its parent registered:
PYTHONIOENCODING=cp1252:openreposhape.surrogateescape-then-backslashreplace
still STARTS the child and fails with LookupError at the first character it
cannot encode. test_the_handler_a_child_would_not_know_is_never_passed_on
runs that child and asserts both halves. At the point the child is built,
main() has already turned a strict stdout into backslashreplace and a
surrogateescape one into the registered handler, so the helper maps
strict, surrogateescape, surrogatepass and the registered name to
backslashreplace, and keeps a handler that never raises.

One difference, and it is small: a byte of a file name that was not UTF-8,
read in as U+DC80..U+DCFF, is written by a validator as the text \udcff
where the tool writes the byte back. A validator prints one only for a path it
lists.

Evidence

Fails on 60d08a1: with the new test files copied into a throwaway worktree at
60d08a1, 42 failed, 12 passed (the 12 are controls that pass either way:
the five --dry-run bumps, cp1252:replace and utf-8:strict for both tools,
cp932:strict for apply, and #230's own two C-locale tests). The decisive
lines:

  • bump, cp1252:strict and cp1252:surrogateescape: exit 2, REFUSED bump-leg-validators-red: scripts/validate-manifest.py exited 1 after the rewrite, UnicodeEncodeError: 'charmap' codec can't encode characters in position 13-14.
  • apply, the same two: exit 2, REFUSED update-validators-red: after the rewrite, scripts/validate-manifest.py refused.
  • the guard in test_git_output_decoding.py flags run_validator once its
    EXEMPT row is gone.

Passes on the head (5757e29):

  • the new file (51), test_stdout_encoding.py (48), test_bump_leg.py (23),
    test_update_shape.py (77), test_update_shape_add.py (17),
    test_git_output_decoding.py (3), test_repo_hygiene.py (50) and
    test_cli_reference.py (13): 281 passed, 1 skipped (the existing
    Windows-only default-stdout test);
  • test_shape_doctor.py: 181 passed;
  • test_family.py and test_update_shape_add_ignored.py: 102 passed;
  • the head merged with main d89fffa, on the files above plus
    test_update_shape_pr_base.py: 392 passed, 1 skipped.

One extra finding, fixed by the same two changes. Under LC_ALL=C PYTHONUTF8=0 (an ASCII stdout; UTF-8 mode has to be switched off by hand, as
it is on by default in a C locale) validate-pins.py raises on the …
(U+2026) it prints, so on 60d08a1 a real bump of a project named Atlas was
refused as well: exit 2, REFUSED bump-leg-validators-red: scripts/validate-pins.py exited 1, UnicodeEncodeError: 'ascii' codec can't encode character '\u2026'. On the head the same run exits 0. This is why the
C-locale bump test fails on 60d08a1 at the PINS validator.

Emulation caveat. The cp932:strict BUMP also fails on 60d08a1 when the
test runs on Linux, but only as an artefact: the child wrote cp932 and a UTF-8
locale cannot read it, where a Japanese Windows' own locale could. It is not
the defect and is not cited as one. The Windows-specific runs here are
emulations (PYTHONIOENCODING=cp1252:surrogateescape); I did not run anything
on Windows
, and these tests first run on Windows in this PR's tests-windows
job. The two C-locale tests are POSIX-only (skipif os.name == "nt") and skip
where the platform reads UTF-8 under LC_ALL=C PYTHONUTF8=0.

Mutation probes. Eleven hunks reverted one at a time in a throwaway
worktree, the whole new file run each time: all eleven killed.

hunk reverted failed first killers
captured_child_environment sets nothing 6 both captured_child unit tests, bump cp1252:strict, cp1252:surrogateescape, cp932:strict, the C-locale bump
captured value utf-8 with no handler 2 the two captured_child unit tests
bump-leg.py drops env= 5 the three bump cp1252/cp932 runs, the C-locale bump, the AST guard
bump-leg.py drops encoding="utf-8" 1 the C-locale bump
update-shape.py drops env= 4 apply cp1252:strict, cp1252:surrogateescape, the C-locale apply, the AST guard
inherited handler passed verbatim 14 strict/surrogateescape/surrogatepass unit and child cases, apply cp1252:surrogateescape, the C-locale apply
inherited always backslashreplace 11 ignore/namereplace/replace/xmlcharrefreplace cases, apply cp1252:replace
replace dropped from the never-raises set 5 the replace cases, apply cp1252:replace
inherited encoding forced to utf-8 27 every non-UTF-8 case
inherited encoding not canonicalised 3 ANSI_X3.4-1968, latin-1, the no-stdout case
inherited drops the no-stdout guard 1 test_a_stdout_that_is_not_there_leaves_the_environment_alone

Byte-identical where nothing was wrong. A real bump and an apply, from
60d08a1 and from the head, on the same project, under utf-8:strict,
utf-8:surrogateescape and no PYTHONIOENCODING, named Atlas and named
日本: twelve pairs, stdout, stderr and exit code identical once the run
directory and the shas are masked.

Sibling merge-trees (git merge-tree --write-tree HEAD <branch>, local
tips of lane 5's branches and every open branch that touches bump-leg.py,
update-shape.py or shape_materialize.py): clean against
fix/yaml-quote-boundaries, fix/yaml-block-key-nul,
fix/yaml-flow-mapping-keys, fix/scaffold-referent-chain-links,
fix/adopt-check-unsafe-path, fix/adopt-plan-quote-yaml-keywords,
fix/adopt-record-submodule-placement, fix/adopt-template-values-utf8,
fix/bump-leg-rollback-every-failure, fix/bump-leg-tracked-workflows-only,
fix/leg-path-yaml-scalars, fix/update-shape-add-dangling-symlink,
fix/update-shape-pr-base-branch and fix/update-shape-pr-create-failure.
One conflicts: fix/adopt-root-kept-symlinks (adopt-project.py and
scripts/shape_materialize.py), and it conflicts the same way with origin/main
itself, so it is not this change.

Left out

Review

An adversarial review of 5757e29 found no blocker. Its one should-fix was a
false sentence in this body's "Left out", corrected above and filed as #280.

  • It ran the inherited case on Python 3.10, 3.12, 3.13, 3.14 and 3.15rc2,
    including UTF-8 mode and the C locale. In each 28-run cell the head had 0
    failures; main had 8, or 12 under LC_ALL=C PYTHONUTF8=0.
  • It compared 30 real runs between main and the head under UTF-8: stdout and
    stderr were identical.
  • The head merged with c3f4987 passed: 264 passed, 1 skipped.

Three nits are kept as they are:

  • The inherited child's stderr. It follows the parent's STDOUT encoding,
    because PYTHONIOENCODING sets both streams. With a console stdout and a
    redirected stderr, the child writes UTF-8 to a cp1252 stderr.
  • errors="replace" in bump-leg.py's run_validator. It is defensive.
    The child now always writes UTF-8, so a strict mutant survives every test.
  • The \udcff text. This is the difference stated above.

Fix round 1 (tests-windows)

The first head, 5757e29, failed tests-windows: 5 failed and 2477 passed, and macOS and Linux passed. All five failures were test_an_apply_on_a_project_named_in_japanese_lands_under_any_stdout. Its assert_the_update_landed compared the applied copy with the upstream clone's WORKING-TREE file. On Windows that file is CRLF, because the shared test helper commit_upstream_changes rewrites it with text-mode write_text. Git stores the blob as LF, which is what apply writes. The test now compares with the committed blob (git show <b>:<path>), as test_update_shape.py already does, and no product code changed.

  • Emulation on Linux. The helper was made to write CRLF: the old assertion failed 6 apply cases, and the new one passed them.
  • Targeted runs. The seven targeted files gave 274 passed and 1 skipped on the head merged with main b30595c (97c4280).
  • A candidate follow-up. commit_upstream_changes leaves the upstream clone's working-tree copy CRLF on Windows for the rest of its module. Writing the appended bytes in binary would remove the trap at its source, but the helper is shared and outside this change.

Closes #254

🤖 Generated with Claude Code

…explicit PYTHONIOENCODING, so a real bump or `apply` on a project named `日本` is no longer refused as `bump-leg-validators-red` or `update-validators-red` under a cp1252 stdout (#254)

A real `bump-leg.py` and `update-shape.py apply` run the project's own
`scripts/validate-pins.py` and `scripts/validate-manifest.py` as children. A
child takes its stdout configuration from its environment, not from the
`reconfigure` its parent did (#214), and the validators are digest-pinned
shape copies that print the display name with a plain `print`. Under a stdout
whose code page cannot hold it (`PYTHONIOENCODING=cp1252`, or the cp1252 with
`surrogateescape` that Windows gives a stdout that is not the console)
`validate-manifest.py` raised `UnicodeEncodeError` at `manifest ok: <name>`,
exited 1, and the tool refused and rolled back a bump the pins had no quarrel
with, naming the validators.

- shape_materialize.py: `captured_child_environment()` and
  `inherited_child_environment()` build the child's environment, and each tool
  passes it as `env=`. Built-in handler names only: a child given
  `<encoding>:<a handler only its parent registered>` starts, and then raises
  `LookupError` at the first character it cannot encode, so #214's
  `openreposhape.surrogateescape-then-backslashreplace` is never passed on.
- bump-leg.py `run_validator` CAPTURES the output. The child is told
  `utf-8:backslashreplace`, whatever the operator set (their value describes
  their stdout, and this pipe is the tool's), and the pipe is read with
  `encoding="utf-8", errors="replace"` instead of the locale's codec, which
  #217 chose while the child still wrote in it. The words reach the person
  through the tool's own escaped stdout.
- update-shape.py `run_validator` lets the child INHERIT stdout, so it is told
  the stdout's OWN encoding, with that stdout's handler where the handler
  never raises (an operator's `replace` still prints `??`) and
  `backslashreplace` for the rest: `strict` and `surrogateescape` raise on the
  name. UTF-8 bytes on a cp1252 pipe would be another mistake.
- tests/test_git_output_decoding.py: the `run_validator` row leaves EXEMPT,
  because the call now names both `encoding` and `errors`.

Also fixed by the same two changes, and not named in the issue: under
`LC_ALL=C PYTHONUTF8=0` (an ASCII stdout) `validate-pins.py` raised on the
U+2026 it prints, so a real bump or `apply` was refused there whatever the
project was called.

Unchanged: the runs under `utf-8:strict`, `utf-8:surrogateescape` and the
default stdout of a UTF-8 locale are byte-identical to 60d08a1's for both
tools, on a project named `Atlas` and one named `日本`, once the run directory
and the shas are masked. One difference, and it is small: a byte of a file name
that was not UTF-8, read in as U+DC80..U+DCFF, is written by a validator as
the text `\udcff` where the tool writes the byte back; a validator prints one
only for a path it lists.

Left out: the shape copies a project runs itself. `scripts/bootstrap.py`,
`scripts/validate-manifest.py` and the family root's `siblings.py` are
digest-pinned and print the display name raw, so `make bootstrap` and
`make validate` on a project named `日本` fail under a cp1252 stdout at their
own `print`, before any child is run (checked: `bootstrap.py` raises at
`bootstrap: <name> (<root>)`). That is a shape change, and it reaches projects
through `update-shape.py`.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-11T23:42Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 11, 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 1 day and 13 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR prevents project validators from failing solely because a child process inherits an incompatible stdout configuration: captured validators are forced to emit and be decoded as UTF-8, while inherited validators receive the actual stdout encoding with a child-known non-raising handler, backed by extensive cross-platform regression tests.

Sequence diagram for safe validator output during bump and apply

sequenceDiagram
    participant Tool as bump-leg.py or update-shape.py
    participant Validator as Project validator
    participant Stdout as Tool stdout

    alt Captured validator output
        Tool->>Validator: subprocess.run(env=captured_child_environment())
        Validator-->>Tool: UTF-8 output
        Tool->>Tool: Decode with encoding="utf-8", errors="replace"
        Tool->>Stdout: Print escaped validator output
    else Inherited validator output
        Tool->>Validator: subprocess.run(env=inherited_child_environment())
        Validator->>Stdout: Write using stdout encoding and safe handler
    end
Loading

File-Level Changes

Change Details Files
Standardize the encoding environment for validator child processes according to whether output is captured or inherited.
  • Add a UTF-8/backslashreplace environment for captured validator output.
  • Add an environment derived from the inherited stream's canonical encoding and a child-known non-raising handler.
  • Preserve the parent environment while selectively overriding PYTHONIOENCODING and leave it unchanged when stdout is unavailable or unusable.
scripts/shape_materialize.py
Make bump-leg validator execution explicitly UTF-8 end-to-end.
  • Pass the captured-child environment to project validators.
  • Decode captured stdout and stderr as UTF-8 with replacement while retaining defensive error handling.
scripts/bump-leg.py
Make update-shape validator execution safe for the tool's actual stdout.
  • Pass the inherited-child environment when validators write directly to the parent's stdout.
  • Ensure validators use built-in handlers rather than parent-only custom handlers.
update-shape.py
Add regression coverage for validator encoding behavior across locales, code pages, handlers, and real tool workflows.
  • Test real bump and apply operations for Japanese project names under cp1252, cp932, UTF-8, and POSIX C-locale configurations.
  • Test helper environment construction, child process output, canonicalized codec names, handler fallback, absent stdout, and preservation of unrelated environment variables.
  • Add an AST guard ensuring validator-launching tools pass an explicit environment.
  • Update the existing decoding guard to recognize explicit encoding and error arguments.
tests/test_validator_child_encoding.py
tests/test_git_output_decoding.py

Assessment against linked issues

Issue Objective Addressed Explanation
#254 Ensure that a real bump-leg.py operation can run the project's validators and succeed for display names such as 日本 under a cp1252 or otherwise incompatible stdout, rather than being refused and rolled back as bump-leg-validators-red. ✅
#254 Ensure that update-shape.py apply can run the project's validators and succeed for display names such as 日本 under an incompatible stdout, rather than being refused and rolled back as update-validators-red. ✅
#254 Configure validator child processes in the two tools with encoding settings appropriate to how their output is handled: UTF-8 with matching decoding for captured bump output, and the parent's canonical stdout encoding with a child-known non-raising error handler for inherited apply output. ✅

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

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he upstream's committed blob, not its working-tree file, so `apply` on a project named `日本` no longer fails on Windows where the helper that rewrote that file left it CRLF (#254)

assert_the_update_landed read `<upstream>/templates/assembly-root/scripts/validate-manifest.py`
from the upstream clone's working tree. commit_upstream_changes rewrites that
file with Path.write_text, which on Windows turns every \n into \r\n, while the
repository's `* text=auto eol=lf` stores the blob as LF. `update-shape.py apply`
writes the blob, so the project's copy was LF and the comparison failed in all
five encodings of tests-windows on #281. Product code is unchanged: the same
comparison now reads `git show <B>:<path>`, as test_update_shape.py already does.

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

Copy link
Copy Markdown

@brettheap
brettheap merged commit b421e6b into main Oct 12, 2026
5 checks passed
@brettheap
brettheap deleted the fix/validator-child-encoding branch October 12, 2026 00:55
brettheap added a commit that referenced this pull request Oct 12, 2026
…s `_entry_findings` composed with #255's, and this PR's pair renamed `unwritable_findings`/`unwritable_remediation`

#231 (6f45bf9) puts each entry's path first in `_entry_findings` and
returns `unsafe_remediation` ahead of the rest; this branch appends each
value `execute` writes into a file that no file can hold. The two hunks
are its docstring's order sentence and its return. The sentence keeps
main's order and appends this PR's clause; the return is
`(findings + submodule_findings + unwritable_findings,
  unsafe_remediation + remediation + unwritable_remediation)`, so the
paths still come first, as #196 needs.

This PR's local pair `value_findings, value_remediation` is renamed
`unwritable_findings, unwritable_remediation`. #261's branch (ca21b57)
binds `value_findings, value_remediation` in the same function, from
`_manifest_findings`: a resolution keeping both lines under one name
would silently drop one PR's findings. With the rename it cannot.

The rest of main (#254/#281, #238/#287, #203/#236, #204/#227,
#249/#250, #241, #215 and the earlier merges) merges with no conflict.
The resolution is the lane's re-verify one (`resolve-main.py`, 385
passed on 6f45bf9), rename aside.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…s `plan-unsafe-path` comes first in the one list of entry findings, and #190's relative urls and NOTEs come last in it, with `execute` still resolving them from the remotes it would create

Main since ede15ce: 6f45bf9 (#196 via #231, `entry_path_problem`) and
b421e6b (#254 via #281, PYTHONIOENCODING for the validators that bump-leg.py and
update-shape.py run). Two files were changed on both sides: adopt-project.py
and tests/test_adopt_submodules.py.

THREE CONFLICT HUNKS, all in adopt-project.py, and each keeps both sides:
1. Before `_entry_findings`: #196's `NOT_EXTRACTED`, its remediation and
   `_unsafe_path_findings` are kept whole. `_entry_findings` keeps #190's
   signature, `local_remote_dir` and the three-tuple with the NOTEs. Its
   docstring now names #196's order (each entry's path, then coverage,
   then the legs and mounts, then the submodules) and #190's NOTEs.
2. `_entry_findings`' return:
   `(findings + submodule_findings, unsafe_remediation + remediation,
   notes)`. This is #196's remediation first and #190's NOTEs, with
   `_submodule_findings(plan, source, tree, local_remote_dir)` as #190
   calls it.
3. `_refuse_what_check_finds`' docstring: #196's "after the entries' paths,
   which are the first of these findings", then #190's paragraph on the
   remotes this run would create.

Auto-merged, #196's: the module docstring; `_unsafe_path_findings` and its
neighbours; `_extract_leg`; `_mount_the_legs`; `_leg_paths`; the rest of
`_refuse_what_check_finds`; and five hunks of tests/test_adopt_submodules.py.

Proofs:
- Each side's +/- lines are unchanged by the merge, except the two
  resolved spots (the `_entry_findings` docstring and its return). That
  holds in adopt-project.py, and tests/test_adopt_submodules.py is
  identical both ways.
- `_entry_findings` is called by `cmd_check` and `_refuse_what_check_finds`
  (both three-tuples) and by #190's own test, and by no test of main's.
- No top-level name is bound twice, and no line over 79 columns is added
  (42 on main, 42 merged).

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…, #242 via #308, #262 and #263 via #313) into fix/adopt-record-submodule-placement

Brings in ede15ce (#238), 6f45bf9 (#196), b421e6b (#254), c0ed615 (#242)
and 71b58a1 (#262, #263) on top of 6d8d88b. No rebase. ede15ce changes
shape-doctor.py. 6f45bf9 and 71b58a1 change adopt-project.py and
scripts/shape_materialize.py. b421e6b changes scripts/shape_materialize.py
too, giving a project's validators an explicit PYTHONIOENCODING. All of
these are files this branch changes. c0ed615 changes scripts/family.py
alone.

No conflicts. Checked:

- adopt-project.py: #196 gives `check` and `execute` one test for an
  entry's path, `entry_path_problem`, as the finding `plan-unsafe-path`.
  #262 and #263 add `_root_shape_findings` to `_leg_findings`, for a kept
  root path where the shape writes. Both reach `execute` through
  `_refuse_what_check_finds`, before any leg exists and before
  `_commit_the_split` appends the record. This branch's `submodule_record`,
  `_resolution_text`, the record's preamble, `_answered_legs`,
  `_kept_in_a_leg`, `_commit_the_split`, `cmd_execute`,
  `submodule_plan_problems`, `_submodule_findings`, `Plan`, `y`, `emit`
  and `walk` are unchanged by the merge. `_root_shape_findings` is asked
  only beside a `legs:` mapping, so the doctor's placement plan, whose
  `legs:` is a list, still goes through `_leg_findings(plan, None)`.
- shape-doctor.py: #238 changes `validator_row` and `run_validator`. This
  branch's `check_placement`, `recorded_submodules`, `audit_leg`,
  `placement_row`, `placement_verdict` and `Context` are unchanged by the
  merge.
- scripts/repo_shape.py is untouched by these commits, so the record's
  writer and reader, and `double_quoted` in `submodule_record`, are
  3e86200's.
- Both sides added lines to the module docstrings of adopt-project.py and
  shape-doctor.py. Every line either side added is in the merge.
- No top-level name is bound twice in adopt-project.py,
  scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py,
  shape-doctor.py, validate-manifest.py, update-shape.py, scripts/family.py
  or scripts/bump-leg.py. Each merged module's names are the base's plus
  what either side added. Every definition only one side changed is that
  side's text.
- AGENTS.md is untouched: 472 lines, under the cap of 472.
- The branch's diff against main is still #189 alone: 12 files, +1670
  -157, file by file the same as against 6d8d88b.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
Main brings five commits since 6d8d88b:

- ede15ce (#238 via #287): shape-doctor.py's validator rows;
- 6f45bf9 (#196 via #231): `entry_path_problem`, one test of an entry's
  path for `check` and `execute`;
- b421e6b (#254 via #281): bump-leg.py and update-shape.py give the
  validators an explicit PYTHONIOENCODING (scripts/shape_materialize.py
  gains `captured_child_environment` and `inherited_child_environment`);
- c0ed615 (#242 via #308): family.py's `add` and `bump` under excludes;
- 71b58a1 (#262, #263 via #313): `check` finds, and `execute` refuses, a
  kept root path where the shape writes a directory or its `shape/`
  copy, and the materializer never writes over a copy.

It merged with no conflict. Only adopt-project.py changed on both sides.
Every other file the merge changes against this branch is main's, and
every file it changes against main is this branch's. In adopt-project.py
the merge's diff against this branch is main's own diff (559 lines), and
its diff against main is this branch's own diff (784 lines). Main's
twenty hunks there, by where they land:

1-2. the module docstring: `check` validates each entry's path and keeps
     no path in the root where the shape writes (6f45bf9, 71b58a1);
3. the `from shape_materialize import` block: `CASE_ASIDE`,
   `MOVE_THE_LEG`, `first_and_more` (71b58a1);
4-6. `_leg_findings`: a docstring line, the call to
     `_root_shape_findings`, and the new block after it (`ROOT_BLOCKS_SHAPE`,
     `ROOT_BLOCKS_COPY`, `_KeptTree`, `_shape_blocks`, `_blocked_sentence`,
     `_fix_the_kept_path`, `_root_shape_findings`) (71b58a1);
7. after `_submodule_findings`: `NOT_EXTRACTED`, `entry_path_problem`,
   `_misread`, `_refused_by_policy` and `_unsafe_path_findings` (6f45bf9);
8-11. `_entry_findings`: its docstring, and the entries' path findings
      first (6f45bf9);
12-13. `_extract_leg`: `literal:` on every `--paths-from-file` line
       (6f45bf9). This branch changed `_extract_leg` as well, further
       down: it reads the commit count and the digest before
       `_push_the_leg`. The two sets of lines do not touch, and the
       merged function has both;
14-15. `_mount_the_legs`: `git --literal-pathspecs rm` (6f45bf9);
16-17. `_leg_paths`: collected, not judged (6f45bf9);
18-20. `_refuse_what_check_finds`: its docstring and remediation
       (6f45bf9, 71b58a1).

The new findings of 6f45bf9 and 71b58a1 are `_entry_findings`, which
`cmd_execute` reaches only through `_refuse_what_check_finds`, before
`_create_leg_remotes`. So they are refused before any leg exists, and no
notice is due. The materializer's new `materialize-copy-blocked` is
raised after both legs are pushed, inside `_commit_the_split`, when a run
reaches the materializer without `check`'s finding. That makes it a late
failure, which this branch reports with the notice and exit 2. Main's
`test_with_check_skipped_execute_stops_at_the_copy_and_overwrites_nothing`
expects it to escape `cmd_execute`, so this merge breaks that test, as
9092fda's merge of #200 broke the mount-guard tests. The next commit
makes the test read the reported refusal.

No top-level name is bound twice in adopt-project.py, scaffold-project.py,
scripts/repo_shape.py, scripts/shape_materialize.py or tests/conftest.py.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…, #238, #196, #254, #242 and #262/#263 merge with three adopt-project.py hunks kept on both sides, and #262's `_CASE_ASIDE` import kept as the name's one binding

origin/main moved b30595c -> 71b58a1:
- be2790e (#204 via #227), update-shape.py;
- 6d8d88b (#203 via #236), family.py;
- ede15ce (#238 via #287), shape-doctor.py;
- 6f45bf9 (#196 via #231): adopt-project.py `entry_path_problem`,
  scripts/shape_materialize.py;
- b421e6b (#254 via #281): scripts/shape_materialize.py, bump-leg.py,
  update-shape.py;
- c0ed615 (#242 via #308), family.py;
- 71b58a1 (#262 and #263 via #313): adopt-project.py
  `_root_shape_findings` and scripts/shape_materialize.py
  `copy_in_the_way`. It fixes what this branch's pull request listed as
  follow-ups (i) and (ii): a source's own `shape/` where the shape
  writes its copy, and a plain FILE where the shape needs a directory.

Conflicts, all in adopt-project.py, each resolved by keeping both sides:
- The module docstring's `check` paragraph. 71b58a1 adds "no path kept
  in the root where the shape writes ..." before the submodule clause,
  and this branch adds the symlink clause after it. Both are kept, in
  that order.
- `_entry_findings`' docstring. 6f45bf9 and 71b58a1 give the order
  "each entry's path, then coverage, then each entry's `leg:`, the two
  mount paths and what the root keeps where the shape writes, then the
  source's own submodules". This branch adds "then each symlink the plan
  keeps in the root that a shape file cannot be written through".
- `_entry_findings`' return line. 6f45bf9 returns
  `unsafe_remediation + remediation`, and this branch appends
  `_root_symlink_findings(plan, source, tree)` to the findings. The
  merge returns both.

Found after the textual merge, and resolved here so that no commit binds
a name twice: 71b58a1 (through 65a9262) imports #198's phrase as
`_CASE_ASIDE` in the "#172's shape half" import block, and b37159a had
imported it on a line of its own. Main's import is kept; this branch's
line and its five-line comment go.

scripts/shape_materialize.py merges with no conflict. `place` gets
71b58a1's `copy_in_the_way` inside its collision block and this branch's
`_refuse_unwritable` after it, as 71b58a1's message foresaw.

Checked on the merged tree:
- `_entry_findings` still hands `_leg_findings` the tree, and the
  symlink findings still come last in its list.
- AST: no top-level name is bound twice in adopt-project.py,
  scaffold-project.py, scripts/repo_shape.py or
  scripts/shape_materialize.py, and none is loaded but bound nowhere.
  Main changed the signature of no function this branch calls.
- Both sides are intact by +/- line sets, apart from the three hunks and
  the import above.

The next two commits fix what this merge breaks, each with a revert
line. #262's rule and this branch's both name a symlink kept AT the
copy under `shape/`. And where the shape's copy lands is now
`copy_in_the_way`'s question, asked before this branch's own check of
it.

Lane: openreposhape-1 (openRepoShape-1)
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