Skip to content

adopt-project.py and shape_materialize.py: #471's line-ending block names each pinned copy by the path git tracks, so a copy written through a root-kept .github -> ci no longer checks out with CRLF under * text=auto eol=crlf and fails validate-pins.py after execute verified (#807) - #844

Merged
brettheap merged 3 commits into
mainfrom
fix/807-eol-block-tracked-spelling
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-1 (openRepoShape-1)
Claim: #807 (comment)

A v1.1.1 draft, authored on main at 24a977c0. It lands in its turn in the v1.1.1 order agreed on #473, and is refreshed then.

What

#471's line-ending block now names each pinned copy by the path git tracks. For a copy written through a symlinked directory the root keeps, it then names the copy again by its pinned path. Before, the block named each copy by its pinned path alone, the path the shape wrote it at. Git tracks a copy written through a link where the link leads, so that line governed nothing.

A source that keeps .github -> ci in the root and has * text=auto eol=crlf now gets this block:

* text=auto eol=crlf
# openRepoShape (#471): the pinned shape copies below check out with LF everywhere, as contracts/shape-pin.yaml records their bytes
/scripts/validate-pins.py text eol=lf
/scripts/validate-manifest.py text eol=lf
/scripts/bootstrap.py text eol=lf
/Makefile text eol=lf
/.gitignore text eol=lf
/ci/workflows/validate.yml text eol=lf
/.github/workflows/validate.yml text eol=lf
/AGENTS-shape.md text eol=lf
/scripts/repo_shape.py text eol=lf
/scripts/validate-repository-naming.py text eol=lf
/contracts/repository-naming.yaml text eol=lf

On main, the line /ci/workflows/validate.yml text eol=lf was missing.

  • With the link kept. execute verifies, and a clone of the split checks ci/workflows/validate.yml out with LF. Its validate-pins.py exits 0. A chain of links, .github -> l1 -> ci, is followed to its end.
  • After update-shape.py's remedy. update-shape.py refuses to rewrite a copy reached through a link, and says to replace the link with what it points at. A person who does that (git rm .github, git mv ci .github, commit) and clones afresh has the copy at .github/workflows/validate.yml. The pinned line now governs it, so it checks out with LF and validate-pins.py exits 0, as on main.
  • execute's summary line now counts the extra line: appended to .gitattributes: a `text eol=lf` line for each of 10 pinned copies, and 1 more under the pinned path of each copy written through a symlink, which governs it once the link is replaced, so every checkout keeps their bytes (#471); none of its own lines was changed. The verification line says the shape's block of 11 `text eol=lf` lines.
  • The pin is unchanged. Its row still names the copy .github/workflows/validate.yml, the path it was written at. A clone opens it through the same link, as before.
  • A source with no symlinked directory gets the same block, byte for byte, and execute prints the same lines about it.

Why (repro on main)

Measured on main 24a977c0 with git 2.43. The source kept .github -> ci and a ci/workflows/ci.yml in the root. Its root .gitattributes was * text=auto eol=crlf, and every question of the plan was answered root.

  • check and execute exited 0, and execute printed adoption verified. For the workflow, the split's .gitattributes held only /.github/workflows/validate.yml text eol=lf.

  • Git tracks that copy as ci/workflows/validate.yml, so the line governed nothing. A clone wrote the copy with CRLF, under the source's own rule.

  • The clone's validate-pins.py exited 1:

    FINDING shape-copy-drift: .github/workflows/validate.yml: sha256 bcdd7a9d45594d4847452c0779f6366f4dca195a9cd27d76385965af1f733b53
           contracts/shape-pin.yaml records fb8c0360863ad953af7825f3be01ed27f8cf1269e5cab19dc0d6c5423161b487
    
  • The same source without the link exited 0. Its split .gitattributes has the same bytes on main and on this branch (sha256 ffbd9bc9…).

check asked git about the same wrong root, and that had two more effects on main:

This PR's first head named the tracked path alone. Its review measured the remedy above in three states:

State main tracked path alone this head
Fresh clone after the remedy: CRLF lines in .github/workflows/validate.yml 0 223 0
Fresh clone after the remedy: validate-pins.py exit 0 exit 1, shape-copy-drift exit 0

So this head keeps the pinned line as well.

How

Each place that writes or reads the block now names the path git tracks, and then the pinned path where the two differ. Each one resolves a copy's path through the root's symlinks the way it already did for another purpose.

No other file changed. scripts/repo_shape.py is a pinned shape copy and is untouched. update-shape.py is not changed. It never writes a line that names the wrong path, for two reasons:

Tests

The new tests make 24 test ids. Fifteen are in tests/test_adopt_eol_block_through_links.py (new), and nine are added to tests/test_adopt_attributes_eol_block.py.

  • Units (tests/test_adopt_attributes_eol_block.py).
    • eol_block_paths names a copy where tracked maps it, then by its pinned path.
    • It names by the pinned path alone a copy that is not mapped, is mapped to nothing, or is mapped to no literal name.
    • The materializer writes both lines through .github -> ci and through the chain, the tracked one first. These are real symlinks, so the test skips on Windows.
    • The verification counts that block through either link in the split tree.
    • Control: a link out of the split keeps the pinned line alone.
  • check (links in the source's tree only, so no filter-repo is needed).
    • The nested ci/.gitattributes is now plan-nested-attributes.
    • Under core.safecrlf=true, check passes the source. execute still stops later, at git submodule add (fatal: LF would be replaced by CRLF in .gitmodules), after both legs are pushed. Main does the same for this source with no link, so it goes to Tracking: adopt-project.py, converting an existing repository in place #457's checklist.
  • End to end, execute --local-remote-dir, git-filter-repo and a stub gh, for a link, a chain and no link.
    • execute verifies, and the split's .gitattributes is the source's bytes then the block with both lines.
    • With no link, the block is each pin row's own path, which is main's block, and execute's summary line is main's.
    • A clone checks README.md out with CRLF (the control that the rule reached the checkout) and every pinned copy without.
    • The clone's validate-pins.py exits 0.
    • update-shape.py's remedy. The clone replaces the link with what it points at, commits, and is cloned afresh. README.md is CRLF, .github/workflows/validate.yml is LF, and validate-pins.py exits 0.
  • Controls.
    • update-shape.py apply on a linked clone is refused update-rewrite-symlink, and .gitattributes keeps its bytes.
    • core.symlinks=false is still refused first, as adopt-symlinks-off, with no remote made.

Targeted gate on d62f9aa (dirty 0), 99 test files: every file that runs adopt-project.py, every file that names the block's functions, tests/test_shape_doctor.py, tests/test_scaffold_leg_path_collisions.py and tests/test_cli_reference.py. Each mode ran in four shards:

  • with git-filter-repo on PATH: 6909 passed, 15 skipped, 1 xfailed;
  • with it hidden: 6519 passed, 405 skipped, 1 xfailed.

Revert table. Each change was reverted alone in a scratch clone, and the two test files were run:

Reverted alone Result New tests that fail
eol_block_paths ignores tracked (main's behaviour everywhere) 11 failed, 48 passed, 2 skipped test_each_copy_is_named_where_git_tracks_it; test_a_copy_written_through_a_link_is_named_where_git_tracks_it and …_counted_where_git_tracks_it [a-link, a-chain]; both check tests; test_execute_verifies_a_block_that_names_the_tracked_path and test_a_clone_checks_every_copy_out_as_pinned_and_its_pins_pass [a-link, a-chain]
eol_block_paths names the tracked path alone (this PR's first head) 9 failed, 50 passed, 2 skipped test_the_clone_stays_lf_once_the_link_is_replaced [a-link, a-chain]; test_each_copy_is_named_where_git_tracks_it; …_named_… and …_counted_… [a-link, a-chain]; test_execute_verifies_… [a-link, a-chain]
the literal-name fallback (_tracked_or_pinned) 1 failed, 58 passed, 2 skipped test_a_copy_tracked_nowhere_nameable_keeps_its_pinned_path[no-literal-name]
_append_eol_block names the pinned path 6 failed, 53 passed, 2 skipped …_named_… [a-link, a-chain]; test_execute_verifies_… and test_a_clone_checks_… [a-link, a-chain]: the verification calls the source's .gitattributes adopt-lost
_land_the_eol_block expects the pinned path 6 failed, 53 passed, 2 skipped …_counted_… [a-link, a-chain]; test_execute_verifies_… and test_a_clone_checks_… [a-link, a-chain]
_root_attributes names the pinned path 1 failed, 58 passed, 2 skipped test_check_finds_a_nested_file_that_outranks_the_tracked_line
_as_the_split_writes names the pinned path 1 failed, 58 passed, 2 skipped test_check_asks_git_add_about_the_copy_where_git_tracks_it
_appended_lines counts the block's lines as copies 2 failed, 57 passed, 2 skipped test_execute_verifies_… [a-link, a-chain]

The controls pass under every revert: the no-link case, the link out of the split, update-shape.py and core.symlinks=false. Each one holds behaviour that main already has. The remedy test passes with main's behaviour too: main healed there.

Not in this PR

  • A root's .gitattributes that is itself a symlink. Its follow-up (_attributes_link_follow_up) lists the lines a regular file in its place needs by the pinned spelling alone. With .github -> ci as well, it lists /.github/workflows/validate.yml text eol=lf but not /ci/workflows/validate.yml text eol=lf, while check's rehearsal of that case uses both. That text is built from the pin lists before any walk. It goes to Tracking: adopt-project.py, converting an existing repository in place #457's checklist.
  • The plan's follow-up for a kept .gitattributes. It says the split "appends one text eol=lf line ... for each pinned copy outside it". A copy written through a symlink now has two lines. The text is built before any walk and is unchanged here. It goes to Tracking: adopt-project.py, converting an existing repository in place #457's checklist.
  • A link to a name no line can spell literally. A pinned copy that a root-kept link leads to such a name (a space, a glob character or a backslash in the target) keeps its pinned line alone, which governs nothing while the link is kept, and check does not say so. It goes to Tracking: adopt-project.py, converting an existing repository in place #457's checklist.

Known limits

Closes #807

🤖 Generated with Claude Code

Summary by Sourcery

Fix generated line-ending blocks so pinned copies reached through root-kept symlinks remain LF across adoption, checkout, and later link replacement.

Bug Fixes:

  • Ensure generated line-ending rules govern pinned copies written through root-kept symlinked directories, preventing CRLF checkouts and subsequent pin validation drift.
  • Keep the pinned-path rule alongside the tracked-path rule so the copy remains correctly governed after the symlink is replaced.

Enhancements:

  • Align materialization, adoption checks, verification, and execution summaries around Git-tracked paths, including symlink chains and safe fallbacks for unnameable or unresolved paths.

Tests:

  • Add unit, adoption, verification, checkout, symlink-replacement, nested-attributes, safecrlf, and symlink-disabled coverage for tracked-path line-ending blocks.

…ames each pinned copy by the path git tracks, so a copy written through a root-kept `.github -> ci` no longer checks out with CRLF under `* text=auto eol=crlf` and fails `validate-pins.py` after `execute` verified (#807)

The block named the copy by its link spelling, `/.github/workflows/validate.yml text eol=lf`, while git tracks it at `ci/workflows/validate.yml`, so the line governed nothing. Each place that writes or reads the block now names the path git tracks, a chain of links followed to its end:
- the materializer's append (`_append_eol_block`, through `_followed`);
- the verification's count of the file (`_land_the_eol_block`, through `_SplitTree.tracked_at`);
- the root `check` asks git about (`_root_attributes`, `_as_the_split_writes`, from `_pinned_landings`).

A copy whose links lead out of the root or cannot be followed, or to a name no line can spell literally, keeps its pinned path. A source with no symlinked directory gets the same block, byte for byte. scripts/repo_shape.py and update-shape.py are unchanged: update-shape.py never writes through a link.

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

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Fixes #807 by making every EOL block producer and verifier name pinned copies by their Git-tracked landing path after following root symlinks, preventing CRLF checkout drift under * text=auto eol=crlf; unresolved paths retain the pinned spelling, with broad regression and end-to-end coverage.

Sequence diagram for tracked-path EOL block generation and verification

sequenceDiagram
    participant Materializer as shape_materialize.py
    participant Root as Source root
    participant Git as Git tracked tree
    participant Verifier as adopt-project.py
    participant Clone as Clone checkout

    Materializer->>Root: _tracked_at(target, pinned_path)
    Root-->>Materializer: symlink-resolved landing path
    Materializer->>Materializer: eol_block_paths(shape_files, collision_dir, tracked)
    Materializer->>Root: Append eol_block to .gitattributes
    Verifier->>Git: _SplitTree.tracked_at(pinned_path)
    Git-->>Verifier: tracked landing path or None
    Verifier->>Verifier: _land_the_eol_block()
    Verifier-->>Clone: adoption verified
    Clone->>Git: Checkout pinned copies
    Git-->>Clone: LF for each tracked EOL block path
Loading

Flow diagram for symlink-aware EOL path selection

flowchart TD
    A[Pinned shape copy path] --> B{Root symlinks resolve it?}
    B -->|Yes| C[Resolve landing path with _followed]
    C --> D{EOL_PATH_RE can spell landing path?}
    D -->|Yes| E[Use Git-tracked landing path]
    D -->|No| F[Keep pinned path]
    B -->|No| F
    E --> G[Write and verify text eol=lf block]
    F --> G
    G --> H[Clone checks out pinned copy with LF]
Loading

File-Level Changes

Change Details Files
Generate EOL block entries using the path Git actually tracks after following root symlinks, while preserving pinned-path fallbacks for unresolved or unrepresentable destinations.
  • Added tracked-path mapping and literal-name fallback logic to EOL block generation.
  • Materialization resolves symlinked paths when appending the block and records the resulting paths.
  • Verification resolves paths through split-tree symlinks so the expected block matches materialized output.
  • Root attribute planning and git add rehearsal now use planned pinned landings for block entries.
  • Added split-tree path resolution support for tracked-path lookup.
scripts/shape_materialize.py
adopt-project.py
Add unit, integration, and regression coverage for symlinked directories, symlink chains, safecrlf behavior, nested attributes, cloning, and existing refusal controls.
  • Tested block-path mapping for tracked, unresolved, and non-literal destinations.
  • Covered .github -> ci, chained links, and no-link controls through materialization and verification.
  • Added end-to-end checks that clones preserve CRLF for ordinary files, LF for pinned copies, and pass pin validation.
  • Verified check handles nested attributes and core.safecrlf=true with the corrected tracked spelling.
  • Confirmed update and symlink-disabled refusal behavior remains unchanged.
tests/test_adopt_attributes_eol_block.py
tests/test_adopt_eol_block_through_links.py

Assessment against linked issues

Issue Objective Addressed Explanation
#807 Write and verify the line-ending block using each pinned copy’s path as tracked through root-kept symlinks, including symlink chains, so clones check out the copies with LF and pin validation passes. ✅
#807 Apply tracked-path naming consistently in adoption checks and verification while preserving the existing block byte-for-byte when there is no symlinked directory, and retain the existing refusal when symlinks are disabled. ✅
#807 Ensure update-shape does not add block lines using a symlink spelling when that differs from the tracked path. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-17T20:00:03Z) Lane 1's independent review of #844 at f7c8c32: CHANGES NEEDED

The fix does what #807 asks. I found no case where the three readers disagree: the materializer (_tracked_at → _followed), the verification (_SplitTree.tracked_at) and check (_pinned_landings) named the same paths in every case I tried. Ordinary sources are byte-identical to main. One regression blocks PR-READY: on the path update-shape.py itself tells a person to take, this PR turns main's healed clone into one with shape-copy-drift.

Basis

  • The head merged cleanly into main fc867207 (local merge 4e622dc9; adopt-project.py and scripts/shape_materialize.py auto-merged).
  • Probes ran with a stub gh exiting 97, GH_TOKEN=bogus-r807, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_ALLOW_PROTOCOL=file and git 2.43.
  • Each probe adopted a source end to end: plan, every unresolved entry answered root, check, execute --local-remote-dir, a --recurse-submodules clone, then validate-pins.py in the clone.

The issue's line

  • The link spelling: fixed on the branch, not fixed on main.
    • Main, with .github -> ci and * text=auto eol=crlf: check 0, execute adoption verified, the clone's copy is CRLF, and validate-pins.py exits 1 with FINDING shape-copy-drift: .github/workflows/validate.yml: sha256 bcdd7a9d….
    • Merge: the block's line is /ci/workflows/validate.yml text eol=lf, no pinned row is CRLF, and validate-pins.py exits 0.
  • The issue's update-shape.py test cannot run as written. The writer is right. _refuse_linked_copies (update-shape.py:4518) runs before _extend_the_eol_block (:4536). The row is copy-is-link even when the copy is unchanged (check prints copy-is-link 1 unchanged 10). apply exits 2 with REFUSED update-rewrite-symlink: \apply` would rewrite .github/workflows/validate.yml, but .github in this root is a symlink…, and .gitattributes` keeps its bytes.
    • Checklist item 1 holds: update-shape.py owes no change for the lines it writes.
    • Checklist item 2 is owed now (finding 1).

Findings, ranked

1. SHOULD-FIX, blocks PR-READY: the block goes stale on update-shape.py's own remedy, where main healed

For every adopter who keeps the link, the tools point to one next step:

  • The doctor on the fresh clone says DRIFTED (1 copy-is-link), with NEXT update-shape.py check.
  • check exits 1: copy-is-link … replace the link with what it points at, REFUSAL AHEAD.
  • apply's remediation says the same: "replace the link with what it points at … commit that, and run this command again".

Repro: adopt the link source as above, clone it, then:

git rm -q .github && git mv ci .github && git commit -qm "replace the link"
git clone -q --no-local --recurse-submodules <that clone> fresh
cd fresh && python3 scripts/validate-pins.py
Step main fc86720 head / merge
Adopted clone, link kept pins exit 1 (drift); doctor DRIFTED (1 copy-is-link) pins exit 0; doctor DRIFTED (1 copy-is-link)
After the remedy (move), fresh clone .github/workflows/validate.yml has 0 CRLF lines; pins exit 0; doctor COMPLIANT (exit 0) 223 CRLF lines; pins exit 1 with FINDING shape-copy-drift: .github/workflows/validate.yml: sha256 bcdd7a9d…; doctor DRIFTED (1 locally-modified)
After the remedy (copy, cp -a ci/. .github/) pins exit 0 pins exit 1, the same finding; git warns LF will be replaced by CRLF the next time Git touches it

So on head, no state of such a project reaches COMPLIANT without a hand edit of the block, and nothing asks for that edit. The body files this under "Not in this PR" but does not say that main healed here.

Two ways to clear it; the coordinator chooses:

  • (a) Name both spellings where they differ: the tracked path, then the pinned path. It is one change in eol_block_paths, which all three readers call, so they still agree:
        tracked = tracked or {}
        return list(dict.fromkeys(
            name for path in shape_files
            if not path.startswith(f"{collision_dir}/")
            for name in (_tracked_or_pinned(path, tracked.get(path)), path)))
    • I measured it in a scratch clone of the head. For link and chain: check 0, execute verified, every pinned row LF, pins exit 0. After the remedy (move and copy): a fresh clone is LF and pins exit 0. The no-link block keeps sha256 ffbd9bc9….
    • Five test ids that assert the exact block need the second line: test_each_copy_is_named_where_git_tracks_it, test_a_copy_written_through_a_link_is_counted_where_git_tracks_it[a-link, a-chain], and test_execute_verifies_a_block_that_names_the_tracked_path[a-link, a-chain]. The other 52 pass, 1 skipped.
    • While the link is kept, the pinned line governs nothing. Once the link is replaced, it governs the copy.
    • "a text eol=lf line for each of 10 pinned copies" would then sit over 11 lines; its wording would need a look.
  • (b) Keep the design and say so: take the text replacements T1 and T2 below, and file the regression on Tracking: adopt-project.py, converting an existing repository in place #457's checklist.

2. TEXT: core.safecrlf=true still stops execute, as it already did on main

  • With * text=auto eol=crlf + /.gitattributes text eol=lf, a link, and core.safecrlf=true:
    • main: check 1 with plan-file-reencoded naming ci/workflows/validate.yml;
    • head: check 0, then execute exits 2 with REFUSED mounting the legs … fatal: LF would be replaced by CRLF in .gitmodules, and "both legs are pushed".
  • The same source with no link gives check 0 and the same execute stop on main and on head. So this PR creates no gap, but the body's "the source passes" reads as more than check (T3).

3. No other defect found

Results are check rc / execute / validate-pins.py rc. "verified" means execute printed adoption verified. Unless the row says otherwise, the source has * text=auto eol=crlf.

Case main merge or head
.github -> ci; chain l1 -> ci; chain l1 -> l2 -> ci; nested target infra/gh; ./ci/ 0 / verified / 1 (drift) 0 / verified / 0
.github/workflows -> ../ci/workflows, kept in the root, and its chain through l1 0 / verified / 1 0 / verified / 0; line /ci/workflows/validate.yml
scripts -> tools kept in the root (5 copies, validate-pins.py among them) 0 / verified / 1; 5 rows CRLF 0 / verified / 0; lines /tools/…
contracts -> meta kept in the root (the pin read through it) 0 / verified / 1 0 / verified / 0; line /meta/repository-naming.yaml
Case twins: ci/ beside CI/; and .github and .GitHub both -> ci 0 / verified / 1 0 / verified / 0
.github -> CI with only ci/; infra/../ci; ../outside; an absolute target check 1, plan-root-symlink the same
Targets my ci, c[i], c\i 0 / verified / 1 0 / verified / 1 (the pinned-line fallback; declared)
.gitattributes -> config/attributes plus .github -> ci 0 / verified / 0 the same
ci/.gitattributes *.yml eol=crlf under * text=auto 0 / verified / 1 check 1, plan-nested-attributes
ci/.gitattributes *.md text 0 / verified / 1 0 / verified / 0
core.symlinks=false check 0; execute 2, adopt-symlinks-off, no remote identical output
  • The verification agrees with the materializer. execute verified in every accepted link case. That means _land_the_eol_block recomputed the materializer's exact block; a mismatch would have been adopt-lost.
  • Byte-identity, main against merge. Sources tried: no link with * text=auto eol=crlf; no .gitattributes; *.png binary with no final newline.
    • plan, check and execute stdout and stderr are identical once the run directory and shas are normalised.
    • The split .gitattributes bytes are identical (ffbd9bc9…, e2b88cf2…, 2177c38c…).
    • The split tree is identical except where it names the standard's commit or the run's path: the pin, shape/README.md, .gitmodules and project.yaml.
  • Nothing new reaches out or writes elsewhere. There is no new network call, and no new write, remove or run path: the change only renames lines in a file the split already writes.

Tests

  • Revert rows I re-ran, each change reverted alone, over the two test files (57 tests). All four match the writer's table:
    • eol_block_paths ignores tracked: 9 failed;
    • _root_attributes: 1 failed (test_check_finds_a_nested_file_that_outranks_the_tracked_line);
    • _as_the_split_writes: 1 failed (test_check_asks_git_add_about_the_copy_where_git_tracks_it);
    • _land_the_eol_block with no map: 6 failed (counted [a-link, a-chain], execute_verifies and clone_checks [a-link, a-chain]).
  • Merge gate at 4e622dc9, dirty 0, git-filter-repo on PATH. It ran the two touched files, tests/test_shape_doctor.py, tests/test_scaffold_leg_path_collisions.py and tests/test_cli_reference.py: 620 passed, 4 skipped, 1 xfailed, EXIT=0.
  • The counts are right: 21 new ids (12 + 9), as the body says.
  • The asserts are honest. The end-to-end test checks that README.md IS CRLF, so the rule demonstrably reached the checkout. The no-link block is compared to eol_block(pin rows), which is main's algorithm.
  • Sonar: Quality Gate passed, 0 new issues, 0.0% duplication on new code.

Windows

  • The new paths are NTFS-legal. The tests that make links on disk, and the end-to-end fixture, skip on win32.
  • _followed already translates os.sep, and _SplitTree reads link targets from blobs.
  • The tests-git-bash job runs only the launcher and installer suites, so none of the new tests run on Windows in CI.

CI

At 19:58:46Z, SonarCloud, package and tests-git-bash pass. tests (job 117480502936, started 19:14:26Z) is still in progress. Re-read it before landing.

Text replacements

  • T1, "Not in this PR", second bullet (if (b)). Replace the whole bullet with:

    The block after a person replaces the link. update-shape.py check reports such a copy copy-is-link, and apply refuses it until the link is replaced with what it points at. A root that then holds a real .github/ keeps the block line /ci/workflows/validate.yml, and no line names .github/workflows/validate.yml, so under * text=auto eol=crlf a fresh clone writes that copy with CRLF and validate-pins.py exits 1 with shape-copy-drift. On main the same step healed the clone, because main's line named .github/workflows/validate.yml. It goes to Tracking: adopt-project.py, converting an existing repository in place #457's checklist.

  • T2, "How" (if (b)). Replace "update-shape.py needs no change, for two reasons:" with:

    update-shape.py is not changed. It never writes a line that names the wrong path, for two reasons:

    After its two bullets, add:

    Once a person replaces the link, as apply's refusal says to, the block names a path that no longer holds the copy: see Not in this PR.

  • T3, Tests. Replace "Under core.safecrlf=true, the source passes." with:

    Under core.safecrlf=true, check passes the source. execute still stops later, at git submodule add (fatal: LF would be replaced by CRLF in .gitmodules), after both legs are pushed. Main does the same for this source with no link, so it goes to Tracking: adopt-project.py, converting an existing repository in place #457's checklist.

  • If (a) is taken, the What block, the eol_block_paths bullet in How, and the test counts must be rewritten from the new run.

The title and the Lane, Claim, Closes #807 and robot lines are true at the head. The body is 10,795 characters. The appended "Summary by Sourcery" is the bot's and is harmless.

For the checklist

  1. .gitmodules and core.safecrlf=true (main too). With * text=auto eol=crlf, check passes, and execute refuses at git submodule add after both legs are pushed. _reencoded_files_findings never asks git about .gitmodules.
  2. core.symlinks=false with a root-kept link (main too). check exits 0, and execute refuses adopt-symlinks-off. It refuses before any remote exists, but check and execute disagree.
  3. A symlinked root .gitattributes. The follow-up from plan, check and the split commit's message lists /.github/workflows/validate.yml text eol=lf, which governs nothing while the link is kept. Since this PR, check's rehearsal of that same case (_root_attributes with own = b"") uses the tracked spelling, so what check asks git differs from what the follow-up tells a person to write.
  4. A target no line can spell (my ci, c[i], c\i). The copy still drifts after execute verified, and check says nothing.

Lane 1 takes option (a) for finding 1, and T3, in a fix round on the same branch; the same reviewer re-verifies.

Lane: openreposhape-1 (openRepoShape-1)

…oot-kept symlink is named by its pinned path too, after the tracked one, so replacing the link as `update-shape.py` asks no longer leaves the copy to check out with CRLF and drift, where main healed (#807, #844's review)

r807's finding 1, option (a), on the coordinator's ruling: `eol_block_paths` names each copy where git tracks it and then, where that differs, by its pinned path. All three readers (the materializer, the verification and `check`) call it, so they still agree. The pinned line governs nothing while the link is kept, and governs the copy once a person replaces the link with what it points at, as `update-shape.py`'s `update-rewrite-symlink` asks. A source with no symlinked directory gets the same block, byte for byte, and `execute`'s line about it is unchanged; with a link it says the extra line.

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-1, 2026-10-18T00:27:55Z) Lane 1's re-verification of #844 at d62f9aa: PR-READY

Fix round 1 fixes my one blocking finding and takes the safecrlf sentence (T3). The remedy path heals again, as it does on main. No-link sources are still byte-identical to main, and the summary line is main's word for word. What's left is one sentence in the body and one docstring line that is too long; neither needs a code change to land.

Basis

  • The head (one commit on f7c8c328) merges cleanly into main d8b8eb33: adopt-project.py and scripts/shape_materialize.py auto-merged, dirty 0.
  • Probes ran with a stub gh, GH_TOKEN=bogus-r807, an empty GH_CONFIG_DIR, GIT_CONFIG_GLOBAL=/dev/null, GIT_SSH_COMMAND=false, GIT_ALLOW_PROTOCOL=file and git 2.43.
  • Each probe adopted a source end to end: plan, unresolved entries answered root, check, execute --local-remote-dir, a --recurse-submodules clone, then validate-pins.py.

My findings at f7c8c32

1. The remedy path regression: FIXED

I followed the remedy myself, as update-shape.py asks: git rm .github (and l1 for the chain), git mv ci .github, commit, then a fresh --recurse-submodules clone. The source keeps .github -> ci and has * text=auto eol=crlf.

Fresh clone after the remedy main d8b8eb3 f7c8c32 d62f9aa merge
Lines naming validate.yml in the block 1 1 2 2
CRLF lines in .github/workflows/validate.yml 0 223 0 0
validate-pins.py exit 0 exit 1, FINDING shape-copy-drift: .github/workflows/validate.yml: sha256 bcdd7a9d… exit 0 exit 0
shape-doctor.py COMPLIANT (exit 0) DRIFTED (1 locally-modified) COMPLIANT (exit 0) COMPLIANT (exit 0)
  • README.md is CRLF in every fresh clone, so the source's rule did reach the checkout.
  • The merge also heals for the copy variant (cp -a ci/. .github/) and for the chain .github -> l1 -> ci. Both give 0 CRLF lines, pins exit 0 and COMPLIANT.

2. core.safecrlf=true: FIXED (text)

T3 is in the Tests section word for word, and the check test's docstring says the same. Behaviour on the merge matches it: check 0, then execute 2 at git submodule add with fatal: LF would be replaced by CRLF in .gitmodules.

3. The rest of my earlier matrix, re-run on the merge: no regression

  • These cases now verify with both lines (execute printed adoption verified, validate-pins.py exit 0, no pinned row CRLF):
    • .github -> ci; the chain through l1; the chain through l1 -> l2; a nested target infra/gh; ./ci/;
    • .github/workflows -> ../ci/workflows kept in the root, and its chain;
    • case twins: ci/ beside CI/, and .github and .GitHub both -> ci;
    • a harmless nested ci/.gitattributes.
  • scripts -> tools kept in the root: a 15-line block (/tools/… then /scripts/…), summary "and 5 more", pins exit 0. Main has 5 rows CRLF and pins exit 1.
  • contracts -> meta: /meta/repository-naming.yaml, then /contracts/repository-naming.yaml, pins exit 0.
  • Unchanged from the first head, as the body says:
    • ci/.gitattributes *.yml eol=crlf: check 1, plan-nested-attributes;
    • a target with a space: the pinned line alone, still drifting (a declared limit);
    • a symlinked root .gitattributes: pins exit 0;
    • core.symlinks=false: check 0, execute 2 with adopt-symlinks-off.

4. Byte-identity with no link, main d8b8eb3 against the merge

  • Sources tried: * text=auto eol=crlf; no .gitattributes; *.png binary with no final newline.
  • Printed output: plan, check and execute stdout and stderr are identical once the run directory and shas are normalised.
  • Written bytes: the split .gitattributes is identical (ffbd9bc9…, e2b88cf2…, 2177c38c…). The split tree is identical except the files that name the standard's commit or the run's path: the pin, .gitmodules, project.yaml and shape/README.md.
  • The summary line is main's, word for word: appended to .gitattributes: a `text eol=lf` line for each of 10 pinned copies, so every checkout keeps their bytes (#471); none of its own lines was changed.
  • With a link, the line the body quotes appears verbatim in execute's output, and so does the shape's block of 11 \text eol=lf` lines. The What block equals the split's .gitattributes` byte for byte, at the head and on the merge.

Revert rows I re-ran

Each change was reverted alone at d62f9aa, over the two test files (61 ids). All four match w807's table, ids included.

  • R1, eol_block_paths ignores tracked: 11 failed, 48 passed, 2 skipped. The remedy test passes here, as it should, because main heals.
  • R1a, the tracked path alone (f7c8c328's code): 9 failed, 50 passed, 2 skipped. The failures include test_the_clone_stays_lf_once_the_link_is_replaced[a-link] and [a-chain].
  • R4, _land_the_eol_block with no map: 6 failed, 53 passed, 2 skipped.
  • R7, _appended_lines counts the block's lines as copies: 2 failed (test_execute_verifies_a_block_that_names_the_tracked_path[a-link, a-chain]), 57 passed, 2 skipped.

Gate, CI and Sonar

  • Gate on the merge acea8174, dirty 0, git-filter-repo on PATH: the two touched files, tests/test_shape_doctor.py, tests/test_scaffold_leg_path_collisions.py and tests/test_cli_reference.py. Result: 622 passed, 5 skipped, 1 xfailed, EXIT=0.
  • CI at d62f9aa: tests succeeded (completed 2026-10-18T00:26:10Z), and tests-git-bash, package and SonarCloud all pass. The only annotations are the runner's Node.js 20 warning and the Ubuntu 26 notice. The first head's tests, still pending in my first review, finished green at 19:59:30Z.
  • SonarCloud: Quality Gate passed, 0 new issues, 0 security hotspots, 0.0% duplication on new code.
  • Windows: the new remedy test uses the same fixture, which skips on win32. Its paths (ci, .github, fresh) are NTFS-legal, and it reads bytes only from files git wrote.

The texts

I checked every sentence of the posted body against my runs. The posted body equals the drafted file, 13,285 characters by python len().

One sentence to replace (Why, before the three-state table). The first head named the tracked path alone; the review did not. Replace:

The review of this PR's first head named the tracked path alone, and measured the remedy above in three states:

with:

This PR's first head named the tracked path alone. Its review measured the remedy above in three states:

One nit in code (non-blocking). The Materialized docstring in scripts/shape_materialize.py now has a 118-character line. The file's longest line before was 90. A reflow:

    `eol_block` is the copies named in the block appended to the source's own
    `.gitattributes` (#471), each where git tracks it and, where that
    differs, by its pinned path too (#807), and empty when none was: no
    source file of that name, one the plan did not keep in the root, or one
    that is a symlink.

For the checklist

  1. The tracked line stays after the remedy. Once the link is replaced, the block keeps /ci/workflows/validate.yml text eol=lf. After a move it governs nothing; after a copy it governs the leftover ci/ copy. It is harmless, and update-shape.py never prunes it.
  2. Already in the body's "Not in this PR", confirmed:
    • the plan's follow-up still says "one text eol=lf line … for each pinned copy";
    • a symlinked root .gitattributes gets a follow-up with the pinned line alone, while check rehearses both lines;
    • a target no line can spell drifts silently.
  3. On main too, unchanged here:
    • core.safecrlf=true stops execute at .gitmodules after both legs are pushed;
    • core.symlinks=false passes check while execute refuses adopt-symlinks-off.

Lane 1 applies the Why sentence to the body now. The docstring nit is non-blocking, and is left for the next change to that file. #844 joins lane 1's landing turns.

Lane: openreposhape-1 (openRepoShape-1)

@sonarqubecloud

Copy link
Copy Markdown

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

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

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

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

@brettheap
brettheap merged commit d6475f9 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the fix/807-eol-block-tracked-spelling branch October 18, 2026 03:56
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