Skip to content

update-shape.py and shape-doctor.py: no NEXT line pre-fills --accept-local for a locally edited shape copy, so the apply a row names, pasted, meets update-local-drift and its question to the human instead of re-pinning the edit as the standard, and a shape/ collision copy merged into its original and removed is merged-away to check, whose apply drops its pin row behind the same yes, so an adopted root that did its follow-up validates green again (#501, #502) - #512

Merged
brettheap merged 6 commits into
mainfrom
fix/update-shape-accept-local-and-merged-copies
Oct 14, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)
Claim: #501 (comment) and #502 (comment)

Two audit findings in update-shape.py and the doctor's shape currency and pins rows, one commit each: 7b4024b (#501) and a5eb55d (#502). Both change which command a row names and what apply does with a pin row. Four rounds followed: 9899f25 (the review's three nits), ddb2df8 (the merge of main d41cc294, #402's squash, which brings #347 and #496 with it), 6943280 (the pins row under #347's link refusal) and c29b9b4 (the merge of main cf118ff7, #513).

#501 (audit auU-02): no NEXT line pre-fills --accept-local

What was wrong. For every locally-modified row, update-shape.py check put --accept-local <path> in the apply on its NEXT line, beside --yes. The doctor's shape currency row did the same in the apply after its check. AGENTS.md tells an assistant to run the command a row names. Pasted, that line re-pinned the project's own edit as the standard: exit 0, a green validate-pins.py and a COMPLIANT doctor. update-local-drift exists to ask the human whether the project keeps the edit, and it never fired.

What changes (commit 7b4024b). Brett Heap ruled on 2026-10-13: "Stop pre-filling it".

#502 (audit auU-03): a merged-away collision copy's row goes

What was wrong. An in-place adoption pins the shape's copy of a name the source already uses under shape/. The adoption's follow-up then says to merge that copy into the original. If the merged copy is then removed, its pin row names a file that is not there. validate-pins.py said shape-copy-missing, and with it bootstrap.py and CI's validate went red. update-shape.py called the row copy-missing and refused update-conflict. That refusal's remediation said to re-run with --accept-local, which _refuse_conflicts never honours for that state. The doctor named that refused step. No tool removed the row: the only ways out were undoing the merge or the hand edit of the pin that AGENTS.md forbids.

What changes (commits a5eb55d, 9899f25 and 6943280). Brett Heap ruled "for auU-03, the pin row goes" (2026-10-12T23:16Z) and "check names, apply drops" (2026-10-13T11:02Z).

  • A new verdict, merged-away. classify asks merged_into first. A row is merged-away when three things hold:
    • its path is under shape/;
    • nothing is at that path (lexists, so a dangling link or a directory still counts as something);
    • the root holds a file at the original path, shape/X -> X.
      The name is the act the follow-up asks for, spelled like the other states. It is not a conflict, and the upstream is not read for it.
  • apply drops the row from the pin it rewrites (_repin). It prints dropped shape/X (merged into X and removed; its pin row goes, and nothing is copied), and its confirmation and commit message say so too. It needs no new flag: the yes that covers every other row covers this one. A dropped row is no longer a pinned path on the NEXT line, and update-rewrite-symlink no longer asks about a path nothing is written at.
  • Every other missing copy stays copy-missing. That covers a pinned copy deleted anywhere else, and a shape/ copy whose original is gone too. update-conflict's remediation is now built per state (conflict_remediation). A copy-missing row is told to put the copy back from the project's own history, because --accept-local has no bytes to keep, and that this command never records the deletion of a copy the project still needs (it does record a merged-away row's). It names both ways back: git checkout HEAD -- <path> for a deletion not yet committed, the parent of the removing commit (git log -1 -- <path> names it) for a committed one (9899f25). both, upstream-removed and unmapped keep their old words (MERGE_BY_HAND).
  • The doctor counts merged-away as drift and names it in the verdict (DRIFTED (2 merged-away)). Its shape currency row's reason counts a merged-away row where there is one (9899f25).
  • The pins row names the same update-shape.py check the shape currency row names, as a single command. It does so only when every finding the gate reports is a merged-away row's shape-copy-missing, judged by the same merged_into, and only where check can read the root. A root that keeps its pin, the contracts directory above it or its manifest as a symlink is refused by check itself (update-shape.py: a pinned copy, contracts/shape-pin.yaml or the manifest kept as a symlink, or under one, is refused as update-rewrite-symlink before anything is written, and check calls such a copy copy-is-link, so apply no longer writes the standard's bytes at the link's end and reports a commit that does not hold them (#286) #347's update-rewrite-symlink), so the row keeps the gate there and the shape currency row says to replace the link (6943280). Any other finding keeps the gate as the NEXT.
  • The adoption's follow-up (collision_follow_up in scripts/shape_materialize.py) tells a pinned copy's follow-up what happens to its row: "Then remove shape/X: update-shape.py check reports its pin row merged-away, and apply drops the row." A rendered file such as shape/README.md has no row and is told nothing about one.

Tests

New and changed tests:

  • tests/test_update_shape_merged_away.py (new) builds one real adoption out of the standard that the doctor tests build, and has seven tests, nine items:
    • test_a_merged_away_copys_row_goes_and_the_project_is_green_again is the issue's own flow, end to end. The gates go red. check and both doctor rows name the step. check's NEXT is pasted, and the two rows go with nothing else in the pin moved. Then bootstrap.py and validate-pins.py are green, the doctor is COMPLIANT, and there is nothing left to do.
    • test_a_shape_copy_removed_with_its_original_stays_copy_missing: the refusal, with or without --accept-local, names the copy's own remedy.
    • test_a_pinned_copy_deleted_elsewhere_stays_copy_missing: next to a merged-away row, it is still refused, and the pins row keeps the gate.
    • test_a_deletion_not_yet_committed_comes_back_from_head (9899f25): after rm and a refused apply, the remediation's git checkout HEAD -- <path>, run as named, puts the copy back, check has nothing to do, and the refused apply wrote nothing.
    • test_the_currency_rows_counts_name_a_merged_away_row (9899f25): the shape currency row's counts sum to its rows where a merged-away row is one of them.
    • test_the_pins_row_keeps_the_gate_while_check_refuses_a_linked_record (6943280), with the pin, the contracts directory and the manifest each kept as a committed link: both copies merged away, check refused update-rewrite-symlink, the pins row keeps the gate and says nothing of merged-away, and the shape currency row names replacing the link. With the link replaced and committed, the pins row names check.
    • test_the_plans_follow_up_says_what_happens_to_the_row.
  • tests/test_update_shape.py::test_checks_next_leaves_a_local_edit_to_the_human (new) pastes check's NEXT over a committed local edit. The paste is refused, the row's digest is unchanged, and the three exits are named. The row is re-pinned only once the person's flag is added.
  • tests/test_shape_doctor.py:
  • tests/test_doctor_next_is_a_command.py (shape-doctor.py: a NEXT is only the command, and the sentence that ended twenty-five of them after # is the row's why, on a WHY line of its own and "why" in --json, so a NEXT pasted into an interactive zsh -- which reads that # as a word -- no longer hands the comment to the command as arguments, runs what follows its ;, or runs the make bootstrap in its backticks, and the template validator's line is env PYTHONPATH=... python3 ... (#277) #402's): _shape_currency_moved asserts --accept-local is absent from the row's why.
  • tests/test_update_shape_next_quoting.py: the local-edit variant expects no flag. test_a_path_check_offers_to_accept_is_one_argument is deleted, because the path it quoted is no longer on that line.

Targeted run at c29b9b4, with git-filter-repo on PATH, over tests/test_repo_hygiene.py, test_agents_md_codes, test_cli_reference, test_update_shape_merged_away, test_update_shape, test_update_shape_rewrite_symlink, test_update_shape_next_quoting, test_shape_doctor, test_doctor_next_is_a_command, test_openreposhape_command and test_adopt_leg_path_collisions: 850 passed, 3 skipped, 0 failed. The three skips are the PowerShell tests at tests/test_shape_doctor.py:9118, which run on a Windows host only. The head before the last merge, 6943280, ran green on tests, tests-macos, tests-windows and SonarCloud. c29b9b4 is that head merged with main cf118ff7, and its one hand resolution is the handbook footer.

Mutants. Each mutant ran in a scratch checkout, with the tests of the commit named and the tools reverted, or, for the last, one line of them changed:

Docs

The docs are corrected in this PR, as Brett Heap ruled at 2026-10-13T11:30Z for each lane-4 fix. The branch holds main cf118ff7 (#513). README.md keeps its 1422 lines, its cap, and AGENTS.md its 490, its cap. docs/handbook.html has no cap and is 1943 lines against main's 1942, because its footer clause wraps onto one more.

Shared files

Left as they were (for the v1.0.0 checklist)

Closes #501
Closes #502

🤖 Generated with Claude Code

Brett Heap and others added 2 commits October 13, 2026 11:37
…`shape currency` NEXT no longer pass `--accept-local` for a locally edited shape copy, so the command a row names, pasted as printed, meets `update-local-drift`, which asks the human whether the project keeps the edit, instead of re-pinning that edit as the standard (#501)

`cmd_check` folded `--accept-local <path>` for every `locally-modified`
row into the `apply` it printed, beside `--yes`, and the doctor's `shape
currency` row did the same in the `apply` after its `check`. AGENTS.md
tells an assistant to run the command a row names, and pasted, that line
re-pinned the project's own bytes: exit 0, a green `validate-pins.py`, a
COMPLIANT doctor, and `update-local-drift`, the refusal that exists to put
the question to the human, never fired.

Brett Heap ruled on 2026-10-13, "Stop pre-filling it". Neither NEXT names
the flag for a `locally-modified` row now, so the `apply` meets that
refusal, whose remediation names the three exits: revert the edit, carry
it upstream, or `--accept-local` on the human's word. `check` still names
the flag in the row's detail, as the human's to give, the way it already
names it for `both` (`keep_local_exits` beside `accept_local_exit`).
`apply_command` loses the parameter that carried the paths, and the
`--yes` in both lines stays as it was, as ruled.

The doctor's `apply` keeps interpolating `accept`, which is now always
empty, so the lines that build that `apply` are left byte for byte as
they were. AGENTS.md's updating step 3 says, in the same line count, that
no NEXT line passes the flag and that `apply` refuses and asks.

Tests: the doctor's drift test pinned the hand-over on purpose, reasoning
that a reader would know not to paste it, and now asserts no line of the
report carries the flag. P10b pastes the `shape currency` row's `apply` as
printed, is refused `update-local-drift`, and ends COMPLIANT with the
person's `--accept-local`. `check`'s NEXT for a root with a local edit
carries no path, so the quoting test for an `--accept-local` path on that
line goes with the parameter. A new test pastes `check`'s NEXT over a
committed local edit: refused, the row's digest unchanged, the three exits
named, and re-pinned only with the flag added.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pe/` collision copy merged into the file it collided with and removed is `merged-away` to `check`, and `apply` drops its pin row in the re-pin commit behind the same yes, so an adopted root that does its own follow-up gets its `bootstrap.py` and `validate` green again instead of a `copy-missing` row whose only remedy was refused in the same words (#502)

An in-place adoption pins the shape's copy of a name the source already
uses where it lands, under `shape/`, and its follow-up says to merge it
into the original. Removing the merged copy left a row naming a file that
is not there: `validate-pins.py`, and with it `bootstrap.py` and CI's
`validate`, said `shape-copy-missing`; `update-shape.py` called the row
`copy-missing` and refused `update-conflict`, whose remediation said to
re-run with `--accept-local`, which `_refuse_conflicts` never honours for
that state; the doctor named that refused step; and no tool removed the
row, leaving undoing the merge or the hand edit of the pin AGENTS.md
forbids.

Brett Heap ruled on 2026-10-12, "for auU-03, the pin row goes", and on
2026-10-13, "check names, apply drops":

- `classify` asks `merged_into` first: a row under `COLLISION_DIR`, with
  nothing at it (`lexists`), whose original path the root holds as a file,
  is `merged-away`. The name is the act the follow-up asks for and the
  phrase this code and AGENTS.md already used, spelled like the other
  states. It is not a conflict, and the upstream is not read for it.
- `apply` leaves such rows out of the pin it rewrites (`_repin`), prints a
  `dropped` line, says so in the confirmation and the commit message, and
  needs no new flag: the yes that covers every other row covers this one.
  A dropped row is no longer a pinned path for the NEXT line, and
  `update-rewrite-symlink` does not ask about a path nothing is written at.
- A pinned copy deleted anywhere else, or a `shape/` copy whose original
  is gone too, stays `copy-missing`. `update-conflict`'s remediation is now
  built per state (`conflict_remediation`): a `copy-missing` row is told to
  put the copy back from the project's history, with or without
  `--accept-local`; `both`, `upstream-removed` and `unmapped` keep the words
  they had.
- The doctor counts `merged-away` as drift, names it in the verdict
  (`DRIFTED (2 merged-away)`), and its `pins` row names the `update-shape.py
  check` the `shape currency` row names, but only where every finding the
  gate reports is a merged-away row's `shape-copy-missing`, judged by the
  same `merged_into`; any other finding keeps the gate as the NEXT.
- `collision_follow_up` tells a pinned copy's follow-up that its row goes
  once the copy is removed, and by which step; a rendered file such as
  `shape/README.md`, which has no row, is told nothing about one.

README.md, AGENTS.md and docs/handbook.html say the new verdict and what
happens to the row, in the same line counts; the handbook names the new
README blob.

Tests (tests/test_update_shape_merged_away.py, one real adoption out of
the standard the doctor tests build): the issue's own flow end to end, red
gates, `check` and the doctor naming the step, `check`'s NEXT pasted, the
two rows gone and nothing else in the pin moved, then green gates, a
COMPLIANT doctor and nothing to do; a `shape/` copy removed with its
original stays `copy-missing` and its refusal, with or without
`--accept-local`, names the copy's own remedy; a pinned copy deleted
elsewhere stays refused beside a merged-away row, and the `pins` row keeps
the gate; and the plan's follow-ups.

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

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

@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes generated remediation commands safer by leaving local-edit acceptance to the human, and introduces an end-to-end merged-away state that removes stale pin rows for intentionally merged collision copies while preserving appropriate handling for other missing files.

Sequence diagram for human-controlled local drift acceptance

sequenceDiagram
    participant Human
    participant Check as update-shape.py check
    participant Apply as update-shape.py apply
    participant Drift as update-local-drift
    participant Pin as shape-pin.yaml

    Human->>Check: Run check on locally-modified row
    Check-->>Human: NEXT apply --yes&#59; detail names --accept-local
    Human->>Apply: Paste NEXT command
    Apply->>Drift: Refuse local drift
    Drift-->>Human: Choose revert, carry upstream, or accept locally
    Human->>Apply: Re-run with --accept-local path
    Apply->>Pin: Re-pin accepted local bytes
Loading

Flow diagram for dropping a merged-away collision pin row

flowchart TD
    A[shape/X pin row] --> B{shape/X is absent?}
    B -- No --> C[Keep normal classification]
    B -- Yes --> D{Root file X exists?}
    D -- No --> E[copy-missing]
    D -- Yes --> F[merged-away]
    F --> G[apply --yes]
    G --> H[Drop shape/X row from pin]
    G --> I[Copy nothing]
    H --> J[validate-pins.py green]
Loading

File-Level Changes

Change Details Files
Require explicit human consent before accepting locally modified shape copies.
  • Removed prefilled --accept-local flags from check and doctor-generated apply commands.
  • Kept the flag in diagnostic remediation text and routed pasted commands through update-local-drift.
  • Simplified apply_command and updated coverage for command quoting and refusal behavior.
update-shape.py
shape-doctor.py
AGENTS.md
tests/test_update_shape.py
tests/test_update_shape_next_quoting.py
tests/test_shape_doctor.py
Add automatic cleanup for collision copies that were merged into their original and removed.
  • Added the merged-away classification for absent shape/ paths whose original root file remains.
  • Excluded merged-away rows from rewritten pins without copying bytes, and reported the drop in output, confirmation, and commit messages.
  • Preserved copy-missing for other deletions and added state-specific remediation.
  • Updated doctor gate recommendations and drift reporting so eligible pin findings name the same update-shape.py check step.
update-shape.py
shape-doctor.py
tests/test_update_shape_merged_away.py
Document collision-copy row cleanup in adoption follow-ups and user guidance.
  • Added follow-up instructions explaining that removing a merged collision copy leads check to report merged-away and apply to drop its row.
  • Documented the new verdict and the rule that NEXT commands do not pre-accept local edits.
  • Updated the handbook change log and README blob reference.
scripts/shape_materialize.py
AGENTS.md
README.md
docs/handbook.html

Assessment against linked issues

Issue Objective Addressed Explanation
#501 Ensure update-shape.py check and the doctor's shape currency NEXT command do not pre-fill --accept-local for locally modified shape copies, so running the command as printed reaches update-local-drift and asks the human to decide whether to keep the edit. ✅
#501 Keep the human's available choices clear while documenting that the flag is a human decision, not something a NEXT command supplies automatically. ✅
#502 Provide a supported way to remove the pin row for a merged-away shape/ collision copy, so the adopted project can complete its follow-up and return its pin validation and bootstrap gates to green. ✅
#502 Keep other missing pinned copies as copy-missing, with a remediation that does not direct users to retry with --accept-local when that option cannot resolve the state. ✅
#502 Make the adoption follow-up and doctor guidance identify the merged-away remedy, and document the new verdict and behavior. ✅

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

…f `update-conflict` no longer says this command never records a deletion and names `git checkout HEAD -- <path>` for a deletion not yet committed beside the parent of the commit that removed one, and the doctor's `shape currency` reason counts a `merged-away` row, so an `rm` then `apply` is sent to a commit that exists and the row's counts sum to its rows (#502)

The fold of rv512's NIT-1, NIT-2 and NIT-3 on a5eb55d (review of PR #512).

NIT-1. "this command never records a deletion" stopped being true in the
commit it was written in: `apply` drops a merged-away row, which is a
deletion recorded in the pin. The sentence now says which deletion it
never records, that of a copy the project still needs; what it tells the
person to do is unchanged.

NIT-2. "the parent of the commit that removed it holds it" is false where
the deletion is not committed, and that is the common way to meet this
refusal: `rm .gitattributes`, then `apply`. No commit removed the copy, and
`git log -1 -- <path>` names the commit that ADDED it. The sentence gives
both exits: a deletion not yet committed comes back with `git checkout
HEAD -- <path>`, a committed one from the parent of the commit that
removed it, which `git log -1 -- <path>` names. `HEAD` is named because
`git checkout -- <path>` restores from the index, which a staged `git rm`
has already emptied of the path, so it refuses with `pathspec did not
match`; `checkout HEAD --` puts back both an `rm` and a `git rm`, and it is
the doctor's own line for a lost `repo_shape.py`. The `<path>` placeholder
stays a placeholder in backticks, as before.

NIT-3. The `shape currency` reason counted the five original states
alone, so one merged-away row read `10 unchanged / 0 upstream-changed / 0
locally-modified / 0 both / 0 upstream-added (...): shape/.gitignore`,
eleven rows under counts that sum to ten and a path named under no count,
in the reason of the very row whose `apply` drops it. `merged-away` is
counted after the five when its count is not zero, so a root with no such
row reads exactly as before.

Tests, in `tests/test_update_shape_merged_away.py`: the copy-missing test
pins NIT-1's sentence and the absence of the old one; a new test deletes
`.gitattributes` without committing, is refused with both exits named and
nothing written, runs the `git checkout HEAD --` the sentence names, and
`check` has nothing to do; a new test merges `shape/.gitignore` away and
reads `1 merged-away` at the end of the `shape currency` counts, which sum
to the row's own `detail.counts`. All three fail on a5eb55d's source and
pass here. No other test quoted a changed sentence (`git grep` over
`tests/`).

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 13, 2026 21:27
…ed-copies, after #402 made every doctor NEXT only a command: `check_shape_currency` keeps #402's `(command, why)` row with #501's `apply` that pre-fills no `--accept-local`, P10b and #402's `_shape_currency_moved` assert no flag in the `why`, #496's `re_pinned_from_the_merge` follows the refusal both ways round, #496's `unmerged` sentence says what the row does under #501, and the handbook footer adds (#502) after main's (#462) (#501, #502)

Merges origin/main at d41cc29: #402's squash of a841533, on lane 2's #286
(8cd7dbe) and lane 3's #462 (d52b100). The resolution is the one prepared
against a841533 in scratch (5a63d84a, 800 passed there), carried onto main:
#402's squash holds a841533's change byte for byte in every file it touched
(same patch-id per file), and main's only other change to the resolved
files is #286's, which `git merge-file` applied onto the prepared files
without a conflict.

Three files conflicted:

- shape-doctor.py, `check_shape_currency`, the pin-only branch and the
  final return: #402's `(command, why)` form, the `apply` in the row's
  `why`, under #501's rule that it carries no `--accept-local`. #402's
  `accept = "".join(...)` is dropped (that is #501), and so is this PR's
  `accept = ""` with its `{accept}`, kept only so #402's lines stayed byte
  for byte (review NIT-5); #501's comment moves beside the `apply` it
  governs, and #402's comment there no longer says the `why` carries the
  flag.
- tests/test_shape_doctor.py, P10b: `assert "--accept-local" not in
  currency["next"] + currency["why"]`, since under #402 the `apply` is in
  the `why`.
- docs/handbook.html, footer: main's text, every clause through (#462),
  then this PR's "the <code>merged-away</code> verdict, whose row
  <code>apply</code> drops, added (#502)", the two lines touched re-wrapped.
  `README.md blob` 8c658ef (main's; the
  branch had 2977f24) becomes
  c4f819a, `git hash-object` of the merged
  README.md (1422 lines; AGENTS.md 490).

Changed beyond the markers, because #501 makes main's words false on the
merged tree:

- tests/test_doctor_next_is_a_command.py, `_shape_currency_moved`: asserts
  no `--accept-local` in the row's `why`, where it asserted the flag.
- #496's `unmerged` sentence in `pin_restore` (shape-doctor.py) said the
  `shape currency` row names `--accept-local` where `check` calls the copy
  locally-modified. Under #501 the row never names it, so the doctor would
  print a false sentence. It now reads "The shape currency row never names
  it. Where update-shape.py check calls the copy locally-modified, the
  apply the row names is refused as update-local-drift, and where check
  calls it both, as update-conflict, and either refusal asks for
  --accept-local on that path"; `pin_restore`'s docstring and the test's
  say the same.
- #496's `re_pinned_from_the_merge` (tests/test_shape_doctor.py) asserts
  the row's `apply` names no `--accept-local` either way round, runs it as
  spelled, expects `update-local-drift` on the update side and
  `update-conflict` on the person's, with nothing moved, then adds the
  person's `--accept-local <copy>` and reaches COMPLIANT;
  `assert_the_pin_alone_is_taken` checks the new sentence.
- Lane 3's #402 review NITs: the helpers' `then[:2]` assertions quote
  `currency["why"]`, and three docstrings say `why` for "note".

Everything else auto-merged, #286's `update-rewrite-symlink` and
`copy-is-link` in update-shape.py and shape-doctor.py included.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erged-away collision copy only where `check` can read the root, so a root that keeps its pin or its manifest as a symlink, which `check` now refuses as `update-rewrite-symlink` (#286), keeps the gate as that row's NEXT instead of a `check` that exits 2 beside a `shape currency` row naming the link (#502)

`with_merged_away_rows` names `check` as the `pins` row's NEXT where every
finding the project's own `validate-pins.py` reports is a merged-away
`shape/` copy's `shape-copy-missing`, and says it is the step the `shape
currency` row names too. Its rule is to do so only where that step clears
every finding. Lane 2's #286, merged into this branch from main, made
`read_pin` and `prepare` refuse a root that keeps `contracts/shape-pin.yaml`,
the `contracts` directory above it or the manifest as a symlink, before
anything is classified, and made the doctor's `shape currency` row name
replacing the link (`linked_record_row`). `validate-pins.py` still reads
through the link and still reports the copies missing. So on such a root
the `pins` row named a `check` that refuses, exit 2, and said that
`check` reports the rows `merged-away` and that the `shape currency` row
names it, both false (measured on the merged tree: pin, `contracts` and
manifest kept as committed links, verdict `INVALID (pins)`).

The row now asks the same `linked_record` question `linked_record_row`
asks, of the root's own kind, and keeps the gate as its NEXT while the
answer is a link, as it does for any finding the step would not clear.
The `shape currency` row names the remedy; with the link replaced and
committed, the doctor run again names `check`, as before.

Also measured on the merged tree, and left as they are: the file a copy
was merged into kept as a link, and `shape/` itself kept as a directory
link, are still `merged-away`, and `apply` drops the rows and writes
nothing through either link; a `copy-is-link` row beside merged-away rows
is refused `update-rewrite-symlink` before `update-conflict`, so
`conflict_remediation` never reads one.

`tests/test_update_shape_merged_away.py` holds the three links, each
failing without this change at the `pins` row's `merged_away` detail.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…5 cites the 34 pull requests merged since #343, section 6 drops the 13 known-limit issues that closed and adds the 63 checklist lines the five parents gained, section 7 lists the 12 open milestone issues, and the preflight sentence offers only what the preflight offers (#472)

Refresh round 1 of the draft, against main d41cc29 (#402, merged
2026-10-13T21:17:39Z). GitHub was read between 21:23 and 21:43 UTC: the
merged pull requests, the v1.0.0 milestone open and closed, every open
issue, and the five tracking parents with their sub-issues, bodies and
comments. Every merge commit of the 217 merged pull requests is on main.

Section 5 places each of the 34 pull requests merged after #343
(2026-10-12T22:31:20Z) in its area, from its own body, one clause each;
all 217 merged pull requests are now cited, with #16, #53 and #57 still in
two areas. Three earlier sentences are made true of main: `check` has
more verdicts than the four it named, and #465's docs and #505's accuracy
pass show that `validate-pins.py` may ask `gh api` and that
`openRepoShape --preflight` offers only `gh`, its login and its credential
helper (`setup-project.py --preflight` also offers `git`).

Section 6 lists the 47 open, unmilestoned sub-issues (it listed 60). It
drops #189, #293, #303, #328, #386, #286, #259, #275, #322, #324, #338,
#339 and #359, which closed. #389 is reworded now that #305's refusal has
landed. #420 and the `a=b` line are narrowed to a hand-edited
`adopt_branch:`, because an offline probe on d41cc29 shows #493's
`plan-tracking-branch-missing` now stops a bad `tracking_branch:` at
`check`, with no leg made, while `adopt_branch: "a..b"` still passes
`check` and fails after both legs exist. The family annotated-tag line
says `bump --to` now refuses one (#466). The 63 new lines are the
parents' checklist lines posted or added since the first build, each
checked against the code at d41cc29. Left out, and said so in the
refresh record: docstring, comment and wording nits (as L2-7 was), lines
that describe a branch not on main (#504's, #507's, #511's, #515's and
#517's), lines filed into the milestone (#485, #514, #516), and three with
no effect a person meets.

Section 7 lists the 12 open milestone issues; 24 of the 30 it listed have
closed. Lane 4's #510, #511, #512 and #513 have not landed, so #471,
#499, #500, #501 and #502 stay open here and the Windows qualifier stays.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…ision_follow_up`'s docstring moves up beside the docstring's first paragraph, so the docstring's end, which #512 (#502) also extends, merges without a hand resolution (#471)

This PR and #512 each appended a paragraph at the END of
`collision_follow_up`'s docstring, so a merge of one into the other, once the
first lands, conflicts in a .py file and needs a hand resolution. The
paragraph about a `.gitattributes` that is a symlink now sits straight after
the docstring's first paragraph, as its own paragraph with one blank line on
each side, and the docstring ends, as it does on main, with "and says both
merges come first (#493's review, F2)." Not one word changes and nothing else
in any file: the sorted lines of the file are equal before and after.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…and-merged-copies: the handbook footer keeps main's clauses and this PR's #502 clause follows #499's

Ruling 27 refresh round. #513 landed on main as cf118ff and rewrote the
footer's clause list and README.md blob in docs/handbook.html, so this branch
conflicts there and nowhere else (git merge-tree --write-tree 6943280 cf118ff
exits 1 with that one path).

The one hand resolution, docs/handbook.html's footer:
- main's text is kept whole: the #462 clause, then #487, #500 and #499;
- this PR's own clause, "the merged-away verdict, whose row apply drops,
  added (#502)", is added after main's last one (#499), in the same
  "; ... added (#NNN)" form and before " &mdash; a page cannot name the
  commit that contains it";
- only the two lines touched are re-wrapped, to 100 columns;
- README.md blob: this branch's c4f819a
  and main's 6bf94d5 give way to
  ae817c5, the git hash-object of the
  merged README.md (AGENTS.md 490 lines, README.md 1422).

README.md, AGENTS.md and tests/test_shape_doctor.py auto-merged with no
conflict; no other file conflicted.

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

Copy link
Copy Markdown

@brettheap
brettheap merged commit 8beb3d9 into main Oct 14, 2026
5 checks passed
@brettheap
brettheap deleted the fix/update-shape-accept-local-and-merged-copies branch October 14, 2026 00:46
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…s a sentence to a pinned copy's collision follow-up and #501 changes what update-shape.py and the doctor print, none of it in this branch's lines, so the copy `place()` writes beside a kept path is walked as before and its follow-up now says how its pin row goes

Merged by sha 8beb3d9, the one commit
of main past cf118ff. No conflict: the tree is
`git merge-tree --write-tree 64b2cc0 8beb3d9`'s, 22a21a3.

The commit that meets this branch's files:
- 8beb3d9 (#501 and #502 via #512): in scripts/shape_materialize.py,
  `collision_follow_up` ends a pinned copy's follow-up (a file in
  `COPIED_VERBATIM` or `COPIED_FROM_SHAPE`) with ". Then remove
  <copy>: `update-shape.py check` reports its pin row `merged-away`, and
  `apply` drops the row", and its docstring says why. Its hunks are in
  `collision_follow_up` alone, away from this branch's
  `_refuse_unwritable`, `place()` and `template_files`. The rest is
  update-shape.py, shape-doctor.py, docs and their tests, which this
  branch does not change.

How it composes with this branch:
- The walk this branch adds asks where `place()` writes each shape path
  and, where that path is taken, its copy under `shape/`. #512 changes
  only the follow-up sentence for such a copy, not where it lands or
  which paths are written, so the walk asks the same paths and refuses
  the same links.
- No test of this branch asserts a follow-up's last words: its three
  test files and every file of the targeted set were searched for the
  old ending ("NOTHING was overwritten"); the one that calls
  `collision_follow_up` (test_adopt_leg_path_collisions.py) compares it
  with `_predict_collisions`, which calls it too. The `  beside  ...
  nothing was overwritten)` lines this branch's tests read are
  `_commit_the_split`'s, which #512 does not change.

Checked on the merged tree: no top-level name bound twice in
adopt-project.py, scaffold-project.py, scripts/repo_shape.py,
scripts/shape_materialize.py, update-shape.py or shape-doctor.py but
main's own (`YamlError`/`parse_yaml` in the materializer, `sys` in the
doctor); no name loaded and bound nowhere; the signatures main changed
(`_land`, `apply_command`, `commit_on_branch` in update-shape.py) are
called only by main's own lines; each side's change reappears line for
line in the other side's diff to the merge, in every file.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…ck: footer-only refresh under ruling 27

Main brought #515, #507, #517, #510, #513 and #512 since the previous merge base
89c6a6c. The one conflict is the handbook footer in docs/handbook.html, which
#512 rewrote so that its "(#502)" clause is now last.

Resolution: main's footer text kept whole (#498, #462, #487, #500, #499, #502),
this PR's clause (the line-ending block an adoption appends to a root
.gitattributes the source keeps, and its verification as the source's bytes
plus that block, added (#471)) added after #502's in the same "; ... added
(#NNN)" form and before " &mdash; a page cannot name the commit", the touched
lines re-wrapped to 100 columns, and "README.md blob" set from
ae817c5 (main's) to
f3b378d, the `git hash-object README.md` of
the merged README.md (this branch's own was 43316b9).
No other file conflicted and nothing else was hand-resolved; README.md stays at
1422 lines and AGENTS.md at 490.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…itattributes-lf-block: three hunks in adopt-project.py and scripts/shape_materialize.py are kept on both sides, and #504's check of root-kept symlinks refuses only the links where #511 alone wrote the shape's copy through or failed after both legs

Main's tree after #504 holds lane 1's `fix/adopt-root-kept-symlinks` as it
landed; this branch held main 8beb3d9 (#512) with #471's block. The merge
conflicted in two files, and the footer of docs/handbook.html did not,
because #504 does not touch the handbook: the branch's footer (main
8beb3d9's clauses, then #471's) stands as it was.

CONFLICTS, AND HOW EACH WAS RESOLVED

- adopt-project.py, the `from shape_materialize import (...)` list: the
  union. #504's lines as they are (`SYMLINK_HOPS`, `TOO_MANY_SYMLINKS`,
  `template_files`), with `ATTRIBUTES` added beside `SHAPE_REPOSITORY` and
  #511's `eol_block_paths, eol_line, with_eol_block` on a line of their own.
- scripts/shape_materialize.py, `materialize_assembly_root`'s docstring:
  both paragraphs, #504's `spell` paragraph first, as main holds it, then
  #511's `.gitattributes` paragraph.
- scripts/shape_materialize.py, its tail: `result = _materialize(...)`
  with #504's `spell=spell`, then #511's `_append_eol_block(target,
  result, collision_dir)` when there is a `collision_dir`, then `return
  result`.

Every other hunk merged without a conflict, among them `_commit_the_split`,
where #504's `spell=_spelled` and #511's `appended to .gitattributes` print
and `appended=materialized.eol_block` sit side by side, and the three test
files #504 adds or changes.

HOW THE TWO MEET, asked of the code and of real adoptions through `plan`,
`check` and `execute` (each read first on #511 alone, on #504 alone, and on
the merge, in a prepared merge of #504's then-head, before main moved)

(a) `materialize-attributes-path` needs no `spell`. Only `eol_line` raises
it, and only over the shape's own copy-list names. `_append_eol_block` reads
`Materialized.shape_files`, which `root_key` names as the copy lists do and
never resolves through a link: a source that keeps `.github -> ci` still
gets `.github/workflows/validate.yml` there. The split message and the
verification name the pin rows written from the same lists, and
`update-shape.py apply --add` names the standard's added copy paths
(`add.path`), which become pin rows. `spell` exists for the names a source's
symlinks lead to, and none of them reaches `eol_line`; `_append_eol_block`
and `extend_eol_block` raise nothing else.

(b) A root `.gitattributes` that is a symlink. Where `check` passes, the
merge does what #511 alone does, to the blob: a link to a file in the source,
or to one outside it that exists, is kept, gets no block, and has the shape's
copy beside it, as #511's `attributes_link` follow-up says. Where #504's
`check` now refuses, `plan-root-symlink`, before any leg, #511 alone did
not do what that follow-up said. A dangling link had the shape's copy written
THROUGH it, to a new root file, and `execute` said `adoption verified`. A link
to an absolute path with nothing there had the copy written outside the
assembly and failed `adopt-shape-file-missing` after both legs. A climb
above the root ended `REFUSED committing the split` on a FileNotFoundError
after both legs. A `.gitattributes` under a symlinked directory: `shape ->
vendor` with `vendor/` free adopts with the block on both; with a
`vendor/.gitattributes` of the source's there, #511 alone stopped at
`materialize-copy-blocked` after both legs, and the merge refuses it as
`plan-root-symlink` first; `conf -> real` holding `real/.gitattributes`
adopts with the block on both. #504 refuses nothing the block or its
verification needs, and the two PRs say the same of a link: git reads no
`.gitattributes` that is one. The plan's follow-up for a link that `check`
refuses is a prediction no `execute` acts on, as for every other follow-up
of a plan `check` refuses.

(c) The rest of #504's diff does not meet the block. `template_files` seeds
legs. `_refuse_unwritable` runs inside `place`, before `_append_eol_block`,
which writes the root `.gitattributes` itself, never through `place` and
never through a link. `adopt-symlinks-off` is about a link ABOVE a shape
path, and a link AT `.gitattributes` is a plain file in such a clone, which
#511's `_tracked_as_link` already skips. `_SplitTree`'s new wording, "follows
too many symlinks", changes no lookup `_pin_through_links` makes.

One defect is on #511 alone as on this merge, and is left for a decision
rather than fixed here: a pinned copy written through a directory link the
root keeps (`.github -> ci`) is named in the block by its pinned path,
`/.github/workflows/validate.yml`, which git never matches against the
file it tracks, `ci/workflows/validate.yml`. A source whose own attributes
say `eol=crlf` then checks that copy out with CRLF, and `validate-pins.py`
reports shape-copy-drift on it.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…s` says what happens to the pin row of its copy, as #512 (#502) has every other pinned copy's say, so a person who merges the rest of `shape/.gitattributes` into the root file and removes it is told that `update-shape.py check` reports the row `merged-away` and `apply` drops it (#471)

#512 (#502) ended the follow-up of every pinned collision copy with "Then
remove shape/X: `update-shape.py check` reports its pin row `merged-away`,
and `apply` drops the row". The `.gitattributes` branch of
`collision_follow_up` returns before that sentence. Its follow-up said to
merge the rest of `shape/.gitattributes` into the root file if the whole
project should check out with LF, and said nothing of the row that removing
the copy leaves, on which the project's own `validate-pins.py` is red until
`apply` drops it.

The sentence is added in #512's words, under the condition the merge already
carries: "Merge the rest of shape/.gitattributes into .gitattributes only if
the whole project should check out with LF, and only then remove
shape/.gitattributes: ...". A copy that is kept, which is what the follow-up
leaves by default, keeps its row, and that row stays `unchanged`.

The follow-up for a `.gitattributes` that is a symlink does not get it. That
follow-up sends nobody to merge or remove the copy, only to replace the link
with a regular file holding the source's lines and the block's. The copy
stays, pinned, so a row sentence there would describe a step nobody was
given.

tests/test_adopt_attributes_eol_block.py holds both follow-ups to that, and
the regular one's new ending exactly. tests/test_update_shape_merged_away.py
turns its fixture's body into one helper and adopts #502's own source a
second time, with a root `.gitattributes` of its own. It then does the
follow-up as a person does it: the copy is merged into the root file and
removed, `check` calls its row `merged-away`, and its NEXT, pasted, drops
that row and nothing else. The root file keeps the merge's bytes, block and
all, and `validate-pins.py` and the doctor are green.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
Merged by sha, 8beb3d9, fetched at
03:08Z, on top of 9efe95a. It brings three commits since 89c6a6c, oldest
first: 173c00e (#510 for #500: `openRepoShape --doctor`), cf118ff (#513
for #499: `--org` and `--project` must name the repository the source
names) and 8beb3d9 (#512 for #501 and #502: update-shape.py,
shape-doctor.py and `collision_follow_up` in
scripts/shape_materialize.py).

One conflict, one hunk, in adopt-project.py `_checked_plan_inputs`, the
only unit both sides changed. Main passes `source` to `_check_names` for
#513's own-name rule; this branch follows that call with #181's comment
and its two refusals (`_refuse_unholdable_values`, then
`_refuse_misread_values`). Resolved as main's call with `source`, then
this branch's comment and two calls, unchanged in order. Main's other
change to the function, `_refuse_another_repository` before any pin's
digest is read, merged cleanly above it. So #513 refuses a plan for
another repository before #181 asks any value.

The comment in the hunk is brought up to date here (rv181g's N-7): since
#259 and #514 a value the loader would misread is only one on a line
written bare, no longer a pin's product. No code line differs from
rv181g's proposed resolution (d55b975, the same merge with cf118ff,
whose adopt-project.py is byte for byte 8beb3d9's).

Every other path is `git merge-tree --write-tree 9efe95a 8beb3d9`'s.
scripts/shape_materialize.py merges cleanly: #512 changes
`collision_follow_up` alone, a function this branch does not touch. No
top-level name is bound twice in adopt-project.py, and no name is read
and never bound; shape_materialize.py and family.py import `parse_yaml`
and `YamlError` twice, as on main.

With this merge alone five tests of this branch's quoting file fail:
their source's `origin` is a path ending in `seed`, which #513 reads as
the repository and refuses `plan --org testorg --project Northwind` as
`adopt-not-the-source`. The next commit puts that `origin` under
`testorg/Northwind`, as cf118ff did for tests/test_adopt_tracking_branch.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 14, 2026
…epo 2.47.0, so the adoption test files that skip without it run there, and a source that keeps its own root `.gitattributes` gets one marked block of `/<copy> text eol=lf` lines appended to it in the split, so a Windows clone writes the pinned copies at the root with the bytes the pin records; the verification counts that file as the source's bytes followed by exactly that block, and `update-shape.py apply --add` extends it (#471) (#511)

v1.0.0 says that every AGENTS.md procedure, adopt included, works end to
end on Linux, macOS and Windows. `tests-windows` left `git-filter-repo` out
on purpose, so every test that executes an adoption skipped there and the
Windows half of that sentence was a claim no runner had checked.
`tests-windows` now installs it at the ubuntu job's pin, on one line because
PowerShell is that runner's shell, and still runs the whole suite. At #497's
base 23 files skipped without it (the 21 that spell
`shutil.which("git-filter-repo")`, `test_adopt_filter_repo_failure.py` and
`test_adopt_git_reads.py`), 178 of their 1731 items. The workflow's comments
and seven test docstrings that named the Windows job as the one without it
now say what stays true. The tool adds about 8 to 11 minutes to the Windows
job. Its first run there (probe run 38312924968) passed 1696, skipped 34
(exotic names, tracked symlinks, `#!` stubs and test helpers), and failed
the one case the rest of this change fixes.

Adoption's first run on a Windows runner failed one ordinary case: a source
that tracks its own root `.gitattributes`. The shape's `* text=auto eol=lf`
went beside it at `shape/.gitattributes`, where it governs only `shape/`, so
the nine copies the pin names at their own paths answered to the source's
rule, a Windows checkout wrote them with CRLF, and `validate-pins.py`, which
digests the bytes on disk (#51), reported shape-copy-drift on each. Under
Git for Windows' default `core.autocrlf=true` any root `.gitattributes` but
the shape's does it. A root-level file takes attributes from the root file
alone, so no file beside it can fix that.

Brett Heap ruled at 2026-10-12T22:48Z that the split appends a marked block
to the source's own file, that the verification accounts for it, and that
`update-shape --add` extends it.

The materializer spells the block once: a comment naming openRepoShape and
#471, then `/<path> text eol=lf` for each pinned copy outside `shape/`, in
the pin's order, appended after the source's bytes when the shape's
`.gitattributes` collided, and never through a symlink. `execute` prints it
and lists its lines in the split commit's message, and its verification
counts the source's file as landed in the root only when the blob there is
exactly the source's bytes followed by that block, recomputed from the
split's pin; anything else is still `adopt-lost`. `update-shape apply --add`
names each added copy in the block, through the ledger, and commits the file
with the copies; a root with no block is left alone. The follow-up says what
the block protects. A source with no root `.gitattributes` keeps today's
behaviour.

The two `git-filter-repo` failure tests whose stub is a `#!` script now run
on Windows, where git starts the stub with its own `sh`. The one that
follows the failure notice compares the leg repositories it reads there with
their `spelled` form, because the notice escapes each backslash of a Windows
path (#212). The test that a `.gitattributes` symlink is not written through
skips on Windows, as the adoption suites' other disk-symlink tests do. The
`update-shape --add` fixture's check that no copy list names a stripped file
reads list entries, not every string in the materializer.

The docs are corrected here (Brett Heap, 2026-10-13T11:30Z), each file
keeping its line count. README.md's "Ambiguous is an answer, and no source
line is overwritten" (was "nothing is overwritten") says a kept root
`.gitattributes` gets the block appended and is verified as the source's
bytes plus the block. Its "Why `.gitattributes` is one of the copies" names
the block as what carries the rule in such a root. AGENTS.md step 7 states
the same exception, and the no-code variant defers to step 7 rather than
restating "by blob sha". docs/handbook.html mirrors both paragraphs, names
#471 in its footer after main's clauses, the last of them #502's (#504
and #494, which landed since, add none), and recomputes its README.md blob
over the merged README.md.

Review round 1. A `.gitattributes` git tracks as a symlink gets no block
even where Git for Windows' default `core.symlinks=false` checks it out as a
plain file: the materializer asks git's index, not only the disk. Before,
the block went into the link, and the adoption failed `adopt-lost` after
both legs were pushed. The plan reads the link from the source tree. Its
follow-up says nothing was appended and git reads no `.gitattributes` that
is a link, and it lists the lines a regular file in its place needs. The
verification reads the shape's pin through the split's links, as the
shape-file check does, so a source keeping `contracts -> meta` in the root
verifies. `--add` extends the last block. README.md's `--add` sentence
names the block, at an unchanged line count.

Merged with main at d41cc29, at 89c6a6c and at 8beb3d9. Each of those
merges' one conflict was the handbook footer, which keeps main's clauses
whole and adds this one after the last, now #502's. Two of what main brought
meet this change, #513 and #512. #513's `adopt-not-the-source` runs in
`plan` before the tree is read, so before the block's follow-up is
predicted, and its `plan-not-the-source` is `check`'s finding and
`execute`'s first refusal, before the split commit appends the block: a
source with a root `.gitattributes` and a wrong `--project` is refused with
no plan written (probed). #512's `merged-away` takes the `.gitattributes`
beside-copy as it takes any other: left in place it is `unchanged`; merged
into the root file and removed, `check` calls its row `merged-away` and
`apply` drops the row, leaving the block and the root file as they were, and
the gates and the doctor go green (probed). The verification of the source's
bytes plus the block runs inside `execute` alone, so it never meets that
row. A `.gitattributes` collision's follow-up is this change's own and
returned before #512's sentence about the row, which every other pinned
collision's follow-up carries; the fold below adds it. The rest do not meet
it. #402 rewrote the doctor's NEXT lines, in a file this change does not
touch. #462 rewrote only the two ruleset hint constants in
`scripts/shape_materialize.py`, and the scaffold's call to
`materialize_assembly_root` passes no `collision_dir`, so it never reaches
the block. #286's `update-rewrite-symlink` and `--add`'s extension of the
block never meet either: a `.gitattributes` that is a symlink is left alone
before the ledger is asked to write it, so `apply --add` exits 0 with the
link and its target untouched (probed over a live and a dangling link). #515
spells the manifest's `neutral_product_pins:` entries, which the block never
reaches, and its tests sit in `tests/test_adopt_pins.py` beside this
change's fixture fix and merged clean. #507 is `family.py` and the quoting
of the commands it prints, #517 is `scaffold-project.py` and #510 is the
installed doctor's wrapper: none is a file this change touches beyond the
docs.

Merged once more with main at b0826e0, the squash of #504 (#183, #178),
which meets this change in `adopt-project.py` and
`scripts/shape_materialize.py`; the handbook footer did not conflict, because
#504 does not touch it. Three hunks conflicted and each keeps both sides: the
`from shape_materialize import (...)` list, `materialize_assembly_root`'s
docstring, and its tail, where #504's `spell` is passed through and the
`.gitattributes` block is appended after it. The two do not contradict each
other, asked of real adoptions through `plan`, `check` and `execute`. (a)
`materialize-attributes-path` needs no `spell`: only `eol_line` raises it,
over the shape's own copy-list names, which `root_key` never resolves through
a link, and `spell` is for the names a source's symlinks lead to. (b) Where
#504's `check` passes, a root `.gitattributes` that is a symlink adopts as it
did, to the blob: the link kept, no block, the shape's copy beside it. Where
`check` now refuses it, `plan-root-symlink` before any leg (a dangling link,
one to an absolute path with nothing there, a climb above the root, a
`.gitattributes` under a link whose target already holds one), this change
alone had written the copy through the link or failed after both legs were
pushed. (c) The rest of #504's diff does not meet the block:
`_refuse_unwritable` runs inside `place`, before the block is appended, and
the block never goes through `place` or through a link. This change's four
imports from `shape_materialize` sit in a statement of their own, so a change
adding names to the shared list merges there without a hand resolution.

Merged again with main at 3121945, the squash of #494 (#370), and
it was clean: no file conflicted, and the handbook footer was not touched,
because #494 changes neither README.md nor docs/handbook.html. It meets this
change in `adopt-project.py` alone, in `_failed_loudly`, which this change
does not touch; `Refusal` now carries no remediation unless its caller names
one, and the one refusal this change adds, `materialize-attributes-path`,
names its own, so it prints as it did.

- The follow-up for a source's own `.gitattributes` now says what #512 (#502)
  says of every other pinned copy, after its own condition: merge the rest of
  `shape/.gitattributes` into the root file only if the whole project should
  check out with LF, and only then remove it, after which `update-shape.py
  check` reports its pin row `merged-away` and `apply` drops the row. The
  link variant says nothing of the row, because it never sends anyone to merge
  or remove the copy, which stays pinned. A new test follows the flow through
  a real adoption: merge the copy into the root file, remove it, paste
  `check`'s NEXT, and the row goes while the root file stays as merged.

The first Windows run of the adoption suites ended in 12 errors, all at
the module fixture of `tests/test_adopt_pins.py`: it deleted a
`shutil.copytree` copy of a repository with `shutil.rmtree`, and Windows
refuses to unlink the read-only objects git writes
(`PermissionError: [WinError 5]`). It now deletes it with `rmtree` from
`tests/conftest.py`, which clears the read-only bit and retries, as the other
suites do.

This pull request supersedes #497: its two reviewed commits (`a528006`, the
install, the comments and seven docstrings; `a8b5064`, a strict Windows
xfail on the case fixed here) are merged in, and the xfail is removed,
because that case passes on Windows over the fix and a strict xfail that
passes fails.

Closes #471

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…5 cites the six pull requests merged since #402, section 6 drops the `--help` verdict line #510 fixed and AT-family N2, which #501's ruling decides, and adds 28 checklist lines from the parents, and section 7 lists the 3 open milestone issues (#472)

Refresh round 2. GitHub was read from 2026-10-14T03:11:22Z to 03:18:32Z;
main was 8beb3d9 (#512) throughout. Every merge commit of the 223
merged pull requests is an ancestor of it.

Section 5 adds, from each squash message and PR body:
- adopt-project.py: #515 (a pinned `007` listed as text) and #513 (a new
  "The source's own name" bullet: `adopt-not-the-source`,
  `plan-not-the-source`, rename first);
- update-shape.py and bump-leg.py: #512 (a new bullet: no NEXT passes
  `--accept-local`; `merged-away` rows dropped by `apply`);
- setup, scaffold, family and the shared helpers: #517 (a `--pin` product
  listed as text) and #507 (`family-landing-in-standard`,
  `family-on-tracking-branch`, a quoted NEXT that pushes the branch);
- shape-doctor.py: #510 (the installed doctor keeps its clone, and passes
  `--json` and `--placement-plan` through). The bullet's "writes nothing
  into the repository" now adds "but a `--placement-plan` file you name":
  a relative plan file given from inside the repository is written there,
  as #460's new line says.

Section 6 drops two lines and adds 28; no known-limit issue closed, so
the 47 issues stay:
- #457, 16 lines: L2-14, which #515 makes true of main; 10 of lane 4's 14
  lines from #513 (00:06Z), two of them, a backslash `origin` and a
  trailing slash, in one bullet; and L1-K1, K2, K3, K7, K9 and K10 from
  lane 1 (01:17Z).
- #458: AT-family N2 is dropped, as lane 4's status note there says
  (00:48Z); 2 lines added from #512.
- #459, 3 lines that #507 and #517 make true of main: the validate-family
  remedies line (03:27Z), the README `--into` line (11:46Z) and L2-15.
- #460: the `openRepoShape --help` verdicts line is dropped (#510 fixed
  it); the `shape currency` line is reworded, because since #402 the
  `apply --branch` is the row's WHY and since #512 it has no
  `--accept-local`, but under `--branch` it is still refused
  `update-branch-uncommitted` before `update-local-drift`; 7 of lane 4's
  8 lines from #510 (23:34Z) are added.

Left out, by round 1's rules: #513's `insteadOf` line and README sentence
(wording), its comments line (comment wording), its extra `git config`
read (no effect a person meets); L1-K4 (cannot arise today); L1-K5, K6
and K8, which describe #518's and #504's branches; and #510's `--help`
synopsis column (cosmetic). #458's 03:26Z restatement is folded into its
existing `repo_shape.py` line: #507 removed the instance it added.

Section 7 lists #370, #471 and #472, the milestone's three open issues of
47, and says #178, #181 and #183 are not in the milestone.

What was read at 8beb3d9, by `git show` or `git grep` of the code or
text each line cites:
- section 5: all six new sentences (adopt-project.py:6986-7030 and
  :7812-7960; update-shape.py:287 and :1939-1969; shape-doctor.py:3238-3243
  and :3298-3314; scaffold-project.py:1293-1330 and the neutral pin
  template's line 30; scripts/family.py:620-720 and :925-960;
  openRepoShape:97-111, :489-490 and :540-558);
- #457: L2-14, the backslash and trailing-slash line
  (`_remote_repository`, adopt-project.py:1164-1182), `check`'s missing
  rename-first remedy (:5137), L1-K3 (scripts/repo_shape.py:2048), L1-K9
  (adopt-project.py:3316-3333);
- #458: both new lines (shape-doctor.py:2005-2060 and :3222-3227), and N2's
  drop (update-shape.py:2915-2919 and `apply_command`);
- #459: all three (validate-family.py:356-358 and :1066-1071; README.md:861-
  862, docs/handbook.html:1247; scaffold-project.py:1313-1330 against
  adopt-project.py:7017-7030);
- #460: the dropped and reworded lines (openRepoShape:97-100, docs/cli.md:
  160-163; shape-doctor.py:3306-3314 and `cmd_apply`'s refusal order), the
  adoption NEXT with no `--org` (shape-doctor.py:6141-6143), the two
  `--placement-plan` line (openRepoShape:540-558, shape-doctor.py:6828),
  the "writes nothing" line (openRepoShape:55 and :605, README.md:1112,
  docs/handbook.html:1467) and the kept-clone line (openRepoShape:490);
- kept lines whose code a landing touched: #458's "not hand-edited" commit
  and title-less `gh pr create` (update-shape.py:2349-2353, :3796-3797),
  AT-family N3, #460's `=word` line (`UNQUOTED_POSIX` admits `=`), and
  #459's unquoted setup `cd` and step (7) (setup-project.py:2023-2029,
  :2169).

The rest rest on the posting lane's measurement, not re-read here: of
#457's lines, lane 4's mirror path, `--source` after the clone, recorded
`source:`, `ATLAS`, fork, `straße` and null-`org:` lines (measured at
cf118ff; adopt-project.py is the same at 8beb3d9), and lane 1's L1-K1,
K2, K7 and K10 (measured on 8beb3d9); of #460's, the TERM, interrupted-run
and drifted-project-test lines (measured at 173c00e; `openRepoShape` is the
same at 8beb3d9).

The file is 7,354 words, from 6,611: section 5 is 2,291 (2,085), section
6 4,351 (3,607) and section 7 109 (316).

No test names `docs/release-notes/` (`git grep -n release-notes
origin/main -- tests/` finds nothing), but two tests in
tests/test_repo_hygiene.py read every tracked `.md` through `git
ls-files`: the host-absolute-path test (`HOST_ABSOLUTE_PATH`, :1496-1527)
and the "<word> naming families" count (:1545-1601). Their two patterns,
applied to this commit's file, match nothing. No test was run.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 14, 2026
…5 cites #511 and drops the Windows qualifier now that the Windows job runs the adoption suites, section 6 cites the v1.1.0 issue of each line the triage scheduled, drops the five lines closed on their parents and #408, and adds #511's line-ending limits, and section 7 lists #472 alone (#472)

Refresh round 3b, against main 5ed3e5d (#511, merged 2026-10-14T16:08:28Z,
closing #471). All 226 merge commits are on main.

Section 5. #511 is cited twice: under adopt-project.py, as a new bullet on
a source's own `.gitattributes`, and under the test suite and CI, in the
"Three operating systems" bullet. "Every source path is verified by blob
sha" gains the exception AGENTS.md step 7 now states. The claims were read
in main's code:
- `.github/workflows/tests.yml`:171-182, where `tests-windows` installs
  `git-filter-repo==2.47.0`; :209, where it runs the whole suite; and
  :118-125, where it runs on pull requests, not on a push to main;
- `scripts/shape_materialize.py`: `EOL_BLOCK_HEAD` (:701), `eol_line`
  (:712-721), `eol_block_paths` (:724-729), `with_eol_block` (:738-744), and
  `_append_eol_block` (:813-831), which writes through no symlink, as called
  from `materialize_assembly_root` (:2256-2257);
- `adopt-project.py`: `_land_the_eol_block` (:8081-8124, called at :8063)
  and the split message's `APPENDED TO` (:7953);
- `update-shape.py`: `_extend_the_eol_block` (:3061-3089).

The qualifier. Main's workflow now does what the unqualified sentences say,
so both `<!-- drop the Windows qualifier once #471 lands as proposed -->`
comments and the two sentences they marked are gone.

Section 6, re-read after lane 3's v1.1.0 triage (#473, 16:03:48Z, on
Brett Heap's approval at 14:31:28Z):
- 74 lines marked `→ #N` or "rides with #N" on their parent now end with
  that number, as in (#524). For each parent, the set of numbers cited
  equals the set its marks name. One sentence in the section's opening
  says what the number is.
- Dropped, each closed on its parent: `--project ATLAS` and a fork's
  `origin` (#457, ruled behaviour); the scaffold's three working trees
  (#459, fixed by #505 and kept documented); and the `shape currency`
  row's uncommitted-copy line (#460, obsolete since #402 and #512). Also
  #408, closed as fixed by #494.
- Added, under #457: the symlinked root `.gitattributes`, the nested one
  (#530), and a root adopted before #511 (lane 4, 2026-10-12T23:25Z);
  then #511's three posted at 16:09:31Z: a copy written through a
  symlinked directory, a CRLF `.gitattributes` under a rule other than
  `text=auto`, and a `filter=` or `working-tree-encoding=` rule. Its
  fourth, a test helper that feeds text on stdin, is left out: no person
  meets it. These rest on lane 4's measurement (V511c at 1ce4f45, whose
  tree is main's); `eol_line` writing only `text eol=lf` and the symlink
  skip were read in code.
- Reworded:
  - AT-scaffold N9 (#458), with lane 4's 16:10:03Z note. `cmd_apply`
    always stages the pin and the manifest (`update-shape.py`:3280-3281),
    and `_land` prints them all (:3224).
  - The annotated-tag line (#459). It is now ruled that `bump --to` peel
    it (#541), so "One rule is still to be chosen" is gone.
  - The Windows symlink line (#457), narrowed to its parent's own words.
    #511's end-to-end test of a source that tracks a symlinked
    `.gitattributes` under `core.symlinks=false` carries only
    `needs_filter_repo` (tests/test_adopt_ignored_shape_files.py:862-863),
    so it now runs on Windows.
- Left out, new: the three CI lines new on #461 (the merge queue and two
  CI costs). They are this repository's CI, not a limit of the shape.
- Lane 1 has posted no #504 batch, so D13 stays out.

Section 7: v1.0.0 is 46 closed and #472 open. #181 is still outside the
milestone.

No claim is made about an unlanded pull request (#518, #551), and nothing
is said about a tag or a date. 7,628 words before, 7,906 after.

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