Repository navigation
scaffold-project.py: the leg mount passes --force to git submodule add, so a leg path an ignore rule matches (__pycache__, .DS_Store, or the default spec under an operator's own excludes) scaffolds instead of dying at the mount after all three repositories exist and both legs are pushed (#229) - #235
Conversation
… add`, so a leg path an ignore rule matches (`__pycache__`, `.DS_Store`, or the default `spec` under an operator's own excludes) scaffolds instead of dying at the mount after all three repositories exist and both legs are pushed (#229) `_build_assembly_root` passes `--force` to `git submodule add`. The root is a fresh `git init` the materializer has just filled, so two sets of rules reach the mount path: the shape's own `.gitignore` (`__pycache__/`, `*.py[cod]`, `.venv/`, `.DS_Store`), copied into the root, and the operator's own excludes (a global `core.excludesFile`). A leg path either one matches was refused at the mount, which runs after `_create_remotes` and `_seed_legs` have made all three repositories and pushed both legs: the run exited 2 with three repositories and no project, `--dry-run` (which asks no ignore rule anything) had planned it, and a corrected re-run met `scaffold-remote-exists`. The DEFAULT paths failed for an operator whose own excludes name `spec` or `code`, so no spelling of the invocation avoided it. A gitlink once added is tracked, so no rule touches it again. It is #174 (adopt, fixed by #200) on the scaffold side. `--force` also turns off two other refusals of git's, which adopt's `_refuse_an_unmountable_path` makes itself before the add. The scaffold needs no such guard, and each claim was checked rather than assumed: - The index holds only the first mount's gitlink (and the `.gitmodules` it wrote) when the second add runs, and `.git/modules` only the first's: the materialized files are staged after both mounts. Logged before each add in a real run, for the default paths and for `legs/spec` with `impl/code`. And `refuse_bad_leg_paths` (#179) lets through no pair that is equal or nested, whatever the case: none of the 21,856 pairs it accepted, out of 24,025 pairs of 155 paths. It is the guard that matters, because with `--force` a second add at the SAME path exits 0 ("Adding existing repo") and the code leg is never mounted. - Nothing the materializer writes is a symlink, so no mount can lie beyond one: none in `templates/`, none tracked, every file written through `write_lf`, none in the root before the first mount of a real run. A mount inside the other leg is the nesting just refused. - A path the SHAPE wrote (`contracts`, `project.yaml`) is #197's, and `--force` changes nothing for it. Of 54 leg paths tried on the real pre-mount root with and without `--force`, 49 behave the same both ways (47 are a file or directory the shape writes, a path beneath one, or a case twin of one, and of those 40 exit 128 both ways and 7 mount both ways); only the 5 that an ignore rule matches differ. tests/test_scaffold_mount_ignored.py runs the real scaffold into `--local-remote-dir`, with a `gh` on PATH that exits 97 if called. Default paths under global excludes naming `code` and `spec`; `--code-path` over `__pycache__`, `legs/__pycache__` (a directory-only rule, which git refuses after the clone) and `legs.pyc` and `.DS_Store` (file rules, refused before it); `--spec-path __pycache__`, the first mount; and `src` as the control. Each case asks git that the rule really matches, then asserts exit 0, a gitlink at each leg path in the pushed root's HEAD at the tip its leg was pushed at (`git ls-tree` on the bare remote, not the exit code), `.gitmodules` naming exactly the two canonical remotes, and a recursive clone that checks out both legs and passes `validate-manifest.py` and `validate-pins.py`. `--code-path README.md`, `scripts` and `contracts` still exit 2 with no gitlink there; the message is not asserted, so the test holds before and after #197. Dropping `--force` fails 24 of the module's 38 tests: all four checks of each of the six cases that an ignore rule matches, and none of the control's or of the three refusals'. The targeted set, this module and five existing files, passes on this head: 157 passed, 0 failed, 0 skipped (tests/test_scaffold_mount_ignored.py, test_scaffold_ignored_shape_files.py, test_scaffold_leg_paths.py, test_scaffold_e2e.py, test_scaffold_pin_and_reuse.py, test_cli_reference.py). `docs/cli.md` is unchanged, since no `--help` text is. `git merge-tree` against fix/scaffold-shape-path-collisions (#197) and fix/decode-git-output-utf8 is clean, and the new module with #197's tests merged into it passes (169 passed). Closes #229 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
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 |
|
@codex review |
Reviewer's GuideThe PR fixes scaffolding failures caused by Git ignore rules matching leg mount paths by passing Sequence diagram for forced leg mounting during scaffoldingsequenceDiagram
participant Operator
participant Scaffold as scaffold-project.py
participant Git
participant Remote as Git remotes
Operator->>Scaffold: scaffold project
Scaffold->>Remote: _create_remotes
Scaffold->>Remote: _seed_legs
loop spec and code legs
Scaffold->>Git: git submodule add --force <leg> <path>
Git-->>Scaffold: gitlink added despite ignore rules
Scaffold->>Git: git config -f .gitmodules submodule.<path>.url
end
Scaffold->>Git: stage and commit assembly
Scaffold->>Remote: push project
Scaffold-->>Operator: successful scaffold
Flow diagram for ignored and invalid scaffold leg pathsflowchart TD
A[Build assembly root] --> B{Leg path matches an ignore rule?}
B -- Yes --> C[git submodule add --force]
B -- No --> D[git submodule add]
C --> E[Tracked gitlink at leg path]
D --> E
E --> F{Path already written by shape?}
F -- Yes --> G[Git refuses; existing validation remains]
F -- No --> H[Both legs mounted and scaffold completes]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…te=`, so an operator's `init.templateDir` holding `modules/spec` no longer plants a module store that the leg mount's `--force` reactivates, which pushed a root whose gitlinks were a stranger's commit and whose recursive clone failed (#229) The review of 5878305 found a third refusal of git's that `--force` on the leg mount turns off and that the commit body did not name: a `.git/modules/<path>` already there is REACTIVATED ("Reactivating local git directory") instead of refused. The assembly root was a fresh `git init`, and `git init` copies the operator's template (`init.templateDir`, or `GIT_TEMPLATE_DIR`) into `.git`, so a template that holds a `modules/spec` store (one made by copying a working Triad root's `.git` does) planted one. Reproduced on 5878305: the scaffold exited 0, the pushed root's `spec` and `code` gitlinks were the planted store's commit and neither leg's tip, and `git clone --recurse-submodules` of it exited 128 (`Fetched in submodule path 'code', but it did not contain ...`). On main, without `--force`, the same input exits 2 at the mount, so 5878305 had turned a loud refusal into a quiet wrong success. The cure is `git init -q --template= -b <tracking branch>`: an empty template is git's documented way to copy none. The assembly root is a temporary build directory the tool fills and pushes, so no template content belongs in it, and whoever clones the pushed root still gets their own template from `git clone`. Leaving the default template out also leaves `hooks/*.sample`, `info/exclude` and `description` out of the root's `.git`; nothing in the scaffold reads them, and the targeted set below, which clones the root and runs its bootstrap, passes. The other `git init` calls the scaffold runs never reach a `--force`, so they are left as they were: scaffold-project.py:708 is `git init --bare` for the three local remotes, which are only pushed to and cloned from; `git_init_commit` (scripts/shape_materialize.py:559, called from `_seed_legs` at scaffold-project.py:758) seeds each leg's working tree, which is the SOURCE of the mount and is never mounted into; and the work root is a `mkdir`. The one `--force` is the submodule add in `_build_assembly_root`, and `_refuse_existing_trees` (scaffold-project.py:675) has made the assembly directory empty before its `git init`, so `.git/modules` holds nothing before the first mount and only that mount's store at the second. The comment in `_build_assembly_root` is corrected where the review found it wrong or stale. It named two refusals `--force` turns off and said neither can arise; it now names three, and says why `--template=` closes the new one. "The index holds only the first mount" was incomplete: when the second mount runs the index holds `.gitmodules` and the first gitlink, and no leg path can be `.gitmodules`, which `refuse_bad_leg_paths` refuses (#201). And "a path the SHAPE wrote ... git refuses it after the clone" was wrong about where git refuses (at the mount, exit 128: `already exists and is not a valid git repo` for `scripts` and `contracts`, `destination path ... already exists and is not an empty directory` for `README.md` and `project.yaml`) and would go stale the day #197 refuses those paths up front. It now says a leg AT a path the shape wrote is a collision of another kind, refused up front by the collision check where there is one and by git at the mount where there is not, and that `--force` changes neither, which holds before and after #197 lands. The same wording is corrected in the test module. A correction to 5878305's body, which cannot be amended: it said every file the materializer writes goes through `write_lf`. `copy_out` (scripts/shape_materialize.py:1183-1186) writes byte copies with `shutil.copyfile`. The conclusion stands: `copyfile` follows a source link and writes a regular file, `find templates -type l` is empty and no tracked entry has mode 120000, so the materializer makes no symlink for a mount to lie beyond. tests/test_scaffold_mount_ignored.py gains four tests over ONE more real scaffold, run as an operator whose `init.templateDir` (set through the test's own `GIT_CONFIG_GLOBAL`, as the excludes are) holds a non-bare store at `modules/spec` and at `modules/code`, each with a commit of its own and an `origin` (a store with none dies at `git remote set-url origin`, loudly, which is not the defect). One asks git that a plain `git init` under that configuration plants the store and `--template=` plants none, so the rest cannot quietly stop running the defect. One asserts exit 0 and no "A git directory for". One asserts the pushed root's `spec` and `code` gitlinks EQUAL the tips the legs were pushed at and are not the planted commit. One clones the root recursively with the template config OFF, so the clone itself is clean, and asserts it succeeds and passes the clone's `validate-manifest.py`, `validate-pins.py` and (review N5) `bootstrap.py`. The clone check is now one helper shared with the ignore-rule cases, so `bootstrap.py` runs in all eight clones, about 0.5 s each. Mutation: with `--template=` removed from the `git init` and nothing else changed, the module fails 2 of its 42 tests, the gitlink test (`{'spec': <planted>, 'code': <planted>} != {'spec': <spec tip>, 'code': <code tip>}`) and the clone test (`Fetched in submodule path 'code', but it did not contain <planted>`), and passes the other 40, the premise test and the exit test among them. Against 5878305 itself the same two fail. The targeted set, this module and five existing files, passes on this commit: 161 passed, 0 failed, 0 skipped (tests/test_scaffold_mount_ignored.py, test_scaffold_ignored_shape_files.py, test_scaffold_leg_paths.py, test_scaffold_e2e.py, test_scaffold_pin_and_reuse.py, test_cli_reference.py). `docs/cli.md` is unchanged, since no `--help` text is. Closes #229 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-4, 2026-10-11T19:57:17Z) Review round 1 is in: 9bb8bd0, one commit on 5878305, answering the adversarial review's MUST and its comment nits.
The targeted set (mount-ignored, ignored-files, leg-paths, e2e, pin-and-reuse, cli) gave 161 passed. The same planted-store input also defeats |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
(lane openRepoShape-4, 2026-10-11T20:03:46Z) Filed, re-verified on |
…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>
|
(lane openRepoShape-4, 2026-10-11T20:19:09Z) A re-check of 9bb8bd0 found it MERGEABLE WITH NITS. MUST-1 is closed: with the planted store, the gitlink equals the leg's pushed tip, the clean clone, the validators and bootstrap pass, and the doctor says COMPLIANT. On the merge with 7779e3d answers its comment nits, comments and docstrings only. The code's AST, with docstrings stripped, is identical.
|
|
You have reached your Codex usage limits. You can see your limits in the Codex usage dashboard. |
|
…) 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>
…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>
…) 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>
…the last that is not a neutral product's name is refused before anything is created, under `--dry-run` alike, so `openXdox,a #b,openDox` no longer creates three repositories and pushes a manifest the project's own `validate-manifest.py` refuses as `yaml-unparsable` (#251) (#271) `scaffold-project.py --referent-chain` now refuses, as `chain-link-malformed`, any link between the first and the last whose name the naming policy does not classify as `neutral-product` (`open<Product>`). The refusal names every such link, says nothing was created, and gives the spelling to use. It runs before `gh api` or git reads a pin's tree, and under `--dry-run` alike. (With no `--elected-by` and no `GIT_AUTHOR_NAME`, resolving the elector runs `git config user.name` first.) - `scaffold-project.py`: new `_refuse_malformed_chain_links(policy, chain)`, called in `_scaffold` right after the policy is loaded and before `_declaration`. That is before `gh api` or git reads a pin's tree, before the `--dry-run` return, and before the work directory and the remotes exist. A link is judged at its basename, the way `resolve_referent` reads it and `naming_block` records it. New `referent_chain_links(raw)` is the one place the argument is read, used by the check and by `_declaration`, so the links checked are the links recorded. `--help` for `--referent-chain` says the rule. The first and last links are unchanged. The refusal quotes the chain and each bad link with `repr`, as `--pin` and `--id` do in their refusals, so a link holding a newline, a carriage return or an ESC byte prints as `\n`, `\r` or `\x1b` and cannot start a second line that reads as a refusal. - `docs/cli.md`: regenerated with `scripts/render-cli-reference.py` (one help paragraph). - `tests/test_scaffold_referent_chain_links.py`: new, 44 tests (30 in the first commit, 14 in fix round 1). **A behaviour change for input that used to work.** A middle link that is not of the form `open<Product>`, with an uppercase letter after `open` (or after `openx`), is now refused. That includes a case variant such as `openydox` or `OPENYDOX`, which main accepted: `openXdox,openydox,openDox` scaffolded on main (the review ran it, against a layer where `openXdox` declares `openYdox`, and `validate-manifest.py`, `validate-repository-naming.py` and `validate-pins.py` all exited 0), because a chain link is compared casefolded elsewhere. The reason is that nothing else anchors a middle link: the run compares it with no pin and no referent, so only its form can say what it is, and the policy's uppercase rule is what keeps `opensoft` (the organisation login) from reading as a product. The refusal names the link and the spelling to use. The first and last links are still compared casefolded, as before, because each is anchored to a name this run holds. `naming_block` writes the chain as a plain flow sequence, `referent_chain: [openXdox, openDox]`, and nothing checked a middle link. `_chain_shape_problem` checks the chain's length, repeats, the first link and the last. A link whose tree is not on the disk is declared-unverified, which is only a warning. On 150c41d, scaffolding `codexDox` (pin `openXdox`, `--pin-source` a layer declaring `openDox`) exited 0 and created and pushed all three repositories for every one of these chains: | `--referent-chain` | recorded | read back by `load_yaml` | |---|---|---| | `openXdox,a #b,openDox` | `[openXdox, a #b, openDox]` | `REFUSED yaml-unparsable`; `validate-manifest.py` exit 2 | | `openXdox,it's,openDox` | as typed | `['openXdox', "it's, openDox"]` | | `openXdox,a]b,openDox` | as typed | `['openXdox', 'a]b, openDox']` | | `openXdox,"q",openDox` | as typed | `['openXdox', 'q', 'openDox']` | | `openXdox,null,openDox` | as typed | `['openXdox', None, 'openDox']` | | `openXdox,007,openDox` | as typed | `['openXdox', 7, 'openDox']` | | `openXdox,Atlas,openDox` | as typed | as typed, a project-leg name recorded as a product link | On this branch, each of those exits 2 with `REFUSED chain-link-malformed`, and no remote directory exists afterwards. The control `openXdox,openDox`, a three-link `openXdox,openYdox,openDox`, `opensoft/openXdox,openDox`, `OPENXDOX,openDox` and `openXdox,OPENDOX` still scaffold. The written projects' `validate-manifest.py` and `validate-repository-naming.py --project` exit 0. **Why refuse, and not quote (the issue's two options).** The naming policy already defines a link. Every link after the first is read from the previous link's `neutral_product_pins:` (`referent.chain.link_declared_by` in `contracts/repository-naming.yaml`). The example chains there are `[openXdox, openDox]` and `[openXwallet, openWallet]`, and `validate-manifest.py` says a recorded chain is "expected a list of neutral-product names". A name in the `neutral-product` form is a single plain YAML word, so the manifest stays plain, and it is still true that no writer quotes a flow item. Quoting would record `a #b` faithfully: a link the scaffold's own check calls broken, naming a repository that cannot exist. It would also make this writer depend on the quoted-flow-item reader, which #249, #250 and #252 are changing now. Checking only the repository-name alphabet is not enough either: `null`, `007` and `true` are valid GitHub names, and the reader resolves them as None, 7 and True. **Why only the middle links.** The first link must casefold to a `--pin` product (`[A-Za-z0-9_.-]`). The last must casefold to the `open<Product>` that the project's name claims. Neither can therefore hold a `#`, a quote, a bracket, a comma or a space, and the last cannot be a YAML bool, null or number. Both comparisons ignore case on purpose: a pin names which repository, not how it was typed (`_pinned_basenames`). Refusing `OPENXDOX` at an end would add a new spelling rule that this issue does not ask for. A test keeps the two ends as they were. A middle link has no such anchor, so it is judged by its form, and therefore by its case (see the behaviour change above). **Where else a chain is accepted.** `adopt-project.py` passes no chain to `naming_block` (:2887-2890). `scripts/family.py` and `update-shape.py` take none. `setup-project.py` passes everything after `--` to the scaffold, so it gets this check. `validate-repository-naming.py --referent-chain` takes a chain but writes nothing, so it is unchanged. - **Fails on 150c41d.** The new file, run in a throwaway worktree at 150c41d: 25 failed, 5 passed. 23 in-process refusal tests fail with `ProcessStarted: ['gh', 'api', 'repos/opensoft/openXdox/git/trees/111…']`: the run went past the chain to read the pin's tree. That `gh` was intercepted and never ran. The 2 end-to-end issue rows exit 0 with three bare remotes. The 5 that pass are the positive and unchanged-ends tests. - **Mutation probes** (each hunk reverted or bent in a throwaway worktree, new file re-run): - call removed: 25 failed; - slice widened to every link: 2 failed (the capitalised first and last links); - slice narrowed to `[1:-2]`: 25 failed; - any classified name accepted: 8 failed (`null`, `Atlas`, `opensoft`, `OPENYDOX`); - basename not taken: 1 failed (the qualified middle link); - check moved after `_declaration`: 23 failed (every in-process refusal hits `gh api`); - plural dropped: 1 failed; - help text reverted without regenerating: `test_cli_reference.py` 3 failed; - `_declaration` reading the argument inline again: 0 failed, as expected, because that change preserves behaviour. - **Reach.** All 14 tracked `*.yml`/`*.yaml` files load the same on 150c41d and on this head (7 ok, and 7 templates refused on their unrendered `{{...}}`). `repo_shape.py` is not touched. `naming_block` called from the scaffold is the only writer of `referent_chain`. - **Fix round 1 (5cb1d3e, on top of 114bec9)**, run at 5cb1d3e with the CI-pinned `git-filter-repo` 2.47.0 on PATH: - `tests/test_scaffold_referent_chain_links.py tests/test_scaffold_e2e.py tests/test_cli_reference.py tests/test_repo_hygiene.py tests/test_naming_policy.py`: 313 passed. - `tests/test_scaffold_pin_and_reuse.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_leg_paths.py tests/test_scaffold_ignored_shape_files.py`: 166 passed. - The new file alone holds 44 tests (30 before). `scripts/render-cli-reference.py --check` prints `[ok]`, `git diff --check` is clean, and no added line holds a host or scratch path. - Six more mutants, each applied to `scaffold-project.py` alone with the whole new file run (no `-x`), all caught: the policy pattern dropped from the remediation (1 failed), the `--explain` sentence dropped (1), links not stripped (3), the check reading its own unstripped split (3), the chain printed raw (26: the 22 refusal rows that assert the quoted chain, and the 4 line-break rows), and a bad link printed raw (7: the two single-quote rows, the two-bad-links refusal and the 4 line-break rows). - The streams under "Targeted runs at 114bec9" below were NOT re-run at 5cb1d3e, except the files named above. Fix round 1 changed three comment or docstring lines and the refusal's own message in `scaffold-project.py`, and the new test file. - **Targeted runs at 114bec9**: all green. - `tests/test_scaffold_referent_chain_links.py tests/test_scaffold_e2e.py tests/test_scaffold_pin_and_reuse.py tests/test_naming_policy.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_leg_paths.py tests/test_scaffold_ignored_shape_files.py`: 402 passed. - `tests/test_yaml_flow_quotes.py tests/test_load_yaml_refusals.py tests/test_adopt_plan_escapes.py tests/test_manifest_validator.py tests/test_pin_validator.py tests/test_neutral_pin_validator.py tests/test_repo_hygiene.py tests/test_cli_reference.py tests/test_adopt_plan.py`: 275 passed, 1 skipped. The skip is `test_adopt_plan.py:393`: `SHAPE_MEDXEHR_CLONE` is unset. - `tests/test_shape_doctor.py`: 181 passed. - In a throwaway merge with main 39ac59c (#229): `tests/test_scaffold_referent_chain_links.py tests/test_scaffold_mount_ignored.py tests/test_scaffold_pin_and_reuse.py tests/test_cli_reference.py` gave 105 passed. Re-checked at 5cb1d3e after fix round 1: `git merge-tree --write-tree` against `origin/main` 4b2add8, `fix/yaml-block-key-nul` 9be6287 and `fix/yaml-quote-boundaries` 21fc213 is clean for each. `fix/yaml-flow-mapping-keys` (#252) has no branch on origin or in the shared repository now; its earlier tip 2438190 is still in the object store and is clean too. `tests/test_scaffold_referent_chain_links.py tests/test_cli_reference.py tests/test_repo_hygiene.py` on the throwaway merge of 5cb1d3e with 4b2add8: 107 passed. `git merge-tree --write-tree` of 114bec9 against each sibling branch and against main: all clean, checked right before the first push. - `fix/yaml-quote-boundaries` (#249, #250) at 150c41d, so no commit yet: clean. It needs a fresh check once it has commits. - `fix/yaml-block-key-nul` (#248) at 9be6287: clean. - `fix/yaml-flow-mapping-keys` (#252) at 2438190: clean. - `origin/main` at 39ac59c (#229 via #235, which also edits `scaffold-project.py`, in `_build_assembly_root`): clean, and its tests pass on the merged tree (above). This change does not touch `scripts/repo_shape.py`, so none of the reader fixes overlap it. - **Follow-up 1: `--pin` accepts a product name the manifest reader resolves as something other than a string.** `--pin null@<sha>`, `007` and `true` each scaffold with exit 0, and the project's own `validate-pins.py` then exits 1 with `neutral-pin-entry-malformed: … entry None` (or `7`, `True`). It reaches the chain's FIRST link too: `--pin null@<sha> --referent-chain null,openDox` scaffolds three repositories, records `referent_chain: [null, openDox]`, reads back `[None, 'openDox']`, and `validate-pins.py` exits 1. That writer belongs to `--pin`, not to the chain, and the eventual fix should cover the first link. It is a candidate follow-up issue. - **Follow-up 2: the validators still accept such a chain.** `validate-manifest.py` and `validate-repository-naming.py` do not flag a recorded middle link that is not a neutral-product name, such as `[openXdox, Atlas, openDox]` written by an older scaffold or by hand (all three validators exit 0 on it). Both are shipped, digest-pinned copies, and neither writes anything. The policy file also says the chain rule is data, while the middle-link form is a constant in `scaffold-project.py` (`CHAIN_LINK_FORM`). It is a candidate follow-up issue. - `<owner>/` before a link is still dropped without being checked, for every link, as `_chain_links` has always done. The manifest records basenames. - The full suite and the macOS and Windows legs were not run locally. The new tests send git nothing on stdin, and write the stand-in repositories' files as bytes. Reviewed adversarially (APPROVE WITH NITS, 0 blockers); fix round 1 escapes the refused chain and covers the remediation and link-splitting tests. CI is green on Linux, macOS and Windows. Closes #251 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-notice It merged with no conflict. Main brings six commits since 54aa69a: - e7d1720, update-shape.py (#208 via #224); - 404760f, scripts/repo_shape.py's double-quoted flow items (#213 via #244), with tests/test_yaml_flow_quotes.py; - 4835fb4, adopt-project.py's plan writer escapes (#199 via #247), with tests/test_adopt_plan_escapes.py; - b538015, scaffold-project.py's leg paths against the shape's own paths (#197 via #234); - 150c41d, update-shape.py's `apply --add` (#202 via #233); - 39ac59c, scaffold-project.py's leg mount `--force` (#229 via #235). Two of them touch adopt-project.py, and no hunk of either meets this branch's: - 4835fb4 changes the plan writer (`y`, `_PLAIN_RE`), adds `_shown` to the plan and check reports, and writes the `--paths-from-file` listing in `_extract_leg` through `write_paths_listing`. This branch changes other lines of `_extract_leg`: it reads the tree digest before the push and pushes through `_push_the_leg`. The signature is the same on both sides, so main's new direct `_extract_leg` test calls it as written. - b538015 moves `_overlap`, `_first_and_more`, `MOVE_THE_LEG` and `CASE_ASIDE` into scripts/shape_materialize.py, and imports the first three back under the same names. This branch adds no use of any of them. A `symtable` check finds no name that the merged adopt-project.py, scaffold-project.py or scripts/shape_materialize.py reads and nothing binds. `git diff -U0` of this merge against 39ac59c equals the branch's against 54aa69a, apart from the hunk headers. On the merged tree, tests/test_adopt_plan_escapes.py, tests/test_yaml_flow_quotes.py, tests/test_scaffold_leg_path_collisions.py and tests/test_adopt_leg_path_collisions.py give 275 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #229 (comment)
Fixes #229.
What
_build_assembly_rootnow passes--forceto the leg mount'sgit submodule add, the cure #200 gave adopt for #174. A comment says why. The root is a freshgit initthe materializer has just filled, so two sets of rules reach the mount: the shape's own.gitignoreand the operator's global excludes. A gitlink, once added, is tracked, and no ignore rule touches it again.--forceis git's flag here, not one of this tool's, which still has none.adopt-project.pypairs its--forcewith_refuse_an_unmountable_path, which refuses before the add the two things--forcewould otherwise let through. The scaffold needs no such guard, and each reason was checked rather than assumed:.gitmodulesand the first mount's gitlink only. This was logged around the real scaffold for the default paths and forlegs/specwithimpl/code.refuse_bad_leg_paths(scaffold-project.py accepts a non-canonical leg path such as--spec-path spec/, writes a straysubmodule.spec/.urlbeside a registration that points at the work directory, and the new repository's recursive clone fails once the work directory is gone: #169's bug on the scaffold side #179, scaffold-project.py: a leg's mount path must be canonical or it is refused before anything is created, so--spec-path spec/no longer scaffolds a project whose recursive clone fails once the work directory is gone (#179) #201) keeps them distinct and non-nested. Over 24,025 ordered pairs of 155 case-varied paths, none it accepts is equal or nested once case is ignored. The guard matters: with--force, a second add at the same path exits 0 with "Adding existing repo" and never mounts the code leg.find templates -type lis empty, and no tracked entry has mode 120000).--force; the 5 that differ are the ignore-rule cases this PR fixes. Refusing a shape-written path up front is scaffold-project.py accepts a leg path that collides with a path the shape writes (--spec-path contracts,--code-path project.yaml), creates all three repositories and pushes both legs, then dies: #172's shape half on the scaffold side #197's (PR scaffold-project.py: a leg path that is, holds or lies inside a path the shape writes into the assembly root, a--pin'scontracts/<product>-pin.yamlincluded, is refused before anything is created, under--dry-runalike, through the one definition adopt-project.py asks, so--spec-path contractsno longer creates three repositories and pushes both legs to die atgit submodule add(#197) #234).--helpis unchanged, sodocs/cli.mdis too.Why (repro on
main)A leg path the shape's
.gitignorematches (--code-path __pycache__,legs/__pycache__,.venv,legs.pyc,.DS_Store, or--spec-path __pycache__) planned cleanly under--dry-run. The real run created all three repositories, pushed both legs, then died at the mount with git's "The following paths are ignored by one of your .gitignore files". That left three repositories and no project, and the corrected re-run was refused asscaffold-remote-exists. Worse, an operator whose own global excludes namespecorcodecould not scaffold with the DEFAULT paths at all, and no spelling of the invocation avoided it. The issue has the table.Tests
tests/test_scaffold_mount_ignored.py, 38 tests, one real scaffold per case:codeandspec;--code-pathover__pycache__andlegs/__pycache__(directory rules, which fail after the clone) andlegs.pycand.DS_Store(file rules, which fail before it);--spec-path __pycache__, for the first mount;srcas the control.check-ignorewithout the slash says "not ignored" for__pycache__/. It then asserts exit 0 and a gitlink at each leg path at that leg's pushed tip (git ls-treeon the bare remote, never the exit code alone)..gitmodulesmust name exactly the two canonical remotes. A recursive clone must check out both legs, and the clone's ownvalidate-manifest.pyandvalidate-pins.pymust pass.README.md,scriptsandcontractsas--code-pathstill exit 2 with no gitlink at that path. Only the exit is asserted, so the test holds before and after scaffold-project.py accepts a leg path that collides with a path the shape writes (--spec-path contracts,--code-path project.yaml), creates all three repositories and pushes both legs, then dies: #172's shape half on the scaffold side #197 lands. With scaffold-project.py accepts a leg path that collides with a path the shape writes (--spec-path contracts,--code-path project.yaml), creates all three repositories and pushes both legs, then dies: #172's shape half on the scaffold side #197 merged in scratch it passes: 169 passed with the collision and leg-path suites.test_scaffold_mount_ignored.py test_scaffold_ignored_shape_files.py test_scaffold_leg_paths.py test_scaffold_e2e.py test_scaffold_pin_and_reuse.py test_cli_reference.py): 157 passed, 0 skipped.--forceturns 24 of the 38 tests red: every check of every ignore-rule case. The control and the still-refused cases stay green.ghtrap is a#!shim, so it is skipped on Windows, where--local-remote-diris the guarantee. macOS and Windows have not run these tests yet; this PR's CI is the first time.Landing notes
main54aa69a, with this lane's scaffold-project.py: a leg path that is, holds or lies inside a path the shape writes into the assembly root, a--pin'scontracts/<product>-pin.yamlincluded, is refused before anything is created, under--dry-runalike, through the one definition adopt-project.py asks, so--spec-path contractsno longer creates three repositories and pushes both legs to die atgit submodule add(#197) #234 (scaffold-project.py accepts a leg path that collides with a path the shape writes (--spec-path contracts,--code-path project.yaml), creates all three repositories and pushes both legs, then dies: #172's shape half on the scaffold side #197; different function in the same file; all three merged together cleanly in scratch), and with shape_materialize.run() and 26 other git and gh calls read what the child wrote as UTF-8 whatever the locale, so anadopt-project.py executewhose leg push is rejected with a message naming a non-ASCII path endsREFUSED pushing the spec legand the ruleset hint underLC_ALL=C PYTHONUTF8=0, not a UnicodeDecodeError traceback (#217) #230 (fix/decode-git-output-utf8).scripts/family.py addruns its owngit submodule add -- <url> members/<Project>without--force. An operator's global excludes namingmembersor a member would likely hit the same class of failure there. This is unverified, and a follow-up if it reproduces.🤖 Generated with Claude Code
Summary by Sourcery
Make scaffolded projects robust to ignored leg paths and inherited Git initialization state.
Bug Fixes:
Enhancements: