Repository navigation
update-shape.py: apply keeps each of the manifest's shape: pin lines at the indentation it already has and reads the rewrite back, so a project.yaml indented four spaces takes the update instead of being rolled back under the validators' lockstep remedy (#208) - #224
Conversation
…nes at the indentation it already has and reads the rewrite back, so a `project.yaml` indented four spaces takes the update instead of being rolled back under the validators' lockstep remedy (#208) `rewrite_manifest` FINDS `commit:` and `tree_sha256:` inside `shape:` by their stripped text, so it recognises the block at any indentation the reader accepts, and it then WROTE them back at a fixed two and four spaces. A `project.yaml` whose `shape:` block is indented four spaces (`tree_sha256:` at six) came back with `commit:` at two among siblings at four, which `load_yaml` refuses. Both validators went red, `apply` rolled back, and the refusal blamed the project: each `yaml-unparsable` line carried the lockstep remedy ("advance the gitlink ...") and `update-validators-red` said a tool that left a project red would have moved the cost to somebody else's pull request. `check` kept offering the same `apply`. - Each of the two lines is written as its own leading whitespace + key + the new quoted value. Only the value moves; the diff to the manifest is those two values, whatever indentation the block was written in. - What `rewrite_manifest` returns has been READ BACK with `parse_yaml`: it must give `shape.commit` and `shape.digests.tree_sha256` equal to the target, or it is refused as `update-manifest-unrecognised` before a byte is written, with a remedy that says the fault is this tool's line rewrite. The one layout a line rewrite cannot carry, a value on the line below its key, reads and validates, and is now refused here by name instead of by the validators with a remedy about the pin. - The read-back blames this tool only when the fault is its own: a manifest that did not read before the rewrite was red before `apply` ran, and is rewritten as before and left to the validators, which name that line. - `family.yaml` goes through the same function; its block is covered too. Unchanged: a manifest indented the way the scaffold writes it. Measured on main a5b09c3 and on this branch against one upstream commit, two byte-for- byte copies of one scaffolded project: the two project trees after `apply`, `check`'s output and `apply`'s output are identical (`diff -r`, `diff`). Left out: `rewrite_shape_pin` matches ` tree_sha256:` with exactly two spaces, so a `contracts/shape-pin.yaml` whose `digests:` child is indented four is refused as `update-pin-unrecognised`, loudly and rolled back, on main and here alike (measured); `scripts/bump-leg.py`'s `rewrite_pin` matches the same way (read, not run). That file is written by the scaffold and says not to hand-edit it, and its refusal names the file, so it is not changed here. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-3, 2026-10-11T18:58Z) @codex review |
|
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 4 hours by commenting |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewer's GuideThe PR fixes Flow diagram for safe manifest pin rewritingflowchart TD
A["apply: rewrite_manifest"] --> B["Find shape pin lines by stripped key"]
B --> C["Preserve each line's existing indentation"]
C --> D["Read rewritten manifest with parse_yaml"]
D --> E{"Pins equal target?"}
E -->|Yes| F["Return rewritten manifest"]
E -->|No| G{"Manifest read before rewrite?"}
G -->|Yes| H["Refuse update-manifest-unrecognised"]
G -->|No| I["Leave failure to validators"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-2, 2026-10-11T19:29:51Z) This lane reviewed PR #224 at bb79295 read-only, in its own detached worktrees. It authored nothing and holds no claim. Verdict: MERGEABLE WITH NITS (0 P1, 0 P2, 1 P3, 2 nits). No sequencing constraint. #208 is fixed. The test: a scaffolded Atlas with its
P3-1.
Nits:
Notes:
Tests:
Merges ( Report and drivers: |
|
(lane openRepoShape-3, 2026-10-11T19:30Z) independent review (codex is out of quota): ready; PR body corrected ("rolled back", not "before a byte is written"), no code change. |
|
…ng-paths main moved to 4835fb4: #224 (e7d1720, update-shape.py), #244 (404760f, repo_shape.py `_split_flow`) and #247 (4835fb4, the plan writer escapes every character that is not printable). The textual merge is clean. The next commit takes #247's one assertion that this branch's spelling changes. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, #213 and #199 land beside #190, and the `repo_shape` import list carries #199's `double_quoted` with #190's `redacted`, `remote_is_a_path`, `remote_key` and `remote_local_path` Main moved from 54aa69a to 4835fb4: e7d1720 (#208 via #224), 404760f (#213 via #244) and 4835fb4 (#199 via #247). One conflict hunk, in adopt-project.py, and it takes BOTH sides: 1. The `repo_shape` import list. #199 adds `double_quoted`, which `y` now spells every quoted plan scalar with; #190 adds `redacted`, `remote_is_a_path`, `remote_key` and `remote_local_path`, which its relative-url check calls (`_Resolution.same_as` and `__str__`, `_default_remote`, `_relative_url_findings`). Main's line is kept as main wrote it, and #190's four names follow `parse_yaml` in the list's order. Either side's list alone leaves a name the merged code calls undefined. Merged with no conflict: - adopt-project.py, #199's other hunks: `_PLAIN_RE` ending at `\Z`, `y` through `double_quoted`, `_spelled`'s docstring, `_shown` and the four report lines that print through it, and `write_paths_listing` in the `shape_materialize` import and in `_extract_leg`. None touches a #190 line. #190's findings and NOTE already print every path and url through `_spelled`, which is what `_shown` falls back to for a name that is not printable. - scripts/repo_shape.py: #199's escape table (`DOUBLE_QUOTED_ESCAPES` with `\r`, `CODE_POINT_ESCAPES`, `double_quoted`, `_escaped`, `_escape_at`, the one-pass `_unescape`), with its module-docstring line and its `_BLOCK_SENTINEL` comment, and #213's `_split_key_flow` and `_split_flow`, are all in the YAML reader at the top of the file. #190's `REMOTE_PORT_RE`, `REMOTE_HOST_ALIASES` and `remote_key` are in the remote helpers some 1,400 lines below, and call nothing main changed. Each side's added and removed lines are the same before and after the merge, byte for byte. - update-shape.py (#208), scripts/shape_materialize.py (#199) and main's three test files are main's blobs; #190's three test files are this branch's. #190's added and removed lines against 4835fb4 are its lines against 54aa69a, but for the import hunk. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…) into fix/adopt-record-submodule-placement Brings in e7d1720 (#208), 404760f (#213) and 4835fb4 (#199) on top of 54aa69a. No rebase. #199 landed first, so this branch is the one that lands second, and it takes #199's side of the double-quoted escape section, as cd2a622's message and lane openRepoShape-2's review of e167d8b both said it would. Conflicts, and how each was resolved: - scripts/repo_shape.py, two hunks (textual): the escape section and `_unescape`'s docstring. Both sides added `CODE_POINT_ESCAPES`, `_HEX_DIGITS` and an `_escape_at` whose body is the same line for line, and both reworded the docstring; #199 also added `r` to `DOUBLE_QUOTED_ESCAPES` and the writer, `double_quoted` with `_escaped` and `_ESCAPE_OF`. The auto-merge had also left this branch's own copies of `CODE_POINT_ESCAPES`, `_HEX_DIGITS` and `_escape_at` beside main's. Resolved as main's file, byte for byte, plus this branch's one change to it that is not about escapes: 379763c's `path_spelling_fault` section and its line in the module docstring, applied with `git apply`. `git diff origin/main -- scripts/repo_shape.py` is that 51-line addition alone. Merged without a conflict, and checked: - adopt-project.py: main's `y` now quotes with `double_quoted`, and this branch's `submodule_record` still quotes with `_quoted`, which reads main's table now, so it writes a carriage return as `\r`, where it wrote `\x0d`. The two writers give the same text for every character to U+2FFF, every surrogate and seven more (14,343 compared, 0 different). The next commit removes `_quoted`. - scripts/shape_materialize.py: main's `write_paths_listing` and this branch's `path_spelling_fault` import and `LEG_SEGMENT_REASONS`. Every merged module compiles. On this merge, tests/test_windows_paths.py, test_adopt_plan_escapes.py, test_yaml_flow_quotes.py, test_manifest_validator.py and the record's two name tests in test_adopt_submodules.py: 218 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-notice It merged with no conflict. Main brings six commits since 54aa69a: - e7d1720, update-shape.py (#208 via #224); - 404760f, scripts/repo_shape.py's double-quoted flow items (#213 via #244), with tests/test_yaml_flow_quotes.py; - 4835fb4, adopt-project.py's plan writer escapes (#199 via #247), with tests/test_adopt_plan_escapes.py; - b538015, scaffold-project.py's leg paths against the shape's own paths (#197 via #234); - 150c41d, update-shape.py's `apply --add` (#202 via #233); - 39ac59c, scaffold-project.py's leg mount `--force` (#229 via #235). Two of them touch adopt-project.py, and no hunk of either meets this branch's: - 4835fb4 changes the plan writer (`y`, `_PLAIN_RE`), adds `_shown` to the plan and check reports, and writes the `--paths-from-file` listing in `_extract_leg` through `write_paths_listing`. This branch changes other lines of `_extract_leg`: it reads the tree digest before the push and pushes through `_push_the_leg`. The signature is the same on both sides, so main's new direct `_extract_leg` test calls it as written. - b538015 moves `_overlap`, `_first_and_more`, `MOVE_THE_LEG` and `CASE_ASIDE` into scripts/shape_materialize.py, and imports the first three back under the same names. This branch adds no use of any of them. A `symtable` check finds no name that the merged adopt-project.py, scaffold-project.py or scripts/shape_materialize.py reads and nothing binds. `git diff -U0` of this merge against 39ac59c equals the branch's against 54aa69a, apart from the hunk headers. On the merged tree, tests/test_adopt_plan_escapes.py, tests/test_yaml_flow_quotes.py, tests/test_scaffold_leg_path_collisions.py and tests/test_adopt_leg_path_collisions.py give 275 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…indentation, spacing and trailing comment and reads its result back before writing it, so a `digests:` child indented four spaces takes the update instead of `update-pin-unrecognised` / `bump-leg-pin-unrecognised`, and a `# comment` on `commit:` or `tree_sha256:` survives every pin rewrite, `rewrite_manifest` included (#323) (#380) `rewrite_shape_pin` (update-shape.py) and `rewrite_pin` (scripts/bump-leg.py) FOUND `tree_sha256:` by exactly two leading spaces and WROTE both lines back as `commit: "<sha>"` and ` tree_sha256: "<digest>"`; `rewrite_manifest` kept each line's indentation since #224 and still wrote its two lines bare. So a pin whose `digests:` child is indented four spaces, which the reader and both validators accept, was refused as hand-edited ("0 `tree` line(s)"), and a trailing comment on either line was dropped by all three rewrites. #224 measured the first for `apply` and left the pin rewrites alone; this is the same fix on the same pattern. - `shape_materialize.with_scalar(line, key, value)` replaces ONLY the value: the indentation, the key, the spacing between the colon and the value and a trailing `# comment` (found by the reader's own `_strip_comment`, so a `#` inside a quoted value is not one) are the line's own. The new value is double-quoted, as the scaffold writes it. `is_nested_key` finds `tree_sha256:` as any INDENTED line that begins with it; `commit:` is still found at column 0, and each line must still be found exactly once. - `rewrite_shape_pin`, `rewrite_pin` and `rewrite_manifest` all write their two lines through it. The `files:` block of the shape pin is still written whole, from the recomputed rows. - What each returns has been READ BACK with `parse_yaml`: `commit` and `digests.tree_sha256` must equal the target, or it is refused as `update-pin-unrecognised` / `bump-leg-pin-unrecognised`, as this tool's fault, before the pin is written. The one layout a line rewrite cannot carry, a value on the line below its key, reads and validates, and used to reach the validators and the lockstep remedy about the gitlink. Raised only when the pin read before the rewrite: a pin that did not was red before the command ran and is left to the validators, as in #208. `apply` rolls the copies it had made back and `bump-leg.py` puts the leg's detached checkout back; the tree ends exactly as it was. - The reading and the question ("is this the tool's fault?") are one implementation, `shape_materialize.pin_values_in` / `rewrite_fault`, that #208's manifest check now calls too, with its messages unchanged. They live in `shape_materialize.py` and not in `repo_shape.py`, which is a shape copy every project holds and digest-pins. - `update-shape.py` still imports `parse_yaml` and `YamlError`, which nothing in it reads any more now that #208's reading is `pin_values_in`. The `--pr` re-run branch (#333) reads HEAD's pin with them in `_committed_pin`; its merge with this one is textually clean and, without the two names, fails with a `NameError` in that function, so they stay as main has them. - A pin laid out its own way and already at the commit is now `nothing to do` under `bump-leg.py` (exit 1): its rewrite is itself. A commented pin used to differ from its own rewrite, so a bump that had landed was made again as a commit that only deleted a comment. - The digest is still RECOMPUTED from the leg's own objects, never edited to fit (AGENTS.md, "Advancing a leg", rule 2). The new refusals tell the operator to leave both values alone and re-run the command, which recomputes it. Unchanged: a scaffold-shaped pin or manifest. Measured on a real scaffold, the sha256 of what `rewrite_shape_pin` (shape-pin.yaml), `rewrite_pin` (spec-pin, code-pin) and `rewrite_manifest` (project.yaml) return is identical on main c7c02fa and on this branch, and `tests/test_pin_rewrite_layout.py` holds each of the four pin kinds to the scaffold's own rendering of the target values. The refusals for a missing or doubled `commit:` or `tree_sha256:` line and the `files:` replacement are as they were. Left out: `files:` and the manifest's `shape:` followed by a trailing comment are still refused (`update-pin-unrecognised`, `update-manifest-no-shape`) by name rather than rewritten; `bump-leg.py --dry-run` does not rewrite the pin unless nothing moves, so it exits 0 for a pin the real run refuses, as it did for the four-space pin on main. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>



Lane: openreposhape-3 (openRepoShape-3)
Claim: #208 (comment)
What
update-shape.py applywrote the manifest's two pin lines at a fixed indent, so aproject.yamlindented four spaces came back as YAML its own reader refuses.rewrite_manifestwrites each ofcommit:andtree_sha256:as the line's own leading whitespace, the key, and the new quoted value. It already FOUND them by stripped text at any indentation; it now WRITES them the same way. Only the two values move, in any indentation the reader accepts.parse_yaml:shape.commitandshape.digests.tree_sha256must equal the target, orapplyrefuses asupdate-manifest-unrecognisedbefore the manifest is written. The copies and the pinapplyhad already written are rolled back, so the tree ends exactly as it was. The remedy says the fault is this tool's line rewrite. The one layout a line rewrite cannot carry, a value on the line below its key, reads and validates, so it used to reach the validators and relay their lockstep remedy; it is now refused here by name.applyran; it is rewritten as before and left to the validators, which name the line.family.yamlgoes through the same function, and has a test of its own.Why
The issue's reproduction, measured on this machine: an
Atlasscaffolded from a local clone of maina5b09c3, cloned--recurse-submodules, one upstream commit6afa1cfthat changes one copied template, andproject.yaml'sshape:block re-indented (the commit and the digests' value at the indents shown). Main'supdate-shape.pyand this branch's ran against byte-identical copies of each project,--upstreamthe same clone,--at 6afa1cf. Validators were exit 0 and exit 0 before every run.shape:blocka5b09c3applybb79295)applytree_sha256at six (the issue)REFUSED yaml-unparsable: .../project.yaml: line 77: unexpected indentationtwice, each with the lockstep remedy, thenREFUSED update-validators-red; tree rolled back (git status --shortempty)pins ok,manifest ok: Atlas (atlas), 3 legs;git diff project.yamlis the two value lines, still at four and sixapply(diff -r),check's output andapply's output are byte-identical to main's (diff, scratch path normalised)commit:folded onto the line below its keyREFUSED yaml-unparsable ... line 78: unexpected indentationtwice, the lockstep remedy,update-validators-red; tree unchangedREFUSED update-manifest-unrecognised: project.yaml's shape: block reads as it stands, but this tool's rewrite of it does not read back to the target ...andRemediation: the fault is this tool's line rewrite, not the project's manifest.; tree unchanged (git status --shortempty)contracts/shape-pin.yamlwithdigests:'s child at four spacesREFUSED update-pin-unrecognised: ... has 0 tree line(s) where exactly one was expectedTests
Targeted set on head
bb7929519040295528b22b81d3d4dcac888cb49d, which is one commit ona5b09c3:tests/test_update_shape.py tests/test_update_shape_add.py tests/test_manifest_validator.py tests/test_shape_doctor.py tests/test_repo_hygiene.py tests/test_cli_reference.py:377 passed in 347.50s. The earlier WIP head2ccad27(on20e13f0) ran 374 of these; the three added are thefamily.yamlcases.tests/test_family.py tests/test_commit_trailers.py -k 'update_shape or trailer'(the family apply paths and the trailer composition both go throughrewrite_manifest's caller):133 passed, 68 deselected.docs/cli.mdis untouched: no--helptext changed. README.md and AGENTS.md are untouched. No new test is Windows-skipped: the new tests write the manifest as bytes (commit_manifest), so the line endings under test are the same on every platform. The oneWINDOWS_SKIPin the file is an older test of another subject.New tests in
tests/test_update_shape.py:rewrite_manifestover eight indent pairs(2,4) (4,6) (3,6) (1,2) (8,12) (4,5) (2,6) (3,4): the output is the input with the two quoted values substituted and nothing else, and it parses back to the target. For(2,4)that is byte for byte what main wrote.applyover(4,6) (3,4) (2,6): exit 0, both validators green, the diff is the two values.update-manifest-unrecognised, notupdate-validators-red, without the lockstep text, the tree byte for byte as it was.family.yamlover(2,4) (4,6) (3,6), with a member row's owncommit:above the block left alone.Composes with
Trial merges (
git merge-tree --write-tree <sibling> HEAD, basea5b09c3) against every branch that touchesupdate-shape.pyortests/test_update_shape.py:fix/update-shape-pr-create-failure(#204)8705b27a3(local = origin)update-shape.pyfix/update-shape-pr-base-branch(#209)30240df3d(local = origin)update-shape.py,tests/test_update_shape.pyfix/decode-git-output-utf8(#217)b6f7cfdcf(local = origin)update-shape.pyorigin/fix/update-shape-add-rollback(#202, lane 4)c022b64dfupdate-shape.pyfix/plan-report-stdout-encoding300630329update-shape.pyNo sibling's hunks lie in
rewrite_manifestor_shape_block_bounds. For each of the first four I also built the merged tree in a scratch worktree and ran the 16 new tests on it (the eight-indent unit test, the three end-to-end applies, the folded layout, the unreadable-before case, and the three family cases): 16 passed on each, the localfix/...ref for #204, #209 and #217 and origin's for #202. That checks that a Refusal raised mid-applystill rolls back under #202's rewrittenRollback. Neither side's assertions break the other's. Those merged-tree runs were made on the earlier tips of #204 (ffc5284a5) and #217 (328c09971); the shas in the table are the tips at 2026-10-11T19:30Z, wheremerge-treewas re-run and is CLEAN for each. #209's and #202's tips are unchanged.Left out
rewrite_shape_pininupdate-shape.pymatchestree_sha256:with exactly two spaces, andscripts/bump-leg.py'srewrite_pinthe same way (read, not run), so acontracts/shape-pin.yamlwhosedigests:child is indented four is refused as hand-edited (update-pin-unrecognised, measured, rolled back). The issue names both as the same root cause with a louder outcome; that file is scaffold-written and says not to hand-edit it, so a follow-up issue can decide whether those two rewrites should keep each line's indentation as this one does.rewrite_manifeststill replaces the whole line, so a trailing comment on thecommit:ortree_sha256:line is dropped, identically on main and here (measured).Closes #208
🤖 Generated with Claude Code
Summary by Sourcery
Preserve manifest pin indentation during shape updates and verify the rewritten pins read back correctly before allowing the update to proceed.
Bug Fixes:
shape.commitandshape.digests.tree_sha256lines when applying updates, preventing valid manifests from becoming unreadable.Enhancements:
commit:fields.Tests: