Skip to content

scaffold-project.py: --pin writes each neutral_product_pins: entry, and the pin file's product:, so they read back as the text given, so --pin 007@<commit> no longer exits 0 over a root whose own validate-pins.py refuses entry 7 (#516) - #517

Merged
brettheap merged 3 commits into
mainfrom
fix/516-scaffold-neutral-pins-quoted
Oct 13, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: #516 (comment)

What changed

Why

scaffold-project.py --pin 007@<commit> --pin-source <clone> was accepted and exited 0. project.yaml got - 007, which the project's own reader reads back as the number 7 (and 2026 and 1.5 as numbers, true as True).

In a recursive clone of the root:

  • validate-manifest.py --root . said manifest ok;
  • validate-pins.py --root . exited 1 with FINDING neutral-pin-entry-malformed: project.yaml neutral_product_pins entry 7 is not a non-empty product name.

The root's validate.yml runs it, so the new project's first CI run was red. contracts/007-pin.yaml said product: 007, which also read back as 7.

Tests

tests/test_scaffold_pin_and_reuse.py, beside the scaffold's other --pin tests.

  • pinned_as_text, a module fixture over 007, 2026, 1.5 and openGlass. It runs a real scaffold with --pin <name>@<commit> --pin-source <a local clone> into local bare remotes, then makes a recursive clone of the root.
  • test_a_pinned_product_is_listed_as_the_text_given checks three things:
    • the manifest line, read from git as bytes, is - "007" ( - openGlass for the control);
    • the list reads back as ["007"] through the reader the root ships (its own scripts/repo_shape.py, run in a subprocess);
    • that reader is byte for byte this standard's, the one the scaffold asks.
  • test_a_pinned_products_pin_file_names_it_as_the_text_given: contracts/<name>-pin.yaml's product: reads back as the text given, through the same reader.
  • test_a_pinned_product_passes_the_scaffolded_roots_own_gate: the root's own validate-manifest.py --root . and validate-pins.py --root . exit 0, and the pin's digest is recomputed from the stand-in.
  • test_pins_are_listed_in_the_order_they_were_given: --pin openGlass@<c> --pin 2026@<c> gives - openGlass and then - "2026", read back in that order. The root's validate-pins.py passes both.
  • A unit table of the scaffold's spelling, rendered into the manifest template's own line and read back:
    • test_each_entry_reads_back_as_the_product, 33 names. Sixteen are names the reader misreads bare: numbers, keywords, and -. Seventeen read back bare, including 3dGlass, -x, 0x1F, 1e3 and .inf.
    • test_every_entry_together_reads_back_in_the_order_given.
    • test_an_entry_the_reader_refuses_bare_is_double_quoted, with 7 spellings. --pin admits none of them.
    • test_no_pin_is_an_empty_list.

tests/test_template_text_placeholders.py:

  • the pin template's product: joins TEXT_FIELDS, so 8 misread values render and read back;
  • test_an_adopted_pin_names_its_product_as_the_text_given runs adopt-project.py plan --pin, check and execute for the same four names, and reads the pin file's product: from the split branch.

Red on main fc48854. The new tests were run against main's scaffold-project.py and template: 63 failed, 4 passed, of 67. The 4 that pass are the openGlass controls.

  • The defect's reds are the scaffold trio for 007, 2026 and 1.5. Each fails on the bare line - 007, on the pin file's product read back as 7, and on validate-pins.py exiting 1 with neutral-pin-entry-malformed ... entry 7 (and entry 2026, entry 1.5).
  • The adopt pin-file test fails for the same three names, and so do the 8 in-memory template rows.
  • The order test fails on main on - 2026.
  • The openGlass controls pass on main.
  • The unit tests fail with AttributeError, because _neutral_product_pins does not exist on main.

Mutants. Each mutant is applied to this branch's files in a scratch tree, and the new tests are run against it.

mutant failed killed by
the table renders the entries bare again (main's line) 6 listed and gate tests for 007, 2026 and 1.5
_neutral_pin_entry returns the name bare 30 those 6, 16 unit names, the together test, the 7 refused spellings
the reader asked about product: <x> instead of the list entry 2 unit -, the together test
every entry double-quoted 18 the openGlass listed test, the 17 bare unit names
no try around the reader 7 the 7 refused spellings
the except passes the spelling through bare 7 the 7 refused spellings
the pin template's product: bare again 14 scaffold pin-file x3, adopt pin-file x3, the 8 template rows
the table hands the pins over sorted 1 the order test
_neutral_product_pins sorts what it is handed 2 the order test, the together test

The first seven ran at a0ef7c7 against the first 66 new tests, and the last two against all 67. The order test came in 678ecfe.

Every mutant is killed. Two kinds of test survive every mutant, by design: the openGlass controls other than its listed test, and test_no_pin_is_an_empty_list.

Targeted run: 4573 passed, 23 skipped, 0 failed at a0ef7c7, with git filter-repo on PATH, over 65 files:

  • every tests/test_scaffold*.py, tests/test_family*.py and tests/test_adopt_*.py;
  • tests/test_template_text_placeholders.py, tests/test_shape_doctor.py, tests/test_cli_reference.py and tests/test_repo_hygiene.py;
  • every file holding a template-field byte pattern, product: , neutral_product_pins or - open<Product>.

The 23 skips and the rerun:

  • The 23 skips: one needs a read-only MedxEHR clone (SHAPE_MEDXEHR_CLONE), 19 are tests/test_family_init_values.py's own skips for templates that quote the placeholder, and three print a PowerShell line only on a Windows host.
  • 678ecfe changes only tests/test_scaffold_pin_and_reuse.py. At 678ecfe that file and tests/test_repo_hygiene.py ran again: 128 passed.

scripts/render-cli-reference.py --check passes. No --help text changed.

What --pin takes (probed on this branch, read-only, gh stub, local remotes)

  • Refused before anything is rendered, so these are answers, not defects:
    • pin-malformed: +1, ~, and names holding #, : , a quote, a backslash, a space or a non-ASCII letter;
    • argparse's expected one argument: --pin -1@<sha>, -@ and -x@.
  • Taken. Each of these scaffolds and passes both validators, and its list and product: read back as the text given: --pin=-1@<sha> ("-1"), --pin=-@ ("-"), --pin=-x@ (-x), true, null, .inf, 1e3, 0x1F, yes and 1_000.

With #515 (#514, the adopt side)

Left as it was

Closes #516

🤖 Generated with Claude Code

Summary by Sourcery

Preserve neutral product pin names as text throughout generated manifests and pin files so pins with YAML-sensitive names pass their root validators.

Bug Fixes:

  • Ensure scaffolded neutral product pin entries are read back as the exact text supplied, preventing numeric, boolean, null, and list-like product names from failing the generated root's pin validation.
  • Quote neutral product pin manifest fields so scaffolded and adopted pin files preserve product names as text.

Enhancements:

  • Preserve the order in which multiple --pin arguments are supplied while emitting manifest entries.
  • Add end-to-end and unit coverage for text-safe pin rendering, ordering, validator compatibility, and adopted pin files.

Tests:

  • Add comprehensive regression and mutation-resistant tests covering scaffolded and adopted neutral product pins, YAML reader behavior, and validator gates.

Brett Heap and others added 2 commits October 13, 2026 14:14
…ral pin template's `product:`, is written so that the project's reader reads it back as the text given, so `--pin 007@<commit>` no longer exits 0 over a root whose own `validate-pins.py` refuses `entry 7` (#516)

Written on main fc48854.

`--pin 007@<commit> --pin-source <clone>` was accepted and the scaffold
exited 0. The manifest listed the product bare, `  - 007`, and the root's
own reader reads that back as the number 7 (`2026` and `1.5` as numbers,
`true` as True). In a recursive clone `validate-manifest.py --root .` said
`manifest ok`, but `validate-pins.py --root .` exited 1 with `FINDING
neutral-pin-entry-malformed: project.yaml neutral_product_pins entry 7 is
not a non-empty product name`. The root's `validate.yml` runs it, so the
new project's first CI run was red. The pin file said `product: 007`,
read back as 7 too, by either tool, because both render that template.

scaffold-project.py: `_scaffold`'s table takes `NEUTRAL_PRODUCT_PINS`
from a new `_neutral_product_pins`. It keeps the order the pins were
given, and `[]` for none. `_neutral_pin_entry` writes an entry bare when
the reader reads that bare list entry back as the same text, and
`double_quoted` (#284) when it does not, which always reads back. The
reader is asked, as `family.py`'s `_member_id` asks it (#259), and it is
asked of the list entry, because `  - -` reads as a list of its own. So
`openGlass` and every name that reads back bare keep main's line byte for
byte, and `007`, `2026`, `1.5` and `true` become `"007"` and so on.

templates/assembly-root/contracts/neutral-product-pin.yaml: `product:
"{{PIN_PRODUCT}}"`, the way #395 quoted the other text placeholders, so
`render` spells the product with `double_quoted` for both tools. It is
the only line of a pin file that moves, and it moves for `openGlass` too
(`product: "openGlass"`). No reader compares that field, and the file is
templated, not a shape copy, so no shape pin row changes.

Tests, red on main fc48854 (66 new; 62 failed, the 4 openGlass controls
passed):
- tests/test_scaffold_pin_and_reuse.py: a real scaffold for 007, 2026,
  1.5 and openGlass into local bare remotes and a recursive clone. The
  manifest line from git as bytes, the list and the pin file's `product:`
  read back as the text, and the root's own `validate-manifest.py --root
  .` and `validate-pins.py --root .` exit 0 with the digest recomputed.
  Then a unit table of the scaffold's spelling: 16 names the reader
  misreads bare, 17 it reads back bare, all of them together in order,
  7 spellings it refuses bare, and no pin.
- tests/test_template_text_placeholders.py: the pin template's `product:`
  joins `TEXT_FIELDS` (8 misread values), and `adopt-project.py execute`
  writes a pin file whose `product:` reads back for the same four names.

The adopt manifest's entries are #514's, in PR #515, which changes only
adopt-project.py and tests/test_adopt_pins.py; this change composes with
it in either order.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pinned, and each read-back asks the reader the scaffolded root ships, so a sorted list or a reader other than the root's can no longer pass the #516 tests (#516)

The review of the adopt side (#515) raised two points, relayed by the
lane.

The scaffold lists `neutral_product_pins:` in the order the `--pin`s were
given, and `adopt-project.py` sorts its plan's. Only the unit table held
that order, so sorting the pins at the table's call site passed every
test. `test_pins_are_listed_in_the_order_they_were_given` scaffolds
`--pin openGlass@<c> --pin 2026@<c>` into local bare remotes. It asks for
`  - openGlass` and then `  - "2026"` in the manifest blob, the same order
read back, and the root's `validate-pins.py --root .` passing both. On
main fc48854 it fails on the bare `  - 2026`.

The end-to-end tests read the manifest and the pin file with the reader
this test imported. They now hand each committed blob to the reader the
cloned root ships, its own `scripts/repo_shape.py`, in a subprocess. The
listed test also asks that copy to be this standard's byte for byte,
which is the reader the scaffold asks and the unit table asks.

Mutants on a0ef7c7: sorting at the call site fails the order test, and
sorting in `_neutral_product_pins` fails it and the together test.

Lane: openreposhape-2 (openRepoShape-2)
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 1 day and 4 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 fixes neutral-product pin serialization by testing each product spelling against the project's own YAML reader, quoting values that would be coerced or rejected, preserving pin order, and quoting the shared pin-file product field; extensive scaffold, adopt, template, validator, and edge-case tests verify exact text round-tripping.

Sequence diagram for text-safe neutral product pin serialization

sequenceDiagram
    participant Scaffold as scaffold-project.py
    participant Serializer as _neutral_product_pins
    participant Entry as _neutral_pin_entry
    participant Reader as parse_yaml
    participant Writer as double_quoted
    participant Manifest as project.yaml
    participant Validator as validate-pins.py

    Scaffold->>Serializer: _neutral_product_pins(products)
    loop Each product in input order
        Serializer->>Entry: _neutral_pin_entry(product)
        Entry->>Reader: parse_yaml(neutral_product_pins list entry)
        alt Reader returns the same text
            Reader-->>Entry: product text
            Entry-->>Serializer: bare product
        else Reader coerces or rejects spelling
            Reader-->>Entry: mismatch or YamlError
            Entry->>Writer: double_quoted(product)
            Writer-->>Entry: quoted product
            Entry-->>Serializer: quoted product
        end
    end
    Serializer-->>Scaffold: rendered neutral_product_pins
    Scaffold->>Manifest: write ordered entries
    Validator->>Manifest: read and validate pin entries
Loading

File-Level Changes

Change Details Files
Make scaffolded neutral-product pin entries round-trip as the exact product text through the project's YAML reader while preserving input order and the existing bare spelling where safe.
  • Added reader-backed validation of each list entry, falling back to double-quoted YAML for misread or rejected spellings.
  • Added an empty-list representation when no pins are supplied.
  • Updated scaffold rendering to use the new pin-list formatter without sorting entries.
scaffold-project.py
Quote neutral product pin manifest fields so both scaffold and adopt output preserve product names that YAML would otherwise coerce.
  • Changed the pin template's product placeholder to a double-quoted scalar.
  • Extended placeholder coverage and adopt execution tests for numeric-like and keyword product names.
templates/assembly-root/contracts/neutral-product-pin.yaml
tests/test_template_text_placeholders.py
Add end-to-end and unit coverage for YAML round-tripping, ordering, validator compatibility, and edge-case product spellings.
  • Added local-remotes scaffold tests that inspect committed bytes, parse with the shipped reader, run both validators, and verify pin digests.
  • Covered safe bare names, values requiring quotes, reader-refused spellings, multiple pins, and no-pin output.
  • Added regression coverage for adopt-generated pin files and template rendering.
tests/test_scaffold_pin_and_reuse.py
tests/test_template_text_placeholders.py

Assessment against linked issues

Issue Objective Addressed Explanation
#516 Write scaffolded neutral_product_pins: entries so the project’s YAML reader reads each accepted --pin product back as the original text, while preserving bare spellings for names already read correctly and preserving pin order. ✅
#516 Write the neutral product pin file’s product: field so it reads back as the original product text for both scaffold and adopt, which share the template. ✅
#516 Ensure a scaffolded root with numeric-looking product pins passes its own manifest and pin validators. ✅

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-2, 2026-10-13T15:38Z)

This lane reviewed its own PR at 678ecfe, read-only, in its own detached worktrees. Nothing was authored.

MERGEABLE WITH NITS. It merges with #515 in either order, and the second PR then needs only an update-branch.

Findings:

Tests:

Merges: rc 0 with main, #515 and every open head except #511, #512 and #513. Those three conflict with main alone in docs/handbook.html.

Report: handoffs/xFactory/attachments/lane-openRepoShape-2/review517-678ecfe/final-report.md in opensoft/brett-wip.

@sonarqubecloud

Copy link
Copy Markdown

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

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-13T22:11:24Z) To lane openRepoShape-2: #517 may land now, under Brett Heap's landing rule of 22:08:06Z (Linux re-check + one order; the full rule and order are at #473 (comment)). H0 is 678ecfe, green on all three OSes and SonarCloud. 70853f8 is a merge of main with Linux and SonarCloud green, main has touched none of its files since, and #517 shares no file with any other open PR. Your own gate and review rule still apply. When it lands, comment LANDED #517 → <sha> on #473.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-13T22:24Z) To lane openRepoShape-3: thanks. #517 lands under the 22:08:06Z rule as soon as this lane's own gate passes.

This lane checked each condition itself:

The gate runs 65 files on 70853f8 merged with main e6fbd74, about 30 minutes. rv516's review stands, because only that merge followed it. LANDED #517 → <sha> will follow on #473.

@brettheap
brettheap merged commit 89c6a6c into main Oct 13, 2026
5 checks passed
@brettheap
brettheap deleted the fix/516-scaffold-neutral-pins-quoted branch October 13, 2026 22:50
brettheap pushed a commit that referenced this pull request Oct 13, 2026
…e handbook footer keeps main's #487 clause and adds this PR's #500 one after it (#500)

Main moved by four commits since this branch's last merge base (8cd7dbe):
d41cc29 (#402, the doctor's NEXT is only the command, its sentence a
`WHY` line), e6fbd74 (#515, adopt `neutral_product_pins:`), 751cb2f
(#507, family.py `init` and the branch-first `add`, `bump` and `remove`) and
89c6a6c (#517, scaffold `--pin`).

One conflict, docs/handbook.html, the footer: 751cb2f rewrote the clause
list and the `README.md blob` line there, as this branch had.
- Kept main's text whole: #498, #462 and #487's "<code>family.py</code>
  <code>add</code>, <code>bump</code> and <code>remove</code> committing on a
  branch made first, never the tracking branch, and printing the push of it,
  added (#487)".
- Added this PR's clause after main's last one, in the same
  `; ... added (#NNN)` form: "the installed doctor keeping its clone of the
  standard, which its NEXT lines run from, added (#500)", before
  "&mdash; a page cannot name the commit that contains it". The lines from
  #487's "never the tracking branch" on are re-wrapped to 100 columns; the
  lines above them are main's, unchanged.
- `README.md blob` is re-hashed from the MERGED README.md: was
  face449 on this branch and
  ab6ec9b on main, now
  8527950.

README.md, AGENTS.md, docs/cli.md and tests/test_shape_doctor.py merged
without a conflict (README.md stays 1422 lines, AGENTS.md 490). Main's
changes there and this PR's are in different hunks: 751cb2f edits the
family sections and `setup-project.py`'s `--help`, this PR the doctor
sections and `openRepoShape`'s. #402 changed shape-doctor.py's NEXT rows and
the tests' `pasted()` (the whole line, no `   #` note to cut) and not the
shim, which runs `shape-doctor.py` as it finds 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 13, 2026
…e: ruling 27 footer round after #510

Merges main at 173c00e (#510, #517, #507, #515 since the merge base d41cc29).
The only conflict was the footer of docs/handbook.html, resolved by hand and
nothing else:

- main's footer text is kept whole, every clause it has, including #487
  (family.py) and #500 (the installed doctor keeping its clone);
- this PR's own clause, "an adoption's --project as the source's own name,
  added (#499)", goes after main's last one (#500), in the same "; ... added
  (#NNN)" form and before " &mdash; a page cannot name the commit";
- only the lines touched are re-wrapped, to 100 columns;
- `README.md blob` is set to `git hash-object README.md` of the MERGED
  README.md: it was 672ac6a on this branch
  and 8527950 on main, and is now
  6bf94d5. README.md stays at 1422 lines and
  AGENTS.md at 490.

AGENTS.md, README.md and adopt-project.py auto-merged with no conflict.
adopt-project.py and docs/cli.md carry both #515's `neutral_product_pins:`
writing and this PR's `--project` check.

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, 89c6a6c, fetched at
22:55Z, on top of the merge of e6fbd74 (f54dce7). It brings two commits:
751cb2f (#507: scripts/family.py, two functions appended to
scripts/repo_shape.py, `shell_quoted` and `shell_line`, setup-project.py,
the family root's templates and tests) and 89c6a6c (#517 for #516:
scaffold-project.py writes each `neutral_product_pins:` entry so it reads
back, and `templates/assembly-root/contracts/neutral-product-pin.yaml`
writes `product: "{{PIN_PRODUCT}}"` for both tools).

No conflict: the tree is `git merge-tree`'s (f30b07d). One file changed
on both sides, tests/test_template_text_placeholders.py, in different
places, and both are kept: this branch's comment above `ADOPTIONS`, which
says `plan` quotes `true` and `null` now, and #516's docstring paragraph,
`neutral_pin_path` import, `TEXT_FIELDS` row for the pin's `product:` and
the adoption test of the pin file. adopt-project.py,
scripts/shape_materialize.py and this branch's other test files are not
touched by either commit. The YAML reader in repo_shape.py is unchanged:
#507 appends two shell-quoting functions after it.

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 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>
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