Skip to content

scaffold-project.py: a leg path that is, holds or lies inside a path the shape writes into the assembly root, a --pin's contracts/<product>-pin.yaml included, is refused before anything is created, under --dry-run alike, through the one definition adopt-project.py asks, so --spec-path contracts no longer creates three repositories and pushes both legs to die at git submodule add (#197) - #234

Merged
brettheap merged 3 commits into
mainfrom
fix/scaffold-shape-path-collisions
Oct 11, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)
Claim: #197 (comment)

Fixes #197.

What

scaffold-project.py now refuses a leg path that is, holds or lies inside any path the shape writes into the assembly root. This includes each --pin's contracts/<product>-pin.yaml. The refusal is REFUSED scaffold-leg-path-collides, exit 2. It runs before any process starts (no work directory, no remote, no gh, no git), and --dry-run runs it too. It asks the same question adopt-project.py asks (#172, #198), from one definition:

  • f8ef35d moves the shape half of adopt-project.py accepts a leg mount path that collides with a directory the shape writes, such as --spec-path contracts, so the adoption fails after both legs are pushed (main: at the first bootstrap) #172's collision rule out of adopt-project.py into scripts/shape_materialize.py, beside assembly_root_paths: shape_path_collisions, path_overlap, first_and_more, CASE_ASIDE and MOVE_THE_LEG. Adopt imports them back under its old names. Its output is byte-identical over the collision cases (a 44523-byte dump of every case, cmp exit 0), and tests/test_adopt_leg_path_collisions.py is unchanged.
  • aabfae0 calls the rule from _scaffold, straight after the leg paths' checked_value and before _identity, _declaration's gh api, _leg_visibility, the dry-run return and the work root. It also adds tests/test_scaffold_leg_path_collisions.py.
  • 87fd767 (review round 1) closes the --pin hole. neutral_pin_path(product) is now the pin file's only spelling in the tools: the materializer writes the pin there, the plan names it there, and assembly_root_paths(neutral_pins=...) lists those files in the order they are written, so the refusal knows them. The commit also pins two ordering claims with tests that run on every OS: the refusal runs before _identity, and before _declaration, checked in-process. It also corrects one docstring.

The relation words, the case aside and the remediation are adopt's own. A case-only variant (Contracts, PROJECT.yaml) is refused with the case aside on every OS: the comparison is between strings, and on a case-insensitive disk the two are one path anyway.

One ordering change comes with 87fd767: a malformed --pin is now refused by parse_pin's own pin-malformed before _identity, ahead of four refusals that used to come first (scaffold-leg-path-collides, scaffold-no-elector, election-date-malformed, scaffold-bad-id). Its text is byte-identical, and every case still refuses before anything is created.

Non-canonical spellings of a colliding path (contracts/, ./contracts, Contracts/) and Git's own names such as .gitmodules are refused first by #179's canonical-spelling check. That check landed on main as 8df21e6 (#201) and runs before this one in _scaffold. This branch must land on a main that holds 8df21e6, which a5b09c3 does, and on that merge every such spelling is refused before anything is created. aabfae0's commit body predates #201 landing and says they "slip past"; that sentence is stale.

--help is unchanged, so docs/cli.md is too.

Why (repro on main)

On main, --spec-path contracts, --code-path project.yaml, or a leg at a pin's file planned cleanly under --dry-run. The real run then created and pushed all three repositories and died at git submodule add, leaving three real GitHub repositories for a run that made no project. A case-only variant of a pin's file (--spec-path Contracts/OpenGlass-pin.yaml --pin openGlass@…) exited 0 with a gitlink beside the file contracts/openglass-pin.yaml, a project a macOS or Windows disk cannot check out.

Tests

macOS and Windows have not run these tests yet; this PR's CI is the first time.

Landing notes

🤖 Generated with Claude Code

brettheap and others added 3 commits October 11, 2026 16:30
…e moves out of adopt-project.py to sit beside `assembly_root_paths`, so scaffold-project.py can ask the same question from one definition, and adopt's sentences stay byte for byte what they were (#197)

`scaffold-project.py --spec-path contracts` creates all three repositories, pushes both legs and only then dies at `git submodule add`, because the shape has already written `contracts/` into the assembly root (#197). The question that catches it is #172's shape half, which #198 wrote inside `adopt-project.py`, where the scaffold cannot import it. A second copy in the scaffold is how the two tools would start refusing different paths, so this commit moves the code and changes no behaviour.

What moved, with #198's docstrings kept: `CASE_ASIDE`, `MOVE_THE_LEG`, `_overlap` (now `path_overlap`), `_first_and_more` (now `first_and_more`), and the half of `_shape_collisions` that reads `assembly_root_paths` (now `shape_path_collisions`). The functions are public because two modules import them. `shape_path_collisions` takes the `collision_dir` the caller hands `materialize_assembly_root`, so the two callers differ here by the argument they differ by there. Adopt passes its `COLLISION_DIR`, and its sentence, which names `execute` and `shape/`, is produced unchanged.

What stayed in adopt-project.py:
- `_shape_collisions`, which now asks the shared half and then its own question about `shape/`. That half reads #198's `SHAPE_DIR_RESERVED`, built from adopt's `COLLISION_DIR` and also said by `LEG_COLLISION_REMEDIATION`. Only an adoption writes copies there, and moving the sentence would have meant a second spelling of the phrase or moving a constant adopt's remediation reads. So it stays, verbatim.
- `leg_path_collisions`, unchanged.
- The root-kept half (`_kept_collisions`, `_kept_in_root`, `_leg_collision_findings`), since only an adoption reads a source tree.
- `LEG_COLLISION_REMEDIATION`, which speaks of `check` and `shape/`.
The moved names come in through one separate import statement, under the private names `_kept_collisions` and `_shape_collisions` already call. The statement sits after `import shape_advisory`, away from the materializer import list that #198 and #179 each edit.

Checked: for every path `assembly_root_paths()` lists plus 24 other values (the `shape/` variants among them), asked as both legs, plus seven root-kept cases, the remediation and `SHAPE_DIR_RESERVED`, adopt's output is identical before and after (78 lines, `cmp` against #198's head 163173e, whose tree main's squash 20e13f0 holds byte for byte). tests/test_adopt_leg_path_collisions.py is unchanged and passes, with test_adopt_leg_paths.py and test_shape_doctor.py (622 passed).

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the shape writes into the assembly root is refused before anything is created, under `--dry-run` alike, so `--spec-path contracts` no longer creates three repositories and pushes both legs to die at `git submodule add` (#197)

`_build_assembly_root` writes the shape into the root FIRST and mounts the legs after it, so a leg path the shape had just written met only `git submodule add`. Measured on #198's head 163173e, whose tree main holds byte for byte since its squash 20e13f0, against a local remote directory:
- `--spec-path contracts`, `--code-path project.yaml` and `--code-path Makefile/code` each planned under `--dry-run` with exit 0.
- The real run then created all three bare repositories, pushed a commit to each leg, left the assembly root with no commit and exited 2 (`'contracts' already exists and is not a valid git repo`, `destination path ... project.yaml already exists`, `could not create leading directories of ... Makefile/code`).
- `--spec-path Contracts` exited 0 with a gitlink beside the shape's `contracts/`, which a macOS or Windows disk keeps as one path.
Against GitHub, each left three real repositories for a run that made no project.

THE QUESTION IS ADOPT'S (#172), through the one definition the previous commit moved: `shape_path_collisions` over `assembly_root_paths()`, with `path_overlap`'s relation (is, contains, is inside) and `CASE_ASIDE`. A file the shape gains is refused here without anybody editing this tool. The refusal is `scaffold-leg-path-collides`, beside adopt's `adopt-leg-path-collides`. It names each leg's overlap in #198's words, then a remediation that lists the materializer's paths and says nothing was created.

`shape_path_collisions` gains `collision_dir=None`, the scaffold's case. The scaffold hands `materialize_assembly_root` no collision directory, so the sentence says what happens here instead of what adopt's `execute` would do: the shape writes the path into the root before the legs are mounted, and Git cannot mount a leg at a path the root already holds, or beyond a file it holds on the way there. Adopt passes `shape`, and its 78 dumped lines are still byte-identical to 163173e's, which are main's.

WHY `shape` IS NOT REFUSED HERE. Adopt refuses a mount at or under `shape/` because `execute` writes the shape's copy of a file the source already holds there, and #198 reserves the directory for those copies. The scaffold materializes into a directory it made itself, where a collision raises rather than landing under `shape/`. `update-shape.py` reads `shape/` only for a pin that names a path under it, which only an adoption writes. So nothing the scaffold or its project's tools write lands there, and refusing it would be a rule with no collision behind it. `shape` and `shape/spec` scaffold, and a test holds that.

WHERE THE REFUSAL SITS. `_scaffold` calls `_refuse_colliding_leg_paths` straight after the last `checked_value`, before any process can start:
- before `_identity`, which may read git's config;
- before `_declaration`, which asks `gh api` for a `--pin` with no `--pin-source`;
- before `_leg_visibility`, which asks `gh repo view` about a reused root;
- before the plan is printed and the `--dry-run` return;
- before `mkdtemp` or `--work-dir`, the local remote directory and `_create_remotes`.
A dry run therefore refuses with the real run's exit code and its exact stderr. `main()` does nothing but parse arguments before `_scaffold`.

tests/test_scaffold_leg_path_collisions.py (89 tests):
- Every path `assembly_root_paths()` lists is refused as either leg. So are a directory that holds them (`contracts`, `scripts`, `.github`, `.github/workflows`) and a path beyond a file (`Makefile/code`, `contracts/shape-pin.yaml/spec`). The case-only variants say the aside.
- The refused files are exactly what `materialize_assembly_root` writes into an empty directory, and each directory written into is refused too.
- Both tools hold `shape_materialize`'s own objects, and a spy shows both call them, adopt with `shape` and the scaffold with none.
- `--dry-run` and the real run give the same exit code and the same stderr.
- The real run leaves the remote directory's listing unchanged and makes no temporary work directory.
- A stub `gh` records no call before the refusal, against a control that reaches it.
- A non-colliding pair (`project`, `scripts/code`) still scaffolds.
Most cases run `main()` in-process with `subprocess.Popen` refused, so a pass proves no git or gh process started.

Out of scope, left: `--pin` also writes `contracts/<product>-pin.yaml`, which `assembly_root_paths()` does not list (`contracts` is refused through the other files under it, a pin's exact path is not). Non-canonical spellings such as `contracts/` and `./contracts` slip past this check until #179 (#201) lands its canonical check before `checked_value`.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-pin.yaml` a `--pin` writes is refused before anything is created, from the one `neutral_pin_path` the materializer writes it at, so `--pin openGlass@<sha> --spec-path contracts/openglass-pin.yaml` no longer plans and then makes three repositories (#197)

#197's review of aabfae0 (S1) found the one path the materializer writes
that the refusal did not know. `_build_assembly_root` hands
`materialize_assembly_root` the declared pins and `_materialize` writes
each at `contracts/<product lowercased>-pin.yaml`, but
`assembly_root_paths()` described a call with no pin. On aabfae0, and on
main a5b09c3, `--pin openGlass@<sha> --pin-source <clone> --spec-path
contracts/openglass-pin.yaml` planned under `--dry-run` with exit 0, and
the real run created all three repositories, pushed both legs and died
at `git submodule add`. `Contracts/OpenGlass-pin.yaml` exited 0 with a
gitlink beside the pin, one path on a macOS or Windows disk.

ONE DEFINITION. `neutral_pin_path(product)` in scripts/shape_materialize.py
is now the tools' only spelling of that path: `_materialize` writes the
pin there, the scaffold's plan line and `descendant_note` name it from
there, and `assembly_root_paths(neutral_pins=())` lists it between the
copies and `contracts/shape-pin.yaml`, where the materializer writes it.
`shape_path_collisions` takes the same keyword and asks about that list,
and the scaffold passes its `--pin` products. A parameter on the list,
rather than the scaffold appending pin paths to it, keeps "every path
this call writes, in the order it writes them" in the one function beside
the materializer, so the collision set, the remediation's list and the
test that holds the list to what was written read one answer. The
products come from `declared_pin_products`: `parse_pin` alone, the parser
`declared_pin_values` uses, keyed and ordered as its dict is, with no
digest and so no process. Adopt hands the materializer no pin and passes
none, so its sentences are byte for byte what they were: the review's
dump of adopt's collision sentences, re-run on these bytes, has sha256
5eb70cfb684a, the same as aabfae0's, and
tests/test_adopt_leg_path_collisions.py is unchanged.

The refusal runs where it did: after `checked_value`, before `_identity`,
`_declaration`, `_shape_revision`, `_leg_visibility`, the dry-run return,
the work directory, the remotes and `git init`. A malformed `--pin` is
now refused there, by `parse_pin`'s own `pin-malformed`, rather than in
`_declaration`; it was refused before anything was created either way.
The remediation lists the pins' files with the rest. With no `--pin`,
`contracts/openglass-pin.yaml` lies inside a directory the shape writes
into, not a path it writes, and it still plans.

Two ordering claims are now tests that run on every OS (review N3). A
run without `--elected-by`, with GIT_AUTHOR_NAME unset because CI sets
it, refuses with no process started, where its control starts `git
config user.name`. With `_declaration` replaced by a recorder, a
colliding `--pin` run never calls it, where its control does. Adopt's
sentence tail, "`execute` would write it into the mounted leg", is pinned
word for word. The `shape_path_collisions` docstring now says adopt
passes its `COLLISION_DIR`, `shape` with no slash, and that `plan`,
`check` and `execute` all reach it through `leg_path_collisions` (N4).

Mutants, each against tests/test_scaffold_leg_path_collisions.py (104
tests): the scaffold passing no pins, 11 failed; `assembly_root_paths`
dropping the pins, 10 failed; `neutral_pin_path` not lowercasing, 9
failed; the refusal moved after `_identity`, 1 failed, the new
no-elector test alone; moved after `_declaration`, 13 failed, the new
recorder test among them.

Runs on these bytes: test_scaffold_leg_path_collisions,
test_adopt_leg_path_collisions and test_scaffold_pin_and_reuse, 216
passed. On their merge with main a5b09c3, those three plus
test_scaffold_e2e, test_scaffold_ignored_shape_files, test_adopt_leg_paths,
test_shape_doctor, test_cli_reference, test_commit_trailers,
test_adopt_mount_ignored and test_scaffold_leg_paths, 999 passed.

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

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

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

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

@brettheap

Copy link
Copy Markdown
Contributor Author

@codex review

@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Scaffold now rejects any leg path that equals, contains, or lies within an assembly-root path written by the shape—including per-pin contract files—before starting any process or creating anything, with collision logic centralized and shared with adopt-project.py.

Sequence diagram for early scaffold leg-path collision refusal

sequenceDiagram
    participant User
    participant Scaffold as scaffold-project.py
    participant Parser as parse_pin
    participant Rules as shape_materialize.py
    participant System as Processes and filesystem

    User->>Scaffold: _scaffold(args)
    Scaffold->>Scaffold: checked_value(--spec-path, --code-path)
    Scaffold->>Parser: parse_pin(raw, pin_owner)
    Parser-->>Scaffold: declared pin products
    Scaffold->>Rules: shape_path_collisions(--spec-path, value, neutral_pins)
    Rules->>Rules: assembly_root_paths(neutral_pins)
    Rules-->>Scaffold: collision sentences or none
    alt collision found
        Scaffold-->>User: Refusal scaffold-leg-path-collides, exit 2
        Note over System: No process, work root, remote, gh, or git is started
    else no collision
        Scaffold->>System: Continue with _identity and project creation
    end
Loading

File-Level Changes

Change Details Files
Centralize assembly-root collision detection and reuse it across scaffold and adopt flows.
  • Move path-overlap relations, case handling, collision formatting, and remediation constants into the materialization module.
  • Update adoption to import the shared collision helpers while retaining its collision-directory-specific behavior.
  • Add cross-tool identity and output-compatibility coverage.
scripts/shape_materialize.py
adopt-project.py
tests/test_scaffold_leg_path_collisions.py
Reject scaffold leg paths that overlap files or directories written into the assembly root before any external work begins.
  • Validate both leg paths immediately after canonical argument checking and before identity, pin declaration, dry-run handling, or work-directory creation.
  • Refuse equal, containing, nested, and case-only overlaps with exit code 2 and the scaffold collision refusal code.
  • Run the same validation for dry runs and preserve clean planning for non-colliding paths.
scaffold-project.py
tests/test_scaffold_leg_path_collisions.py
Include declared neutral-product pin files in the materializer’s authoritative written-path list.
  • Introduce neutral_pin_path as the single spelling used by materialization, planning, and collision checks.
  • Parameterize assembly_root_paths by declared pin products and include every generated pin file in write order.
  • Parse pin products without reading digests so collisions are detected before git, gh, or other processes run.
scripts/shape_materialize.py
scaffold-project.py
tests/test_scaffold_leg_path_collisions.py
Add comprehensive regression and ordering tests for collision refusal behavior.
  • Cover all materialized paths, parent/child overlaps, case variants, multiple pins, and clear paths.
  • Verify real and dry runs produce identical refusals without creating remotes or work directories.
  • Assert refusal precedes identity and declaration, and exercise local end-to-end and shared-definition behavior.
tests/test_scaffold_leg_path_collisions.py

Assessment against linked issues

Issue Objective Addressed Explanation
#197 Prevent scaffold-project.py from accepting a spec or code leg path that equals, contains, or lies inside any path materialized by the shape. ✅
#197 Reject colliding leg paths before any work directory, remote repository, GitHub/gh command, or Git operation is started, including during --dry-run. ✅
#197 Use the materializer's single assembly_root_paths() definition, including paths generated for declared --pin files, so scaffold and adopt apply the same collision rule. ✅

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

@chatgpt-codex-connector

Copy link
Copy Markdown

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

@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-11T19:43Z) Independent review of #197 for lane openRepoShape-4, as offered.

VERDICT (aabfae0; delta to 87fd767 assessed): APPROVE WITH NITS. Nothing blocking or should-grade is open at 87fd767.

Verified at 87fd767:

Carried from aabfae0 (not re-run):

  • 948 passed, 1 skipped (the MedxEHR clone test).
  • Adopt output is byte-identical to main over 438 files.
  • A 116-case scaffold probe on main, the head and the merged tree, with gh never called.

#235 appears to cover the .gitignore-hidden leg path. I read its body and diff only and did not run its tests.

Confirmed follow-up, not a finding against this PR: a --pin whose file the shape already writes (--pin spec@<sha>, --pin shape@<sha>), or two pins that differ only by case, passes --dry-run. The real run then dies with materialize-collision after three repositories exist. Your PR body already says you will file it.

The full report is in opensoft/brett-wip at handoffs/openRepoShape/attachments/lane-openRepoShape-5/review197-aabfae0-87fd767.md (commit 851ebf6c3). It cites every command and log behind each finding.

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-4, 2026-10-11T20:03:46Z) The follow-up the re-review asked for before this lands is filed, re-verified on main 54aa69a and on this head: #241. A --pin whose product names a shape file (spec@, code@, shape@), or two --pins whose products differ only by case, passes --dry-run, then creates all three repositories and dies in the materializer. A repeated --pin for one product also silently drops the first. This PR's neutral_pin_path and assembly_root_paths(neutral_pins=…) are the hooks a fix for it can use. Nothing in this PR changes it.

brettheap added a commit that referenced this pull request Oct 11, 2026
…ce` changes in git 2.43 (a module store reactivated, a gitlink accepted, an early dry run skipped and its refusal made later) and names the assembly directory `_refuse_existing_trees` checks, and the test module's comments hold before and after #197 lands (#229)

Comments and docstrings only. The AST of `scaffold-project.py` and of
`tests/test_scaffold_mount_ignored.py`, with docstrings removed, is identical
to 9bb8bd0's, and `tests/test_scaffold_mount_ignored.py`,
`tests/test_scaffold_e2e.py` and `tests/test_cli_reference.py` pass as they
did. This answers the four nits of the re-check of 9bb8bd0:

NIT-2. The comment in `_build_assembly_root` and the test module's docstring
said `--force` "turns off three other refusals". Probed one by one on git
2.43.0 in a scratch repository (`GIT_CONFIG_GLOBAL` pointed at a file of its
own, `GIT_CONFIG_NOSYSTEM=1`), two of the three are not turned off:
- A `.git/modules/<path>` already there: exit 128 "A git directory for 'spec'
  is found locally with remote(s)" without `--force`; with it, exit 0
  "Reactivating local git directory", and the gitlink is the store's commit,
  not the leg's. Turned off, as the comment said.
- An entry at the path in the index: without `--force`, exit 128 "'spec'
  already exists in the index" for a gitlink, a regular file and a tracked
  directory alike. With it, a GITLINK is accepted (the leg is cloned and the
  entry is replaced, or, with a repository already checked out there,
  "Adding existing repo at 'spec' to the index" and the entry keeps that
  repository's commit), and a regular file or a tracked directory is still
  refused, as "already exists in the index and is not a submodule".
- git's early dry run of the path, which refuses a path beyond a symlink
  ("pathspec 'link/sub' is beyond a symbolic link") or inside a submodule
  ("Pathspec 'spec/sub' is in submodule 'spec'") before anything is cloned:
  with `--force` it is skipped, and git still exits 128 on both, later. Beyond
  a symlink it first CLONES the leg into the link's target and fails at the
  last step ("Failed to add submodule"); inside a submodule it refuses to
  create a git directory inside the other's. The refusal is moved, not turned
  off.
The comment now says each of these as probed. The conclusion stands: none of
the three can arise in `_build_assembly_root` (the root is `git init`ed with
`--template=`, the index holds only `.gitmodules` and the first gitlink at the
second mount, and the materializer writes only regular files), and the
paragraph on why `--template=` closes the planted-store case is unchanged.

NIT-3. The comment said "The work directory was empty (`_refuse_existing_trees`)".
That function refuses a `<work-dir>/<assembly>` that exists and is not empty,
and says nothing about the work directory, which may hold anything. The
comment now says that, and draws the same conclusion from it: no `.git` was
inside the assembly directory when the run began.

NIT-1. The `SHAPE_PATHS` comment said git refuses those paths "at the mount
(exit 128)". That is true on main, and false once #197 (PR #234) lands, when
they are refused up front as `scaffold-leg-path-collides`. The comment now
says both ("where #197's collision check is in force ... where it is not"),
without claiming which state it is in, and that the tests assert only exit 2
and no gitlink, so they hold in both. It mirrors the comment in
`_build_assembly_root`.

NIT-5. The clone test's docstring quoted `Fetched in submodule path 'spec'`;
under the mutation (`--template=` removed) git names `'code'`, because the
template plants both stores. The docstring now says the path is a leg's, and
names both.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap
brettheap merged commit b538015 into main Oct 11, 2026
5 checks passed
@brettheap
brettheap deleted the fix/scaffold-shape-path-collisions branch October 11, 2026 21:44
brettheap added a commit that referenced this pull request Oct 11, 2026
…) into fix/plan-report-stdout-encoding (#214)

One conflict, resolved by keeping both sides: the `from shape_materialize
import (...)` list in update-shape.py. This branch added
`escape_what_stdout_cannot_encode` and main added `force_stage` (#202) in the
same place. Both names are now in the list, in its existing order
(commit_trailers, escape_what_stdout_cannot_encode, force_stage,
lane_trailer_argument, ...), wrapped to 79 columns like its neighbours, so the
list's tail lines re-flow with it. No other file conflicted, and nothing else
was changed.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 11, 2026
… `opensoft/openGlass` "by default" since `--pin-owner` changes it, its docstrings say "On main through 39ac59c" now that "as on main" is no longer true, and a pin's file is compared with the shape's files case-folded on both sides, as the leg-path check does (#241)

Fix round 1 for #241's review (a25c2e1, MERGEABLE WITH NITS); the merge of
origin/main (S1) is the commit beneath this one, and its changed lines are
exactly #241's own, `git diff 87fd767 a25c2e1`.

N1: `PIN_COLLISION_REMEDIATION` said `--pin openGlass@<40 hex commit>` "names
opensoft/openGlass" as a fixed fact. Under `--pin-owner MedxSoft` it names
MedxSoft/openGlass, so the sentence was false for that run. It now says
"by default", as the neighbouring `pin-malformed` remediation does
("resolved under opensoft/openGlass by default"). The test's `REMEDIATION`
constant, which asserts the message word for word, says the same.

N2: the `_refuse_colliding_pins` docstring and the test module docstring said
"On #197's 87fd767, as on main" (and "On main and on #197's 87fd767"),
which stops being true once this lands: main then refuses what they
describe. They say "On main through 39ac59c" now, the file's own style
(`_refuse_colliding_leg_paths`: "On main through 20e13f0"). The test
docstring of `test_a_dry_run_refuses_exactly_as_the_real_run`, "On main the
dry run said yes", gets the same words, being the same sentence.

N3: `_refuse_colliding_pins` asked `path not in shape_files`, exact, while
#234's `path_overlap` compares the leg paths case-folded for the case-
folding disk of macOS and Windows. Both sides are `casefold()`ed now, with a
comment saying why: a future mixed-case file the shape writes under
`contracts/` would otherwise be passed by a pin whose lowercased path
collides with it only on that disk, and die in `place()`. The change is
EQUIVALENT TODAY and no test is added for it: `neutral_pin_path` lowercases,
and every `contracts/*-pin.yaml` in `assembly_root_paths()` (`spec-pin`,
`code-pin`, `shape-pin`) is lowercase, so the review's mutant that
case-folds this comparison passes every test and no test could tell the
two apart.

N4 and O1-O3 of the review are follow-up issues, not this change.

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 11, 2026
…ng-paths

main moved to 39ac59c: #197 (b538015, PR #234) moved `_overlap` and
`_first_and_more` into scripts/shape_materialize.py as `path_overlap` and
`first_and_more`, and adopt-project.py imports them back under the old
names; 150c41d and 39ac59c (PRs #233 and #235) touch update-shape.py and
scaffold-project.py. One conflict, in adopt-project.py: this branch had
edited `_first_and_more` to `_spelled` its path, and main removed the
function.

Resolved by taking main's side of that one hunk: `_overlap` and
`_first_and_more` are deleted from adopt-project.py, and main's import of
the shared definitions stays as it is. No line is written by hand in this
merge. It leaves `_kept_collisions` naming a source path through
`first_and_more`, unspelled, so the spelling case of
tests/test_adopt_finding_spelling.py fails on this commit alone; the next
commit gives `first_and_more` an optional `spell` and passes `_spelled`.

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 11, 2026
…) into fix/adopt-record-submodule-placement

Brings in b538015 (#197), 150c41d (#202) and 39ac59c (#229) on top of
4835fb4. No rebase. They landed while this branch's targeted run was
going, and the files they change overlap this branch's: adopt-project.py
and scripts/shape_materialize.py.

No conflicts. Checked:

- adopt-project.py: main moves `_overlap` and `_first_and_more` into
  shape_materialize.py as `path_overlap` and `first_and_more`; this
  branch's `submodule_record` and its record append are untouched.
- scripts/shape_materialize.py: main adds `shape_path_collisions`,
  `force_stage` and `neutral_pin_path`, and `assembly_root_paths` takes
  `neutral_pins`. This branch's `_spelling_reason` and
  `LEG_SEGMENT_REASONS`, which ask `repo_shape.path_spelling_fault`, are
  untouched, and no line main added names either, nor
  `leg_path_problem` (`git diff 4835fb4 origin/main`).
- The branch's diff against main is still #189 alone: 12 files, +1670
  -156, the same as against 4835fb4.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…e-notice

It merged with no conflict. Main brings six commits since 54aa69a:

- e7d1720, update-shape.py (#208 via #224);
- 404760f, scripts/repo_shape.py's double-quoted flow items (#213 via
  #244), with tests/test_yaml_flow_quotes.py;
- 4835fb4, adopt-project.py's plan writer escapes (#199 via #247), with
  tests/test_adopt_plan_escapes.py;
- b538015, scaffold-project.py's leg paths against the shape's own
  paths (#197 via #234);
- 150c41d, update-shape.py's `apply --add` (#202 via #233);
- 39ac59c, scaffold-project.py's leg mount `--force` (#229 via #235).

Two of them touch adopt-project.py, and no hunk of either meets this
branch's:

- 4835fb4 changes the plan writer (`y`, `_PLAIN_RE`), adds `_shown` to
  the plan and check reports, and writes the `--paths-from-file` listing
  in `_extract_leg` through `write_paths_listing`. This branch changes
  other lines of `_extract_leg`: it reads the tree digest before the
  push and pushes through `_push_the_leg`. The signature is the same on
  both sides, so main's new direct `_extract_leg` test calls it as
  written.
- b538015 moves `_overlap`, `_first_and_more`, `MOVE_THE_LEG` and
  `CASE_ASIDE` into scripts/shape_materialize.py, and imports the first
  three back under the same names. This branch adds no use of any of
  them. A `symtable` check finds no name that the merged adopt-project.py,
  scaffold-project.py or scripts/shape_materialize.py reads and nothing
  binds.

`git diff -U0` of this merge against 39ac59c equals the branch's against
54aa69a, apart from the hunk headers. On the merged tree,
tests/test_adopt_plan_escapes.py, tests/test_yaml_flow_quotes.py,
tests/test_scaffold_leg_path_collisions.py and
tests/test_adopt_leg_path_collisions.py give 275 passed.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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