Repository navigation
templates: project.yaml, the leg pins and family.yaml quote their text placeholders, so --id 007, --tracking-branch 2026, --spec-path true or --org null read back as typed, and family.py add quotes a member id alike (#259) - #395
Conversation
…r text placeholders between double quotes, so `--id 007`, `--tracking-branch 2026`, `--spec-path true` or `--org null` are read back as typed instead of as a number, a boolean or no value, and `family.py add` quotes a member's id the same way (#259) The assembly root's `project.yaml` wrote `id`, `elected_on`, `visibility`, `tracking_branch` and both legs' `path` bare, each leg pin its `submodule_path`, and `family.yaml` its `id`, `org`, `created_on`, `visibility` and `tracking_branch`. Every reader of those files reads them with `repo_shape`'s YAML subset, which takes a bare `007` as 7, `2026` and `20261011` as numbers, `1.5` as a float, `true` as True and `null` as None, and the checks on the flags admit all of them. So on c7c02fa: - `scaffold-project.py --id 007` (or `2026`, `true`, `null`, `20261011`) exited 0 with a project whose own gate failed `manifest-id`, and `--spec-path 2026` one that failed `manifest-leg-path` and `pin-path-mismatch`; - `--tracking-branch 007` pushed each leg on `007`, then the root's own `bootstrap.py` put the legs on a local `7` with no upstream, and `true` and `null` did the same on `True` and `main`; - `adopt-project.py execute` wrote the same lines under `adoption verified`; - `family.py init --id 007` or `--org null` pushed a holder its own `validate-family.py` refused (`family-id`, `family-org`). Brett Heap ruled on 2026-10-12, "quote #259". `render` already spells a value between a YAML template's double quotes with `double_quoted` (#284), so each of these values now reads back exactly; an ordinary value renders byte for byte as before apart from the two quote characters. `org:` is quoted with the family's other text fields: it is the same defect in the same file (`--org 2026` and `--org null` pass `checked_value`). `family.py`'s `members:` writer records each member's own `project.yaml` id. That id is read back exactly now, so written bare `007` came back as 7 and the holder reported `member-id-mismatch` for a member it had just pinned. `_member_id` quotes an id only where the reader would not take the bare text back, so no existing holder's block moves by a byte. No pinned digest moves: the four files are TEMPLATED, not COPIED, so no row of any project's `contracts/shape-pin.yaml` names them. Projects made before this keep their bare lines, which read correctly for every plain-text value, so the readers keep their `str()` fallbacks. Tests: `tests/test_template_text_placeholders.py` (new) renders every quoted field with eight values the reader misreads, and runs `scaffold-project.py`, `adopt-project.py` `plan`/`check`/`execute` and `family.py init`/`add` into local bare remotes, then a clone, the root's own `bootstrap.py`, its validators and `bump-leg.py`; all 123 fail on c7c02fa. Fourteen existing tests that asserted or edited a bare rendered line now spell it quoted. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…are id the reader misreads is rewritten quoted, and a test hands `rewritten` a row whose id the reader raises on, so the false "no existing holder's `members:` block moves by a byte" is gone and deleting the `except YamlError` fails 7 tests (#259) The review of 233dc71 found two nits in `_member_id`. N1. The docstring said no existing holder's `members:` block moves by a byte. It does move once: a holder written before #259 holds `id: 2026` for a member scaffolded with `--id 2026`, and the next `bump`, `add` or `remove` rewrites that row as `id: "2026"`. That is harmless, because `validate-family.py` compares the id with `str()` and passes before and after. The docstring now says so, and a row whose id reads back as text does not move. The member test's docstring had the same overstatement ("no existing holder's block moves") and is corrected the same way. The `except` carries a comment naming the ids that reach it. N3. The `try/except YamlError` had no test: with it removed every test still passed. `[`, `{`, `&a`, `*a`, `!t` and an unclosed `'` or `"` raise `YamlError` when read bare, and none of them is a `PROJECT_ID_RE` id, so no tool writes one, but a hand-edited holder can hold one between double quotes. `test_a_member_id_the_reader_refuses_bare_is_rewritten_between_quotes` (7 cases) opens such a holder, rewrites its `members:` block with `Family.rewritten`, and asks that the id read back exactly and the text come out byte for byte. With the `try/except` removed all 7 fail with the reader's `YamlError`, and with the `except` returning the bare text all 7 fail too. No behaviour changes: the `.py` change is a docstring and a comment. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-12T15:28Z) @codex review |
|
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 21 hours and 13 minutes by commenting |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewer's GuideThe PR fixes YAML scalar coercion by quoting all affected text placeholders in project, pin, and family templates, and makes family member rewrites quote only IDs that would not round-trip as the same text. Extensive unit and end-to-end tests cover misread values, legacy compatibility, parser errors, and the scaffold/adopt/family workflows. Sequence diagram for preserving text placeholders through YAML renderingsequenceDiagram
participant CLI
participant TemplateRenderer
participant YAMLReader
participant Validator
CLI->>TemplateRenderer: render(value)
TemplateRenderer->>TemplateRenderer: double_quoted(value)
TemplateRenderer-->>CLI: quoted YAML field
CLI->>YAMLReader: load_yaml(document)
YAMLReader-->>CLI: original text value
CLI->>Validator: validate manifest, pins, or family
Validator-->>CLI: validation passes
Sequence diagram for conditional family member ID quotingsequenceDiagram
participant Family
participant YAMLParser
participant Holder
participant FamilyValidator
Family->>Family: _member_id(value)
Family->>YAMLParser: parse_yaml(id: value)
alt bare value round-trips as the same text
YAMLParser-->>Family: {id: value}
Family->>Holder: write bare id
else value is coerced or parser raises YamlError
YAMLParser-->>Family: different type or YamlError
Family->>Holder: write double_quoted(value)
end
Holder->>FamilyValidator: load_yaml(family.yaml)
FamilyValidator-->>Holder: member ID matches project ID
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
#314 landed as 3ef059c: scaffold-project.py and adopt-project.py ask the new project's YAML reader and validate-manifest.py about the manifest's values before anything exists. The merge is clean. The next commit composes this branch's quoted templates with #314's tests. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ues.py and scaffold-project.py: #314's tests compose with the quoted templates, so the 40 values it refused as misreads are asserted held as typed, `visibility: "true"` and `elected_on: "20261011"` stay refused in the validator's words, `--tracking-branch=~` is a strict xfail until #355, and `--help` no longer says `--id 007` or `--tracking-branch null` is refused (#259) #314's misread gate (`manifest_misreads`) asks the TEMPLATE how a value is written. With `project.yaml` quoting `id`, `elected_on`, `visibility` and `tracking_branch`, it refuses nothing on the shipped templates, and 45 of #314's tests failed on the merge: 36 in the scaffold file and 9 in the adopt file, the same 45 ids the composition note counted on #314's older head. They are not one kind: - 40 expected a refusal the value no longer needs. Each now asserts that the value is ACCEPTED. A scaffold of `--id null|true|false|007|2026`, or of `--tracking-branch true|True|TRUE|false|False|null|Null|NULL|007|2026|1.5|1.50|0`, exits 0. The manifest it pushes holds the text, and each branch is the one every remote has. `--dry-run` prints the plan, and an id derived from `--project True|Null` is held as `"true"`/`"null"`. `--id=null` and `--tracking-branch=true` reach the forge's `gh repo create`. Adopt's `id: "007"` checks clean and `execute` writes it as typed. No coverage of the rule is dropped. A module copy of the standard writes the four lines bare again, as before #259, and there the rule's sentence, its remediation, the derived-id wording, the line-break sentence and `misread_placeholders` are asserted as before. The shipped templates are asserted to give no misread for any of those values. - 4 are still refused, by #314's validator gate, in other words: `visibility: "true"` and `elected_on: "20261011"` are now text the validator refuses (`visibility is 'true'`, `elected_on is '20261011'`, no longer `True` and `20261011` the number). Their premise that a misread rule judges them, so that the validator is not asked again, no longer holds, and the test now asserts the validator's one finding with or without `misreads_elsewhere`. - 1 is a real break: `--tracking-branch=~` reads back as `~` and dies at `git init -b ~`, after `gh repo create` against GitHub. That is #283, and PR #355's `git check-ref-format --branch` refuses it. No branch check is added here. The row is `xfail(strict=True)`; no test file used xfail before. It asserts the refusal #355 gives, and on a local merge with #355's head it reports XPASS(strict), so #355's CI says when to take the mark off. The adopt file has one test fewer (78). Two `[id-007]` rows, the flipped `..._on_this_rule_alone` and the passing `misreads_elsewhere` one, are one test that asserts both. `scaffold-project.py`'s `--id` and `--tracking-branch` help said a value the reader would read as another type, `null`, `true`, `007`, is refused, and its module docstring and `_refuse_unholdable_manifest` said the same. On this tree each is accepted, so the sentences are dropped or reworded, and `docs/cli.md` is regenerated. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…}"` composes with #381's `member-duplicate-id` test, whose rewrite now matches the id as the template writes it and keeps its quoting, so the full suite no longer fails `test_a_member_whose_id_changes_between_the_probe_and_the_mount_is_unwound` on Linux (#259) #381 landed `member-duplicate-id` with a test that renumbers a member between the probe and the mount by rewriting `id: irqs` to `id: irrs` in the member's `project.yaml` with `re.sub(rb"(?m)^id: irqs$", ...)`, and asserts the line changed. #395 makes the assembly-root template write `id: "irqs"`, so that pattern matched nothing and the assertion failed. The two PRs were green separately and met only when #395 merged main, and the targeted runs that carried #395 never included this file. The rewrite is now `re.sub(rb'(?m)^id: ("?)irqs\1$', rb"id: \1irrs\1", text)`: it matches the id bare or quoted and writes the new id back in the quoting the old one had, so the rewritten member still loads. The `assert changed != text` stays as it was, so the test still fails loudly if the line ever stops matching. Swept `tests/` for every other regex or literal that matches an id, org, created_on, elected_on, visibility, tracking_branch, path or submodule_path line of a template-written file in its unquoted form. The only other one that matches such a line is `tests/test_shape_doctor.py`'s `_bad_family_id` (`^id: .*$` to `id: Bad Id`): it matches the quoted line too, and `Bad Id` is a plain scalar that loads, so it still makes `family-id` the one finding. Nothing else needed a change. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lines and clones nothing, and render-cli-reference.py refuses a tree that is not a checkout, so asking a download for help is no longer a clone of GitHub (#341) (#452) From anywhere but a checkout of the standard (a downloaded `setup.sh`, `curl … | bash -s -- --help`, a `git archive` extraction), `setup.sh --help` went down section 0 like any other run. It cloned the public upstream before printing a word, died with git's exit 128 offline, and otherwise printed the CLONE's `setup-project.py` usage. `scripts/render-cli-reference.py` inherited all of it in such a tree: every rendering, `--check` included, reached GitHub and then documented upstream's flags under that tree's `setup.sh` heading. The clone attempts were reproduced on 87d2eb7, offline (`GIT_ALLOW_PROTOCOL=file`); what the generator renders when that clone succeeds is read from the code, not reproduced: | | base 87d2eb7 | this branch | |---|---|---| | lone `setup.sh`, `bash setup.sh --help` | `(0) self-bootstrap`, `git clone --quiet --depth 1 -- https://github.com/opensoft/openRepoShape.git <tmp>`, `fatal: transport … not allowed`, exit 128 | the shim's own text, exit 0, no clone (`GIT_TRACE`), no temporary directory | | same, `OPENREPOSHAPE_REPO=<checkout>` | a local clone, then the clone's `usage: setup-project.py …` | the shim's own text, no clone | | `bash -s -- -h < setup.sh` | the clone, exit 128 | the shim's own text, exit 0 | | `render-cli-reference.py --check` in a copy with no `.git` | tried the clone (stopped only by `GIT_ALLOW_PROTOCOL`, surfacing as `setup.sh --help exited 128`) | refuses, naming the tree, exit 2, no clone | **What changed.** **`setup.sh`** - Section 0's one scan of the command line now also notes `-h` and `--help` before the first `--`. - The shim answers them first: before the `$OPENREPOSHAPE_REPO` guard, before `mktemp`, before `git clone`. That is where `setup-project.py` answers them: before it looks at anything. - The text says: - this is the bash front door to `setup-project.py`; - any other run from here first clones the standard into a temporary directory, from `$OPENREPOSHAPE_REPO` or, when that is unset or empty, `https://github.com/opensoft/openRepoShape.git`; - the one flag list is `python3 setup-project.py --help`, which answers with no network from a checkout or from that one file downloaded on its own, and `docs/cli.md` in the repository. - It lists no flag, so #50's one `--help` still holds. - The default is now one constant, `DEFAULT_SHAPE_REPO` (the name `setup-project.py` gives it), read by both the clone and the help. The help never prints the variable's value. - Inside a checkout nothing changes: the shim still execs `setup-project.py` before section 0 reads a word. - The header comment no longer says the shim "parses nothing". It now says the shim parses nothing of the RUN, and names the three things section 0 reads. The comment above the scan no longer says the scan never decides what the run does: the first two reads only decide whether a temporary directory is removed and whether a ref is checked out, and the third decides whether there is a run at all. **`scripts/render-cli-reference.py`** - `require_a_checkout()` asks `setup.sh`'s own `is_shape_checkout` question: `git -C <tree> rev-parse --show-toplevel`, and `scaffold-project.py` plus `contracts/repository-naming.yaml` at that top level. - It also asks that the top level IS the tree (`os.path.samefile`). A copy nested inside another checkout passes the shim's probe, and its `setup.sh --help` would print that checkout's usage. - It is called in `main()`, before the no-bash logic, and at the top of `render()`, which `tests/test_cli_reference.py` calls directly. The refusal names the tree and says to run it in a `git clone`, exit 2. - It reads git's answer the way the shim's `$(...)` does: `os.fsdecode(...).rstrip("\r\n")`, so a checkout whose path ends in a space, or is not UTF-8, is a checkout here too. - Every child now runs under `GIT_ALLOW_PROTOCOL=file`, so a regression fails closed. - Inside a checkout neither measure changes a byte of a rendered help block. `--check` was `[ok]` before, and `[ok]` after regenerating. **Docs** - `README.md`: "`python3 setup-project.py --help` and, from a checkout, `setup.sh --help` list every flag, this one included. From a download, `setup.sh --help` clones nothing and lists none: it names `setup-project.py --help` … and `docs/cli.md`". That is +3 lines against main; the README cap in `tests/test_repo_hygiene.py` goes 1419 -> 1422, with its dated paragraph. `openRepoShape --help` is not the subject any more, because it names eight of `setup-project.py`'s fourteen flags besides `--help`. § What setup.sh does no longer gives the shim a line count ("about seventy lines of code"; it was 93 before this change and is 122 now), here, in the handbook and in `setup-project.py`'s docstring. - `docs/handbook.html`: the page's copy of that sentence and of the shim's description, the footer's history clause, and the new README blob (`5465d135…`). - `openRepoShape --help`: its one sentence "`./setup.sh --help` prints setup-project.py's usage, which lists them all" was false from a download. It now names `python3 setup-project.py --help`, and `./setup.sh --help` "in a checkout of this standard". - `docs/cli.md` is regenerated: a note under `setup.sh` saying what `--help` does outside a checkout, and that `openRepoShape` sentence. - `tests/test_setup_sh.py`: `test_no_test_here_depends_on_gh_being_authenticated`'s docstring is made true. **Decisions** (the coordinator's; Brett may reverse). 1. **The shim's answer outside a checkout is option (a), with no flag list.** It is a short text of its own, exit 0, with no clone, no network and no temporary directory. Inside a checkout `--help` is still `setup-project.py`'s. - `-h` is treated like `--help`, because `setup-project.py` reads both. - Only arguments before `--` count. `setup.sh <Project> -- --help` is still a run, whose scaffold is asked for ITS help. - **Known false positives** (written into the shim's comment): runs `setup-project.py` would refuse anyway, such as `--org --help` or `--wat --help`, get the text and exit 0 instead of exit 2. Nothing is created either way. - **One known false negative**: `--visibility -- --help`, where `--` is a value, still clones as before. Fixing it would need the value-flag table in bash, which is the second parser #50 removed. 2. **Whose help.** The shim's text is its own, so it names no clone URL or ref. No path that prints a usage is left unattributed. `setup.sh <P> -- --help` prints the clone's scaffold usage under the `(0) self-bootstrap` clone line and `checkout: <tmp>`, which already say whose it is. 3. **The generator gets both measures**: the checkout refusal and `GIT_ALLOW_PROTOCOL=file`. 4. **Docs**: README, the handbook's reading of it, the shim's header, the test docstring and `openRepoShape --help`'s sentence, with `docs/cli.md` regenerated. 5. **The #50 guard is changed on purpose.** `tests/test_setup_project_py.py::test_setup_sh_parses_no_flags_of_its_own` refused any `--help` in the shim's code. It now excuses the spelling in exactly two places: the lines of `shim_help()`, each of which must be one `say "..."` with no `$(`, `${` or backtick in it, and section 0's one `-h|--help)` label. Outside those two, neither `--help` nor a bare `-h` may occur in the code. It still refuses `-y)`, `-h)`, `--yes)` and a second `--help)` label, and since fix round 1 it also refuses a quoted `"--help")`, a `[[ $1 == --help ]]` reader anywhere else (the developer path, the scan), and a reader hidden in a `say` line. **Evidence.** The runs and the merge-tree below were taken at e2df9a8, the branch's head before the last merge of main (the last paragraph). - **Targeted runs on e2df9a8** (d15a888, the fix round, merged with `origin/main` 53bee5b; the branch's first commit 968db85 is the writer's 16d1639 re-authored with the same tree 0d3243f), with a stub `gh` first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`, `GIT_ALLOW_PROTOCOL=file` and a blanked git identity: - `tests/test_setup_sh.py` and `tests/test_setup_project_py.py`: **132 passed**, 0 skipped; - `tests/test_cli_reference.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_openreposhape_command.py`: **164 passed**, 0 skipped; - all six files together: **296 passed**, 0 skipped, 0 failed. Before the merge, on a throwaway merge of d15a888 and main 5678f68, the same six files gave 296 passed as well. review341 ran them on 968db85 merged with main 3ef059c: 132 + 162 passed. - **The new tests against base 87d2eb7's code**: 15 failed. These are every new behaviour test (the shim's help ×4 spellings, its text, no echo, `bash -s`), the five generator refusal tests, `GIT_ALLOW_PROTOCOL` and the shim-probe tie. The 9 that pass are the "unchanged" guards (`-h`/`--help` inside a checkout, `-- --help` passthrough ×2) and pre-existing tests. - **Mutation proof**: 18 single-hunk mutations in a throwaway worktree, each caught by a named test. They cover: - the help exit; - `-h`; - the `--` stop; - help after the env guard; - the default not named; - a flag line added; - the default not shared; - a second `--help)` label; - `main`'s guard, `render`'s guard, and both; - the top-level-is-the-tree check; - the files check; - `GIT_ALLOW_PROTOCOL`; - the files drifting from the shim; - the Tool note; - the `openRepoShape` sentence; - the README sentence. - **The new tests**: - `tests/test_setup_sh.py` (`WINDOWS_SKIP` with the rest of the file): - `--help`, `-h`, after a run's arguments, and beside `--keep-shape-checkout --shape-ref`: exit 0, the shim's text, no `(0) self-bootstrap`, no usage, no `git clone` in `GIT_TRACE`, nothing in `TMPDIR`; - the text names the variable, its default, `setup-project.py --help` and `docs/cli.md`, and no flag but `--help`; - a forged `$OPENREPOSHAPE_REPO` is never echoed; - `bash -s -- --help`; - `<Project> -- --help` and `-- -h` still clone (locally) and reach the scaffold's usage. This is also the positive control that the trace sees a clone; - inside a checkout, `-h` and `--help` are `setup-project.py --help` byte for byte, on stdout and on stderr (stderr was not compared before the fix round; by probe it was already identical). - `tests/test_cli_reference.py`: - a `git ls-files` copy with no `.git`, one inside an unrelated work tree, and one inside a work tree that looks like a checkout of the standard: `--check` and a plain write both exit 2, naming the tree, with nothing written and no clone; - `render()` itself refuses; - the checkout refusal comes before the no-bash one; - `child_env()` pins `GIT_ALLOW_PROTOCOL=file`; - the generator's files are the ones the shim's probe reads; - (fix round 1) `--check` in a real `git init` copy whose path ends in a space, and in one whose path is not UTF-8, exits 0 with `[ok]`; each is skipped where the file system cannot hold the name (both on Windows; the second wherever creating the directory fails, which is expected on macOS, where no runner was available here). **Merge-tree, as of e2df9a8 (re-run after merging `origin/main` 53bee5b into the branch).** - At e2df9a8, `origin/main` (53bee5b) is an ancestor of the head: the branch carries a merge commit, so it was mergeable with main by construction. `render-cli-reference.py --check` was `[ok]` on it. - **#352 landed as 53bee5b (#289) and conflicted in `docs/handbook.html`'s footer, as predicted.** The merge keeps both "added (#n)" clauses in landing order after "(#288)": (#289), then (#341), and keeps this branch's `README.md blob 5465d13`, which is `git hash-object README.md` again: main does not touch README.md and its own blob is `e8da9381…`. - #314 has landed (3ef059c). The other PRs then open that share a file with this branch, re-checked against d15a888 before the merge (heads fetched at that time) and #403 again against e2df9a8: - clean against this branch: #394 (`tests/test_repo_hygiene.py`, not the README cap; it conflicts with main alone in `tests/test_manifest_validator.py`, a file this branch does not touch), #395 (one-line `visibility` assertion edits in `tests/test_setup_sh.py` and `tests/test_setup_project_py.py`, independent of this branch; its hunks in `docs/cli.md` are at :246-279), #397, #400 and #401 (each regenerates other sections of `docs/cli.md`); - **#403 (head 5b1da66) conflicted in the same footer hunk**: it merged clean with main alone and conflicted with this branch. It has since landed as dba5fb0 (#383, #403), and the merge of main into this branch (the last paragraph) keeps every clause in landing order after "(#289)", that is (#383) and then (#341), and names the blob of the README that lands, `5465d135ca7263010ee354424041f90c4553d2e7`. Main does not change README.md, so that is the blob this branch already named. - `docs/cli.md`: #395, #397, #400 and #401 all merged clean with d15a888 plus main 5678f68, and the generator's `--check` was `[ok]` on each merged tree (#395's head 1ef3310, #397's abb27e6, #400's 7062a47, #401's 409f10b). Should one ever conflict, regenerating resolves it. **Left out** (candidate follow-ups, not filed here). - `setup.sh --preflight` from a download still clones before checking the machine, though `setup-project.py` answers `--preflight` before its own self-bootstrap. The preflight is Python, so the shim cannot answer it itself; it needs its own decision. - From a download, `setup.sh --help` still needs Python 3.9 and git first: the interpreter and git checks run before the checkout probe, as they always did. - Windows does not run the shim. The generator refusal tests do run there: they reach the refusal before any bash. The `tests-windows` job was their first run on that platform. **Fix round 1 (review341's CHANGES NEEDED 0/1/5).** One commit (d15a888) on top of 968db85, then a merge of `origin/main` 53bee5b (e2df9a8), no force: - **S1 (should).** The #50 guard excused the spelling `--help` everywhere in the shim, so a second reader in any form but an unquoted label passed it. It now excuses the spelling in `shim_help()`'s `say "..."` lines and the one `-h|--help)` label, and nowhere else (decision 5). Mutation proof in a throwaway worktree: the guard passes on the unmutated shim and fails on each of review341's three variants (a quoted `"--help")` label, a `[[ "${1:-}" == --help ]]` reader in the developer path, a `[[ "$arg" == --help ]]` reader inside the scan) and on four more (a reader as its own line in `shim_help()`, one chained after a `say`, one substituted into a `say`, a bare `-h` reader). The first four and the bare `-h` reader fail only the guard, and the help tests around it stay green; the chained and substituted `say` variants also break the help's own output. - **N1.** The inside-a-checkout test compares stderr as well as stdout. With the developer-path reader above, it now fails and `test_help_exits_zero` still passes. - **N2.** The generator reads git's top level with `os.fsdecode(...).rstrip("\r\n")`, so a checkout at a path ending in a space, or not UTF-8, is accepted again, as the shim accepts it. A test for each; on 968db85's generator both fail with exit 2 "not a checkout". - **N3.** README and the handbook name `python3 setup-project.py --help` as the command that lists every flag; `openRepoShape --help` names eight of fourteen. The handbook's copy, its footer clause and its README blob follow. README stays 1422 lines, so its cap does not move. - **N4.** "About seventy lines of code" is dropped from README, the handbook and `setup-project.py`'s docstring: the shim is 122 lines of code, 93 before this change, and a count in prose goes stale again. - **N5.** The comment above section 0's scan says the third read decides whether there is a run at all. - `docs/cli.md` is unchanged: no `--help` text moved, and `--check` is `[ok]`. Reviewed adversarially at 968db85, the branch's first commit (CHANGES NEEDED: 0 blockers, 1 should-fix, 5 nits). The shim and the generator behaved as this description says. The should-fix was the #50 guard, which a second `--help` reader slipped past; the nits were a test that compared stdout and not stderr, two real checkouts the generator refused (a path ending in a space, a path that is not UTF-8), and three pieces of stale doc or comment text (the README and handbook sentence that said `openRepoShape --help` lists every flag, the shim's line count, and the shim's own comment). Fix round 1 (d15a888, merged with main 53bee5b at e2df9a8) resolved them, and its re-verification of e2df9a8 read APPROVE WITH NITS (0 blockers, 0 should-fixes, 1 nit: § What setup.sh does does not say that the shim answers `--help` itself outside a checkout, left as it was because README says it a few lines above). Main 75052cb was then merged in at 4c28890: the one conflict, `docs/handbook.html`'s footer, keeps both "added" clauses (#383's, then #341's) and names the merged README's blob (5465d13), and the merged `docs/cli.md` matches its generator. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 4c28890 (main 75052cb merged in). Closes #341 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is `PRODUCT=PATH` only when its key names a chain link, and a link source that cannot be entered is a WARNING with the system's reason, not a traceback (#338) (#453) **What was wrong.** Two faults, one in the parser and one in the lookup. - **The parser.** `_link_sources_from_args` split every `--link-source` value at its first `=`. A link tree under a directory named `a=b`, passed bare, became a source keyed to a link `a` that no chain names. Nothing asked for it, so the link stayed `declared-unverified`, exit 0. The WARNING then offered `--link-source openXdox=<path>` to the person who had passed exactly that. A mistyped key failed the same silent way. - **The lookup.** `repo_shape.link_manifest_path` asked `Path.is_dir()` and then `is_file()`. Before Python 3.14 those swallow only ENOENT, ENOTDIR, EBADF and ELOOP. So each of these ended the naming gate in a traceback, exit 1: - a keyed source under a parent this user cannot enter; - `SHAPE_PIN_SOURCE_<LINK>` pointing there; - a checkout `../openXdox` at mode 000, which every `--project` run asks for; - a 300-character name. **What changed.** **How a value is read** (`LinkSources` in `scripts/validate-repository-naming.py`). This mirrors #312's `PinSources`, with a chain link where that file has a declared pin: - The text before the first `=` is a LINK only when it casefolds to a link of a chain this run reads. Those are the entries of `--referent-chain`, and of each leg's `naming.referent_chain:` under `--project`. Both are read before any leg is classified. - The keyed reading wins even over a directory spelled like the whole value. `./` reads that directory. - Any other value is a bare PATH, taken whole. **What a lookup says** (`scripts/repo_shape.py`): - `link_manifest_lookup` asks `stat()` inside a guard, as `_digest_from_source` does in #312. It returns the system's reason when a source cannot answer, or says which of four plain cases it is (`no such file or directory`, `it holds no project.yaml`, `its project.yaml is not a file`, `it is neither a directory nor a file`), and it never raises. It replaces `link_manifest_path`, which is gone: nothing calls it. `link_declarations` gives the reason a manifest could not be read, and `declared_neutral_pins`, which it left with no caller, is removed (fix round 1). - `resolve_link_source` and `link_pins_from_trees` take a keyword-only `tried`. It records each source asked, in order, with its reason. Nothing in `repo_shape` prints. - `link_pins_from_trees` also takes a keyword-only `with_reasons` (fix round 2). Asked, it returns a `LinkPins`, a dict equal to the plain mapping that also remembers why a tree it reached could not be read. Only the scaffold asks. **What the validator prints** (stderr, after each name's WARNINGs): - `WARNING link-source-unreadable: <path> (<how>) could not answer for <link>: <reason>`. This covers a `--link-source` asked for a link that holds a pin, a `SHAPE_PIN_SOURCE_<LINK>` likewise, and a checkout beside the project that exists and still cannot answer. A keyed value that also names a directory gets a second line saying how to ask for that directory with `./`. A bare value holding `=` says it was read as a PATH. - `NOTE --link-source <typed>: not used — <why>.` for a value no such link asked for. The why is one of five: a later value replaced it; no name reads that link's own declaration (a keyed value); no name records a chain at all; no name reads any chain link's own declaration (a bare value, where chains exist and none is read); or every link that is read had a more specific source. - Three kinds of source are never named: a variable or a checkout asked only for a chain's LAST link (no one ever reads that link's own declaration); a variable or a checkout for a chain no classification reads (fix round 1: a spec leg's chain, a name with no referent, a chain whose shape fails; a `--link-source` value for such a chain is the NOTE); and a checkout that is absent or holds no manifest. - `--quiet` drops both, with the WARNINGs. So `make bootstrap` and the doctor's `naming` row print nothing new. `--help`, the module docstring and `docs/cli.md` (regenerated) say all of this. **Fix round 1 (lane 5's review of 40762ca: 0 blockers, 2 should, 4 nits).** Second commit, 4b21263. Each review finding, in order: - **A link manifest whose `neutral_product_pins:` is not a list of names is a tree that cannot answer** (SHOULD-1). `link_declarations` iterated `data.get(key) or []` as it stood, against the `link_pins_from_trees` docstring this change wrote ("nothing a source does raises out of here"). `5` ended the naming gate, the scaffold and the project's `validate-manifest.py` in `TypeError: 'int' object is not iterable`. A scalar `openDox` was read as the characters `D`, `e`, `n`, `o`, `p`, `x`, so the chain broke, and under `--project` that is a FINDING. A mapping was read as its keys, so `openDox: yes` DECLARED `openDox`. Each is now `(None, why)`, as `not a mapping` is: declared-unverified, with the reason named ("`neutral_product_pins` is not a list", "holds an entry that is not text"). An absent or null value, an empty list, and a blank entry (`null`, `''`) in a list, declare nothing, as ever. The fix is in the shared function, so the scaffold and `validate-manifest.py` are reached too. **Three behaviours change on purpose here, and the tests of fix round 2 pin each:** - **A falsy value is no longer an empty list.** `neutral_product_pins: 0`, `false` or `''` was read as no pin at all (`or []`), so a chain through the tree was BROKEN, and under `--project` that is a FINDING. It is now a tree that cannot answer: the link is declared-unverified, a WARNING, exit 0. - **An entry that is not text is no longer read as text.** `[openInk, 0]` and `[openInk, false]` dropped the entry as a blank, and `[openInk, true]` read it as the text `True`, so a chain through the list VERIFIED. Each is now declared-unverified, with a WARNING saying the list "holds an entry that is not text". That is stricter than the link tree's own `validate-manifest.py`, which takes `str(pin)` of an entry (`_declared_pins`, `templates/assembly-root/scripts/validate-manifest.py:85`): a chain is better left unverified than verified through a name nobody wrote. - **The scaffold no longer refuses `chain-not-declared` over a pins value that is a scalar.** With `--pin-source` a tree whose `neutral_product_pins:` is the scalar `openInk`, the scaffold used to refuse (`REFUSED chain-not-declared: ... openXdox declares I, e, k, n, o, p, not openInk`, exit 2, from the characters). It is now the declared-unverified WARNING and the scaffold goes on, exit 0, which is its documented rule for a tree that cannot answer: "a warning, never a refusal, because the check is offline". The warning says the tree was reached and could not be read (fix round 2, below). - **The reach wording is corrected** (SHOULD-2): "Who else this reaches" below, and the docstring of the test that made the claim. What stops crashing is the naming and manifest steps. `make validate`, `validate.yml` and `make bootstrap` still end in a traceback on the sibling row before Python 3.14, at a file this change does not touch (below). On 3.14 the sibling is passed over without a word there instead, which is #430. - **The mutation gaps** (nit). Six of the review's ten mutations survived. Five now die, each by a new test: a `project.yaml` that is a directory; a device or a FIFO as the source; the order A, B, A; a value holding `=` with no chain at all; and every failure of `SHAPE_PIN_SOURCE_<LINK>` for a link that holds a pin (a path that is not there, a directory with no manifest, a file that is no manifest), not only a permission. The sixth, the `typed in self.used` test in `unread_lines`, could not be killed because it was dead: a value is read as one link's keyed source or is the one bare PATH, so every lookup asks it of the same path and it is used or unread, never both. That was asserted over 293 tests and the test is removed. - **A chain no classification reads** (nit). `classify` consults a link's own declaration only for a name that claims a referent, with a recorded chain whose shape holds. For any other chain (one recorded on a spec leg; one on a name with no `open<Product>` to reach; one that does not begin at a held pin or does not end at a referent of the name) a source was still asked, so a path that was not there was reported as `link-source-unreadable` and a good tree was counted used, each for a question nobody put. `NamingPolicy.reads_link_declarations` now says when `resolve_referent` reads a chain, and `LinkSources.link_pins` asks it first: such a chain asks nothing, and a value for it is the NOTE that no name reads that link's declaration. A test runs `resolve_referent` with a mapping that records whether it was read, over every way a chain is and is not, and holds the new method to it. - **Dead code and stale sentences** (nit). `declared_neutral_pins` is removed: `git grep` over the tree, every pushed branch and every sibling worktree finds no caller. `link_manifest_lookup`'s docstring no longer says "replaces" over a name that is gone. This description no longer says `tests/test_naming_policy.py` takes `SEARCH_DENIED` from #312: it defines a mark of its own, with the same condition and its own reason text. **Fix round 2 (lane 5's re-review of 4b21263: 0 blockers, 0 should, 3 nits).** Third commit, 2ac911c. Each finding, in order: - **The three behaviours the body did not state are stated and tested** (nit 1). Round 1's not-a-list fix changed three behaviours on purpose, each listed under SHOULD-1 above, and the re-review found two of them had no test. R1 (`if declared is None` read as `if not declared`, which makes `0`, `false` and `''` declare nothing again) and R2 (`if pin is None or pin == ""` read as `if not pin`, which drops a `0` or `false` entry as a blank) both passed all 210 cases of `tests/test_naming_policy.py` at 4b21263. Six rows join `NOT_A_LIST_OF_NAMES`, which runs at the CLI and in process: `0`, `false` and `''` as the value, and `0`, `false` and `true` among the names. R1 now fails 9 cases and R2 fails 6. - **The scaffold's warning is true for a tree it reached** (nit 1, the third behaviour). Over a `--pin-source` whose `neutral_product_pins:` is not a list, round 1 turned the `chain-not-declared` refusal into a declared-unverified WARNING that says the tree "is not reachable here (a sibling checkout, SHAPE_PIN_SOURCE_<LINK>, or --link-source <LINK>=<path> would let it be read)". `--pin-source` did reach the tree, and only its declaration could not be read, so that blamed a path that was right. Now `link_pins_from_trees` takes a keyword-only `with_reasons`; asked, it returns a `LinkPins`, a dict equal to the plain mapping that also keeps `unreadable`, `{link: why}` for each link whose source answered and whose manifest could not be read. `_chain_link_findings` words such a link "which was reached but could not be read as a declaration (<why>)", in a branch of its own, and only the scaffold asks. The new text is asserted for each of the eleven not-a-list rows, with `scaffold-project.py --dry-run` run as a subprocess. - **Every other path's text is the bytes it was.** The sentence for a link no source reached is the same code in the same lines, and four tests hold it: the generic warning for a plain mapping, for an empty `LinkPins` and for an absent tree; the scaffold's own old warning for a clone that holds no `project.yaml`; the naming validator's two lines (its warning, and the `link-source-unreadable` line that names the reason); and `validate-manifest.py`'s. The naming validator and `validate-manifest.py` do not ask for reasons, so they print what they printed. - **One more scaffold path changes with it, on purpose.** A tree the scaffold reached and could not read as a declaration for any other reason (a manifest that does not parse, or is not a mapping) used to say "not reachable here" too, and was no truer. It now says what it is, with the system's reason, and a test for each of the two (a manifest that is not a mapping, one that does not parse) holds it, at the scaffold's command line and in process: narrowing the wording to the not-a-list reasons fails both. - `_declaration` still returns its four values, so nothing else in `scaffold-project.py` moved. - **A qualified chain link in the spy test** (nit 2). `_chain_read_cases` had no case whose chain link is qualified, so `reads_link_declarations` could take the chain as typed and not through `_chain_links` (R3) and pass every test. With `--referent-chain testorg/openXdox,openDox` that turns a verified chain into a declared-unverified one. `("codexDox", {"openXdox"}, ("testorg/openXdox", "openDox"), True)` is a case now, and R3 fails it. - **The description** (nit 3): the ten corrections of the re-review are applied (the reason a lookup returns, the fifth NOTE reason, the third kind of silent source, the SHOULD-1 bullet, "before Python 3.14", the `validate-pins.py` bullet, the 3.14.7 rows, the merge-tree section, the filed numbers, and the project's own `validate-manifest.py`). The mutation proofs: all three survivors were reproduced first at 4b21263 (210 passed each) and each dies at 2ac911c. Six mutations of the new code each die too: the warning ignoring the reasons (12 cases fail), the lookup recording none (12), the scaffold not asking (11), `with_reasons` on by default (3), an absent tree called reached (3), and the reached sentence used for a tree nothing reached (17). **Where it departs from #312, and why.** - **An explicit source that cannot answer stays a WARNING, exit 0.** #312 made an unreadable `--pin-source` a finding. Three reasons for the difference: - Exit 1 here means "a name is wrong", and this module has always said an unread link tree is a warning. - `SHAPE_PIN_SOURCE_<PRODUCT>` also answers `validate-pins.py`'s digest question, from a checkout that need not hold a manifest. - The lookup falls through to the next source, which may still verify the link. - **The fall-through is kept**, as it always was. With `--pin-source`, a keyed value is final. - **The lines go to stderr, spelled `WARNING`/`NOTE` as this file spells them.** #312's notes are ` note` lines on stdout, where that file's report lives. **Who else this reaches.** The guard is in `repo_shape.py`, so the same `link_pins_from_trees` lookup stops crashing for these callers too: - **The project's own `validate-manifest.py`.** It reaches the lookup through `SHAPE_PIN_SOURCE_<LINK>` or a sibling checkout, and now gives its usual declared-unverified WARNING instead of the traceback. - **`scaffold-project.py --referent-chain`.** A `SHAPE_PIN_SOURCE_<LINK>` under a locked parent no longer kills the plan, and `--pin-source` verifies the link. Neither of these prints the reason a source could not be entered; the scaffold says it only for a tree it reached and could not read (fix round 2, above). A `--pin-source` the scaffold cannot enter is still a traceback: that is #339 (`pin_digest_from_source`), and this PR does not touch it. **What this does not reach, and what the doctor says.** - **The project's gates still end in a traceback on the sibling row.** The NAMING and MANIFEST steps of `make validate`, `validate.yml` and `make bootstrap` stop crashing for a checkout `../openXdox` at mode 000. Those targets also run `validate-pins.py`, whose `resolve_neutral_pin_source` asks `(sibling / ".git").exists()` with no guard (`templates/assembly-root/scripts/validate-pins.py:463` on main), so before 3.14 a sibling at mode 000 still ends them in a `PermissionError`. That file is #312's, and `fix/pin-source-env-collisions` (PR #409, since landed), `fix/adopt-pin-writes-pin-file` and open PRs #343 and #419 touched it when this was written, so this PR does not edit it. The same line is listed for Python 3.14 by #430 (lane 2), where the helper that would cure both halves is asked for; the pre-3.14 traceback reproduction is added there as a comment, not filed apart. A `SHAPE_PIN_SOURCE_<LINK>` under a locked parent is different: `validate-pins.py` makes that a `neutral-pin-source-unreadable` FINDING (#312), and that stays. - **The doctor's `naming` row** runs the STANDARD's copy of the validator, so it stops crashing at once. **Its `manifest` row** runs the project's OWN `validate-manifest.py` on the project's own `repo_shape.py`, so it changes only once the project has applied this change. Over an existing project the doctor says `INVALID (manifest, pins)` until `update-shape.py apply` lands the two `upstream-changed` rows below. - **The verdict.** For `SHAPE_PIN_SOURCE_<LINK>` under a locked parent, on a project that carries these copies, `INVALID (naming, manifest, pins)` becomes `INVALID (pins)`, and `pins` is #312's finding for that locked variable, which is right. For a sibling at mode 000 the `pins` that remains is the traceback above, not a finding. The two copied scripts this change edits (`scripts/repo_shape.py` and `scripts/validate-repository-naming.py`) are copied into every project and pinned there. A project takes the change through `update-shape.py` (two `upstream-changed` rows), and a family holder takes the `repo_shape.py` row. Neither file has a second copy in this repository. **Evidence.** - **Issue rows, main ab623dd** (`repro338.sh`, a `gh` stub first on PATH). Under 3.10.21 and 3.12.3, rows 4-7 (keyed under a mode-000 parent, `SHAPE_PIN_SOURCE_OPENXDOX` there, a sibling at mode 000, a 300-character name) are a traceback, exit 1. Rows 2-3 (`a=b`, the typo) are a silent declared-unverified, exit 0. Under 3.14.7 row 1 verifies, and rows 2-7 are the silent declared-unverified WARNING, exit 0. - **Issue rows, this PR.** Under 3.10.21, 3.11.16, 3.12.3, 3.12.14, 3.13.15, 3.14.7 and 3.15.0rc2 (first commit), all 7 rows exit 0 with no traceback, and the output is byte-identical across versions. `a=b` verifies. The other five rows each name their source with its reason. Re-run on the fix-round head, 4b21263, under 3.10.21, 3.12.3 and 3.14.7: byte-identical to the first commit's run. Re-run on the fix-round-2 head, 2ac911c, under the same three: all 7 rows exit 0 with no traceback, the output is byte-identical across the three versions, and it is identical to the re-review's runs of the previous head over main. - **Reach on main.** Main gives a `PermissionError` traceback, exit 1, for `scaffold --dry-run`, the project's `validate-manifest.py`, `validate-repository-naming.py --project` and its `--quiet` form, each with `SHAPE_PIN_SOURCE_OPENXDOX` under a locked parent. This PR gives exit 0 for all four. `validate-pins.py` on the sibling row is the exception said above. - **New tests.** 17 cases (11 functions) in the first commit; 16 fail on ab623dd. The fix round adds 31 cases in `tests/test_naming_policy.py`, and 17 of them fail on 40762ca: the five shapes of pins that are not a list (each at the CLI, in-process and, for the scalar, under `--project`), and the chains nobody reads. The other 14 guard what a mutation could break (below) or the side the fix must not move (an absent, null or blank pin list still answers). - **Targeted run on 4b21263**, files `test_naming_policy`, `test_manifest_validator`, `test_scaffold_referent_chain_links`, `test_cli_reference`, `test_repo_hygiene`, `test_neutral_pin_validator`, `test_scaffold_e2e`, `test_shape_doctor`, `test_git_output_decoding` and `test_pin_validator`: **765 passed, 3 skipped**. The 3 skips are the PowerShell-only rows. (40762ca: 734 passed, 3 skipped.) `render-cli-reference.py --check` is ok. - **Mutation probes, first commit.** Reverting the stat guard, the parse, keyed-over-directory, the NOTE lines, the chain's-end restriction, the sibling-absence filter, the manifest reason, the `--quiet` drop, the "read as a PATH" sentence, the `./` line, the project-chain read, the WARNING lines or the env reporting each fails at least one new test. - **Targeted runs on 2ac911c (fix round 2).** Under 3.12.3, `test_naming_policy` (238 cases: 210 and 28 new), `test_cli_reference`, `test_manifest_validator`, `test_scaffold_referent_chain_links`, `test_repo_hygiene` and `test_setup_project_py`: **468 passed**; and `test_scaffold_e2e`, `test_scaffold_pin_and_reuse`, `test_scaffold_leg_path_collisions`, `test_scaffold_pin_collisions`, `test_neutral_pin_validator`, `test_git_output_decoding` and `test_pin_validator`: **312 passed**. Under 3.10.21 and under 3.14.7, `test_naming_policy`, `test_manifest_validator` and `test_scaffold_referent_chain_links`: **321 passed** each. `render-cli-reference.py --check` is ok, and `ast.parse(feature_version=(3, 9))` passes on the three changed files. - **Mutation probes, fix round.** The review's ten, re-run on 4b21263: MW1, MW2, MW3, MW5 and MW11 now die (they survived), and MW4, MW7, MW12 and MW13 still die; MW6 is the dead test, removed. (Fix round 2 kills the three the re-review found, R1 to R3, and six more: see above.) Nine more for the new code (the list check, the entry check, the null and blank tolerances, `reads_link_declarations` always true, without the shape check, without the held pins, and the validator ignoring it): eight die. The ninth, `reads_link_declarations` ignoring whether the name has a referent, survives as an equivalent mutant: with no referent `_chain_shape_problem`'s last question always reports a problem, so the explicit early return is redundant, and is kept because `resolve_referent` has the same first line. The spy test holds the two together. - **Windows.** The mode-000 rows skip on Windows and as root, using a `SEARCH_DENIED` mark this file defines, with the same condition as #312's. The new FIFO and `/dev/null` rows skip where those do not exist. `a=b` and the 300-character name run everywhere. On Windows the long-name row asserts the line, not POSIX's reason. None of it had been run on Windows or macOS when this was written; the last paragraph gives the CI result. **Merge-tree.** Recomputed at 19:04Z with `git merge-tree --write-tree` of 2ac911c against `origin/main` as it was then, 53bee5b, and against every one of the other 37 pushed branches that touches a file this PR changes (a scan of each head against its merge-base with main): - **Clean:** origin/main 53bee5b (it carries #314 and #390); #394 `fix/adopt-record-submodule-placement` 563ea16 (`scripts/repo_shape.py` and `validate-manifest.py`) and #406 `fix/validate-family-unnamed-mounts` 369af2e (`scripts/repo_shape.py`); #397 abb27e6, #400 7062a47, #401 409f10b, #416 7cca975, #452 e2df9a8 and #395 1ef3310 (`docs/cli.md`); #355 6fdbe3d, #373 741cbf2 and #395 (`scaffold-project.py`). - **Conflict, four hunks each, all in `scripts/repo_shape.py`:** `fix/pin-source-env-collisions` (PR #409) 6494714, and `fix/adopt-pin-writes-pin-file` 421e4e5 (no PR). Both also touch `scaffold-project.py`, and that merges clean with this PR's one edit there. Fix round 2's new hunks merge with neither: #409's change to the "not reachable here" sentence (the `offered` text, when a variable is shared) and the reached branch sit in different lines of `_chain_link_findings`. The four hunks, each taking both sides: 1. the `resolve_link_source` signature: `default_source=None, env_shared: bool = False, *, tried: list | None = None`; 2. its docstring: both paragraphs; 3. the `link_pins_from_trees` docstring and the lines after it: both paragraphs, then `found`, this PR's `unreadable` and #409's `links` and `shared`; 4. the call: `resolve_link_source(name, root, keyed_sources, default_source, pin_product_key(name) in shared, tried=asked)` (the same spelling on both branches), followed by `if tried is not None: tried[name] = asked`. Resolved that way in a throwaway and committed there, the merge with 6494714 passed **496 tests** (`test_naming_policy`, `test_pin_source_env_collisions`, `test_neutral_pin_validator`, `test_scaffold_referent_chain_links`, `test_manifest_validator`, `test_cli_reference`), and the merge with 421e4e5 passed **559 tests** (the same files and `test_pin_source_unreadable`). `render-cli-reference.py --check` is ok on both, pyflakes is clean, and the grammar parses as 3.9's. Neither branch carried the resolution when this was measured. #409 has since landed, and the merge of main into this branch (the last paragraph) takes those four hunks both ways. The earlier counts (468 and 434 from the first fix round, 430 from the re-review) are those runs' own, at their heads. **Left out.** Each of these was found by lane 5's reviews of this change, and each is filed or folded: - **A bare `--link-source` answers for every link with no more specific source** (#441). In a chain of three or more links, one bare tree is read as two links' declarations, and a chain that holds is reported broken, a FINDING for a name with no direct pin on its referent. Pre-existing, the same on main; `validate-pins.py`'s `--pin-source` has the same documented rule and was not examined. - **An empty path after a key (`--link-source openXdox=`) is `Path("")`, the current directory** (#442). Run from a project root, the project's own `project.yaml` is read as the link's declaration. Pre-existing; `validate-pins.py`'s `--pin-source openXdox=` has the same shape (there a finding naming `.`). - **`validate-pins.py`'s `(sibling / ".git").exists()` is unguarded** (the sibling row above), folded into lane 2's #430, which already covers the Python 3.14 half: the pre-3.14 traceback and its reproduction are added there as a comment. #312's file. - **The project's OWN manifest** with `neutral_product_pins: 5` ends in a `TypeError` in two gates before either can say anything about it (#443): `_pins_from_project` in the naming gate (`scripts/validate-repository-naming.py:302` on main), and `_declared_pins` in the project's own `validate-manifest.py` (`templates/assembly-root/scripts/validate-manifest.py:85`), which ends it before its own `manifest-neutral-product-pins` finding (`_neutral_product_pins_type_finding`, `:524` on main) can print. Reproduced on 3ef059c. That is the project's own declaration, read by the project's own gates, and not a link source, so it is outside this change; "read by the project's own gates" covers two crashing gates. Also not done here: - **Python 3.9 (the floor) was not run** because it is not installed here. The grammar of the three changed files was checked with `ast.parse(feature_version=(3, 9))`. Windows and macOS were not run here; CI ran them (the last paragraph). Reviewed adversarially at 40762ca, the branch's first commit (CHANGES NEEDED: 0 blockers, 2 should-fixes, 4 nits). The head stopped every crash in the issue's table, and the should-fixes were a docstring claim the code did not keep for a `neutral_product_pins:` that is not a list, and reach claims that overstated what the project's gate does on the sibling row. Fix round 1 (4b21263) resolved every finding, and the re-review of 4b21263 read APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits: three behaviour changes this description had not stated, two of them untested; a spy-test case for a qualified chain link; and description corrections). Fix round 2 (2ac911c) resolved those three, and its re-verification of 2ac911c read APPROVE WITH NITS (0 blockers, 0 should-fixes, 2 nits: no test of the scaffold's wider wording for a manifest that is not a mapping or does not parse, now pinned by two tests that each fail when the wording is narrowed, and a cross-reference in this description that pointed the wrong way, now corrected). Main (9736424, #409's squash) was merged into the branch as ced8fac. `scripts/repo_shape.py` conflicted in four hunks; each takes both sides, as the review of 2ac911c resolved it: `resolve_link_source(..., default_source=None, env_shared: bool = False, *, tried=None)`, both docstrings kept, `link_pins_from_trees` opening with `found`, `unreadable`, `links` and `shared`, and the call passing `pin_product_key(name) in shared` with `tried=asked`. Nothing else conflicted. After the merge, `tests/test_pin_source_env_collisions.py` and the targeted set passed (667 and 312), and `render-cli-reference.py --check` printed `[ok]`. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 938a131 (main 48585e6 merged in). Its merge with main 994be39 passed the targeted set (the naming, manifest, scaffold, pin and setup test files): 983 passed. Closes #338 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… writes its pin file as the scaffold does, so an adopted root passes `validate-pins.py` (#273); a `--pin-source` that cannot answer is `pin-source-unreadable` (#339) (#508) **Brought up to date after #409 squash-landed** by merging origin/main (9736424, #409's squash) as f9ed46f. Its `scaffold-project.py` conflict was resolved to this branch's side once `git diff b70a07d 9736424 -- scaffold-project.py` (#409's final head against its squash) came out empty. So the file is the one tested on this branch plus main's own later changes to it, and holds nothing of #409's; details below. This branch carries #275's adopt half. It was built on `fix/pin-source-env-collisions`, PR #409 (#275's scaffold half and its fix round), merged at 6494714 as 583f647, over the earlier 5690235 (34dbd86). After 6494714, #409 only merged main, five times, up to its final head b70a07d, and none of those merges has a hand edit (`git show --remerge-diff` is empty for each). Adopt's refusal of two products that share a source variable goes through #409's function, so this branch lands after #409. It was never rebased. `git diff b70a07d 9736424` is empty for each file #409 touched: `scaffold-project.py`, `scripts/repo_shape.py`, `templates/assembly-root/scripts/validate-pins.py` and `tests/test_pin_source_env_collisions.py`. What f9ed46f resolves, four files with one hunk each: - **`scaffold-project.py`.** - The conflict hunk is main's block from `PIN_COLLISION_REMEDIATION` through `_pin_collision_remediation`, which #409's squash edits: it adds `PIN_VARIABLE_REMEDIATION`, `_colliding_pin_variables` and `_pin_collision_remediation` and changes `_refuse_colliding_pins`. This branch moved all of that to `scripts/shape_materialize.py`. The hunk takes this branch's side. (The merge commit's message says "re-adds"; this is the exact account.) - #409's auto-merged import of `PIN_SOURCE_ENV_PREFIX, pin_source_env_collisions` is dropped, because nothing in the file uses either name any more. - It is not `git checkout --ours` of the whole file. Main changed the file beyond #409 since this branch last merged it: #355's `--tracking-branch` check (6c2a371) and a73b4e9's `RULESET_HINT`. Those lines stay. - The result is the file `git merge-tree` gives for this branch with main before the squash (518010b). Its diff from the file tested here has the same patch-id as main's own 3ef059c..518010b change to it. - **`templates/assembly-root/scripts/validate-pins.py`.** Main's side, which is #409's final blob, with main's #359 workflow reader in it. This branch had no change of its own to the file beyond #409. - **`adopt-project.py`, `_commit_the_split`.** - Main: #445 wraps the function in its late-failure `try`, and #394 builds an `append` with the plan's submodule record. - This branch: its only edits there were a comment and `neutral_pins=_recorded_pins(plan)` on the materializer call. - Both now sit on main's call inside the `try`: `append=append, neutral_pins=_recorded_pins(plan)`. - **`docs/cli.md`.** It is regenerated with `scripts/render-cli-reference.py`, and `--check` is ok. It differs from main's only in the `--pin`, `--pin-owner` and `--pin-source` help of `adopt-project.py plan`. `scripts/repo_shape.py`, `templates/assembly-root/scripts/validate-pins.py` and `tests/test_pin_source_env_collisions.py` are main's blobs, which are #409's: this branch adds nothing to them. Nothing of #409 is lost. Its scaffold behaviour is `shape_materialize.refuse_colliding_pins` and `pin_variable_collisions`, keyed by #409's `pin_product_key`. **73d65c6, after the merge.** - **The change.** `_read_pins` reads `plan.get("pins")` where it read `plan.data.get("pins")`. - **Why.** #445's `tests/test_adopt_late_failure.py` hands `_commit_the_split` a `SplitPlan` as its plan, a `dict` that adds `entries`. On f9ed46f, three of its tests stopped at "committing the split" with `AttributeError: 'SplitPlan' object has no attribute 'data'` (3 failed, 83 passed in that file). - **Nothing else changes.** `Plan.get` with no default returns `plan.data.get(key)` exactly, so a loaded plan reads as before. `adopt-project.py plan --pin` took a NAME only (`--pin openGlass`) and refused a trailing `@<commit>`. `plan`, `check` and `execute` each exited 0, and `execute` printed `adoption verified`. The manifest then listed the name in `neutral_product_pins:` with no `contracts/<product>-pin.yaml` beside it, and on its first run the adopted root's own `validate-pins.py` refused it as `neutral-pin-missing`. #273's table, reproduced on c7c02fa with the issue's own commands (local bare remotes, and a stub `gh` that exits 97): | `--pin` | c7c02fa | this branch | |---|---|---| | `openGlass` | `execute` exit 0, `- openGlass`; clone's `validate-pins.py` exit 2 `neutral-pin-missing` | `plan` exit 2 `pin-malformed`, with the scaffold's remediation (`--pin openGlass@<40 hex>`); no plan is written | | `opensoft/openGlass` | exit 0, `- opensoft/openGlass`; exit 2 `neutral-pin-missing` for `contracts/opensoft/openglass-pin.yaml` | `plan` exit 2 `pin-malformed` | | `spec` | exit 0; `validate-pins.py` exit 0, reading the leg's `contracts/spec-pin.yaml` as the neutral pin | `spec@<sha>` is `adopt-pin-collides` at `plan` | | `shape` | exit 0; `contracts/shape-pin.yaml` read as the neutral pin | `shape@<sha>` is `adopt-pin-collides` at `plan` | | `openGlass@<sha>`, `opensoft/openGlass@<sha>` | `plan` exit 2 `adopt-pin-malformed` | `plan`, `check` and `execute` exit 0, and `gh` is called 0 times. `contracts/openglass-pin.yaml` is written and the manifest lists `- openGlass`. In the clone, `validate-manifest.py` exits 0, and `validate-pins.py` exits 0 with `openGlass: pin commit … digest recomputes` | Brett ruled for decision 1's first option ("go with your recommendation on #273"): adopt takes the scaffold's pin and writes the scaffold's pin file. That reverses the NAME-only `--pin` from #6, and the comment that described it is replaced with the new contract. - **One implementation.** The scaffold's pin code moved into `scripts/shape_materialize.py`, next to `neutral_pin_path` and the materializer that writes the file. What moved: - `PIN_ARG_RE`, `parse_pin`, `pin_digest_from_source` and `pin_digest_from_gh`; - `declared_pin_values`, `declared_pin_products` and `PIN_COLLISION_REMEDIATION` (in #309's wording); - from #275's branch, `PIN_VARIABLE_REMEDIATION` and the variable check; - `_refuse_colliding_pins`, split into the pure `pin_collisions` and `pin_variable_collisions`, and `refuse_colliding_pins(refusal=...)`, which raises one refusal for both. The code did not move verbatim. `_refuse_colliding_pins` became the three functions above; `pin_digest_from_source` was rewritten for #339 (below); and fix round 1 changed three of them (below): `pin_variable_collisions` keys each pin by #275's `pin_product_key`, `pin_digest_from_source` spells a path that is not printable, and `pin_digest_from_gh` refuses a missing `gh`. The scaffold's behaviour is otherwise unchanged. `scaffold-project.py` imports what it calls under its old names. Lane 5's review ran 34 scaffold cases (every pin refusal, the `gh` stub's four modes, dry and real successes) on 34dbd86 and on this branch at 7a94b6b. rc, stdout, stderr, the `gh` log and the files made were identical, apart from the shape's own commit and digest and #339's row. Its pin tests and #275's pass as they are. `tests/test_git_output_decoding.py`'s exemption for `pin_digest_from_gh` follows the function to its new file. - **`plan`.** `--pin`, `--pin-owner` and `--pin-source` mean what they mean for the scaffold. `plan` reads the digest from `--pin-source` or `gh api`, and records product, repository, commit, digest and `digest_source` in the plan, so `check` and `execute` read no network for a pin. `plan` refuses: - a name with no `@<commit>`, as `pin-malformed`. That includes an empty `--pin ''`, which adopt used to drop and now refuses, as the scaffold always has; - `spec`, `code` and `shape`, in any case, as `adopt-pin-collides` (#273 decision 4), in the scaffold's sentence and remediation (#241, #309); - one product given twice, the same way, whether it is a repeat or two names that share one file (decision 5); - two products that one `SHAPE_PIN_SOURCE_<PRODUCT>` names, such as `open-glass` and `open_glass`, the same way, through #275's check; - a leg path over a pin's file, as `adopt-leg-path-collides`; - a `--pin-source` path that `check` would refuse in `digest_source:` (fix round 1), as `adopt-unwritable-value`. It is asked only when a `--pin` records the path; - with no `gh` on PATH and no `--pin-source`, `gh-not-found` (fix round 1). - **`check` and `execute`.** These are new `check` findings, and `execute` refuses each one before any leg exists. They come from `_leg_findings`, the list `check` and `_refuse_what_check_finds` share: - `plan-pin-unrecorded`: a name alone (an old plan's `pins: [openGlass]`, or a hand edit), or an entry missing its commit or digest; - `plan-pin-malformed`; - `plan-pin-collides`, which covers two pins at one file, a pin at a file the shape writes, and two products that share one source variable; - `plan-root-holds-pin`: a source path kept in the root at the pin's file. The materializer would write the shape's pin beside it under `shape/`, and `validate-pins.py` would read the source's file instead. How each finding ends: - `plan-pin-unrecorded` and `plan-pin-malformed` end with the `plan --pin <product>@<40 hex commit>` that records the pin whole. - `plan-pin-collides` ends with re-running `plan` with each product once, or, for two products that share a source variable, with one product of each such group. - `plan-root-holds-pin` ends with the kept path's own remedy: send that entry to a leg, `drop` it, or rename it in the source and re-run `plan`. A leg moved over a pin's file in the plan is `plan-leg-path-collides`. - **Also part of #386: its `pins:` row.** A hand-edited `pins:` that is not a list (a name such as `pins: openGlass`, a number, a mapping, a quoted string) is now `plan-pin-malformed` in `check` (`` `pins:` is 'openGlass', not a list of pins ``), and `execute` refuses it before any leg exists. - Where: `_read_pins` in `adopt-project.py`; the test row is `plan-pin-malformed-not-a-list` in `tests/test_adopt_pins.py`. - Before: on main, `check` said `plan ok` for the name, the mapping and the string, and gave a traceback for the number (reproduced by lane 5's review on cb7063f). #386 records that `execute` then wrote the name as eight one-letter pins. - Not fixed here: #386's other keys, `allow_empty_legs:`, `paths:`, `legs:` and `source:`, so #386 stays open. Lane 2's #435 takes all of #386, `pins:` included, and its `pins:` rule keeps only text items where this branch writes mappings; whichever lands second reconciles them (#435 (comment), and "Where the meaning meets" below). - **`execute`.** It writes each pin through `materialize_assembly_root(neutral_pins=...)`. The manifest lists the product with the owner stripped (decision 3), so the entry and the file agree. - **Docs.** `--help` is updated and `docs/cli.md` regenerated (`render-cli-reference.py --check` ok). `pin_digest_from_source` caught only `Refusal`. A directory that is no clone is git's own failure, so it was always `pin-source-unreadable`, exit 2. A path `subprocess.run` cannot enter never reaches git: it is a `FileNotFoundError`, `NotADirectoryError` or `PermissionError` out of `cwd=`, and nothing caught it. `scaffold-project.py --pin ... --pin-source <that>` ended `--dry-run` and the real run alike in a traceback, exit 1. Since the move above, `adopt-project.py plan --pin` reads its digest through the same function and ended the same way. Both reach it through `declared_pin_values`, and `check` and `execute` read no source, so that call is the only reader. The issue's table, run on ab623dd for the scaffold and on d806e8f for `adopt-project.py plan` (adopt has no `--pin-source` on ab623dd), then on this head. `gh` is a stub that logs and exits 97, with `GH_TOKEN` bogus and an empty `GH_CONFIG_DIR`; it was called 0 times in every run: | `--pin-source` | before | this branch | |---|---|---| | a directory that is not a clone | `pin-source-unreadable`, exit 2 | unchanged | | a path that does not exist | `FileNotFoundError` traceback, exit 1 | `pin-source-unreadable`, exit 2: `<path> could not answer for opensoft/openGlass @ <40 hex>: no such directory` | | a file | `NotADirectoryError`, exit 1 | exit 2, `... it is a file, not a directory` | | under a parent this user cannot enter | `PermissionError`, exit 1 | exit 2, `... Permission denied` | | a symlink loop (not in the issue) | `RuntimeError` out of `Path.resolve()` on 3.12, exit 1 | exit 2, `... Too many levels of symbolic links` | The real run and `--dry-run` give the same stdout and stderr, and nothing exists afterwards (no remote directory, no work directory; for adopt, no plan file). What changed, in `scripts/shape_materialize.py` only: - `pin_digest_from_source` catches `OSError` as well as `Refusal` and raises the existing `pin-source-unreadable` with its remediation unchanged. The message names the path and the reason. - `stat()` judges the source inside the guard, as #312 does in `validate-pins.py`, and the sentences are that file's: a missing path, or one beneath a file, is "no such directory"; a file is "it is a file, not a directory"; any other `OSError`, from `stat()` or from git's `cwd=`, is the system's own reason (`strerror`). It is not `exists()`/`is_dir()`, which before 3.14 swallow only a missing path. A source `stat()` can read but `cwd=` cannot enter, a directory with no search bit, fails in `tree_digest` and the same guard answers it. - `declared_pin_values` resolves the path through a guarded `_resolved_pin_source`. `Path.resolve()` raises a `RuntimeError` on a symlink loop before 3.13 (an `OSError` since), before the digest was asked for. As typed, `stat()` meets the loop and says so. - A source that answers is read exactly as before: a relative path resolves against the working directory, a symlink to a clone is followed, `digest_source` reads `local-clone (<path as typed>)`, and the digest is `tree_digest`'s. `tests/test_pin_source_unreadable.py` (46 tests in the #339 commit; fix round 1 adds 17 for S1 and moves one, below) runs each case through `scaffold-project.py --dry-run`, its real run and `adopt-project.py plan`: missing, a file, beneath a file, relative and missing, a parent and a source with no search bit (skipped on Windows and as root, by a mark, so before any fixture builds a directory), a 300-character name, a symlink loop (skipped on Windows) and a directory that is no clone. Each asserts exit 2, the refusal naming the path and the reason, its remediation, no traceback and nothing created. It also holds `--dry-run` and the real run to the same stdout and stderr, checks the path is printed escaped under an ASCII stderr and as a surrogate when it is not UTF-8, and checks that a relative source and a symlink to a clone read as before. The session `gh` guard fails any test whose run called `gh`. origin/main ab623dd reached this branch through implQ's branch: 34dbd86 is a30091f merged with ab623dd. One file conflicted there, `templates/assembly-root/scripts/validate-pins.py`, in seven hunks, because #312 (8cdca5e) replaced `keyed_sources` and `default_source` with `PinSources` and #275 added `env_shared` and `shared`. #312's blob is the same at its PR head, at the squash and at ab623dd, so it landed the file implQ resolved against. The resolution keeps #312's side in all seven and adds #275's on top: | # | hunk | resolution | |---|---|---| | 1 | `resolve_neutral_pin_source` signature | #312's `sources: PinSources` and 3-tuple return, plus #275's `env_shared: bool = False` | | 2 | its docstring tail | #312's `Returns (path, how, typed)` text, plus #275's "Rule 2 is skipped when `env_shared`" paragraph | | 3 | `_recompute_neutral_pin_digest` signature | #312's `sources`, plus #275's `shared: frozenset[str]` after `report` | | 4 | the `resolve_neutral_pin_source(...)` call inside it | #312's 3-tuple unpack, plus `env_shared = product.casefold() in shared` and the extra argument | | 5 | `_check_one_neutral_product_pin` signature | #312's `sources`, plus #275's `shared` | | 6 | its call of `_recompute_neutral_pin_digest` | `sources, report, shared` | | 7 | `_check_neutral_product_pins` docstring and body | #312's `PinSources(...)` and `_note_unused_pin_sources`, plus #275's paragraph, `_report_pin_source_collisions(products, report)` and `shared = frozenset(pin_source_env_shared(products))` | This branch's own merge of 34dbd86 (5690235) had no conflict, and neither had its merge of #409's fix round, 6494714 (583f647). That fix round made `neutral-pin-source-collides` a note and keyed "one product" by `repo_shape.pin_product_key`. At 421e4e5, `templates/assembly-root/scripts/validate-pins.py`, `scripts/repo_shape.py` and `tests/test_pin_source_env_collisions.py` were byte-identical to the tree `git merge-tree` gives for 6494714 with origin/main 3ef059c (blobs 5604007, 091966b, 5220787), so this branch added nothing to those three files beyond #409. The 34dbd86 merge brought in #335, #312, #317, #270 (f795cf7), #344 and #349 with no conflict. Earlier merges of origin/main (87373a6, for #309's reworded constant, and e43fa95) are unchanged. Fix round 1 merged origin/main three more times: - **cd1fc79 (2bcfa69).** It brought #245, #351, #350, #345, #372, #371, #354, #380, #377 and the rest since ab623dd, with no conflict. - **45e01f4 (4098b61).** It brought #396's `tests/test_agents_md_codes.py`. That test finds `scaffold-pin-collides` as a string constant of `scripts/shape_materialize.py`, which `SHIPPED` lists. - **3ef059c (421e4e5), #314.** One conflict, one hunk in `adopt-project.py`. This branch's `_refuse_an_unrecordable_pin_source` and #314's section "The project must be able to hold what `execute` writes into it (#261)" were added at the same place; both are kept. Where the two meet, lane 5's earlier recipe applies: - `_flag_plan` builds each pin as the `PIN_FIELDS` mapping `plan` writes, not `list(pins)` of products. So the plan `plan` asks the validator about is the plan it writes. - #314's `test_plan_asks_what_check_asks_of_the_same_keys` plans `--pin openGlass@<commit> --pin-source <stand-in>` where it planned the name alone, which `plan` now refuses. Then f9ed46f, after #409 squash-landed, and 73d65c6 after it (top of this description). On the merged tree exactly one test failed: `tests/test_adopt_template_values.py::test_check_finds_and_execute_refuses_a_hand_edited_value[pins-surrogate]`. It plans, then hand-edits the plan to `pins: ["openX\ud800"]`, a name-only pin with a lone surrogate. Before this branch the refusal's headline was `REFUSED plan-unwritable-value`. A name-only pin in a plan is now `plan-pin-unrecorded` (#273), so `check` has a second, independent finding for that very plan, and the headline is the first finding's: ``` REFUSED plan-pin-unrecorded: 2 finding(s) ... FINDING plan-pin-unrecorded: `pins:` entry 1, 'openX\ud800', is a name with no commit and no digest ... FINDING plan-unwritable-value: a `pins:` item is 'openX\ud800', which `execute` writes into the assembly root's `project.yaml`, and it holds a lone surrogate ... ``` Nothing about #270's contract changed. The lone surrogate is still found as `plan-unwritable-value`, listed under the refusal, and `execute` still refuses before any leg exists. `check` builds `_leg_findings`, where `_pin_findings` sits, before it calls `_value_findings`, so the pin finding comes first. d806e8f changes one assertion for that row, `pins` only: the headline is `REFUSED plan-pin-unrecorded: ` where it was `PLAN_REFUSED`. The other 15 rows and the finding assertions, including the `plan-unwritable-value` line, are untouched. Lane 5's review of 7a94b6b found 3 should-fix and 5 nits. Each is answered here: - **#409's `pin_product_key` (review N1).** The merge of #409's fix round could not carry it, because this branch had moved the function that used it out of `scaffold-project.py`. `pin_variable_collisions` now keys each pin by that function, where it casefolded. `openß` and `openss` are two pin files and one `SHAPE_PIN_SOURCE_OPENSS`, and a casefold named each of the two pins twice. No `--pin` or recorded `pins:` entry `PIN_ARG_RE` admits can spell either, so the test asks the function directly. `refuse_colliding_pins`' docstring now says, in #409's words, that the root's `validate-pins.py` only notes such a pair. - **S1, a `--pin-source` path that is not printable.** `pin_digest_from_source` names the path as typed unless it is not printable (`str.isprintable`), and then as Python's `repr` spells it (`shown_source`). That covers the refusal's head and git's own reason, which names the directory it ran in. Printed raw, an ESC reached the terminal, and a line break or a carriage return began a line of the path's choosing, a forged `REFUSED` line among them, from the scaffold and from adopt. The traceback #339 replaced had shown a `repr`. A printable path is named byte for byte as before. - **S2, no `gh` on PATH.** `pin_digest_from_gh` refuses as `gh-not-found`, exit 2, as `repo_shape.tree_digest_from_gh` names it, and offers `--pin-source`. It was a `FileNotFoundError` traceback, exit 1, from the scaffold and, new with this branch, from `adopt-project.py plan --pin`. - **S3, a hand-edited `commit:` or `tree_sha256:`.** The lane's decision is to accept it and document it (option (c)); no behaviour changed. A pair that disagrees passes `check` and `execute` (`adoption verified`), because neither reads a network for a pin, by design. The adopted root's own `validate-pins.py` then exits 1 with `neutral-pin-digest-mismatch` wherever it can read the product. `check` now says so once, after the pins it will write: "each pin as `plan` read it, not read again here: the root's own `validate-pins.py` recomputes its digest wherever it can read the product". Options (a), `execute` recomputes before it creates the legs, and (b), `check` recomputes from a clone still on the machine, are filed as #449. - **N2, a `--pin-source` that `plan` cannot record.** `plan` asks `check`'s question (`one_printable_line`) of the path it records in each pin's `digest_source:`, before any digest is read. It refuses one that fails as `adopt-unwritable-value`, in #270's words (`unwritable_reason`), or as "an invisible formatting character" where those name nothing. Before, it was planned, exit 0, and printed raw in the report. `check` then refused it as `plan-pin-malformed` and said to re-run the same `plan`. The report's `pin` line and `check`'s `write` line now spell the digest's source with `_shown`. The scaffold half is pre-existing (a line break in the path is written into the pin file; a byte that is not UTF-8 dies after all three repositories exist) and is filed as #450. - **N3, six hunks no test guarded.** Each now has a test that fails when it alone is reverted: - the two name guards (a `pins:` name holding an ESC and a forged line); - the lower-casing of the commit and the digest (an upper-cased hand edit); - the `digest_source` printability check; - the not-text check (`commit: 1234567` unquoted); - the gitlink filter (a submodule kept where the pin is written is `plan-submodule-root` alone). - **N4.** This body. - **N5, #339's third decision.** A `--pin-source` that nothing reads is still silent, now in adopt too. This PR does not decide it. #451, filed, lays out refuse, note and leave, so closing #339 loses nothing. Lane 5's second review, of 421e4e5, found no blocking item, 2 should-fix items and 3 nits. The two should-fix items are this body: the landing after #409 (above) and #386 (above). The code needed no change for them. f63d1ed adds two tests and no code: - **n1. `plan` asks nothing of a `--pin-source` no `--pin` records.** With the `args.pin and` guard dropped, such a path holding an ESC or a zero-width joiner was refused in a sentence naming no file ("`execute` writes into , and ..."), and every test still passed. `test_a_pin_source_no_pin_records_is_not_asked_about` holds the plan to exit 0 and `pins: []`, with the path named nowhere in the plan or the report. The ESC row is POSIX-only by the file's `POSIX_NAMES` mark; the zero-width joiner row runs everywhere. It holds #451's answer today, silence, so a decision there changes this test on purpose. - **n2. A printable `--pin-source` is printed as it is.** With `_spelled` in place of `_shown` in `plan`'s `pin` line, every test still passed. `test_a_printable_pin_source_is_printed_as_it_is` holds `plan`'s `pin` line, and `check`'s `write` line for the plan `plan` wrote, to their exact text, ending `(local-clone (<path>))` with no backtick. - **n3** needs a code change and is left out (below). **Fix round 2, on f63d1ed.** Each run had a `gh` stub first on PATH (it logs and exits 97), `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`, `git-filter-repo` 2.47.0 next on PATH, and the identity blanked with `GIT_CONFIG_COUNT`; the session `gh` guard came first inside each run. - **The pin set: 812 passed, 2 skipped.** It covers adopt pins, adopt plan, #339's file, #275's tests, the pin and neutral-pin validators, git output decoding, adopt template values, CLI reference, scaffold pin and reuse, scaffold pin collisions, #314's two files and #396's file. The skips are `test_adopt_plan.py:403` (no MedxEHR clone) and `test_adopt_template_values.py:223` (this host reads `git config user.name` from the repository, not from `GIT_CONFIG_GLOBAL`). That is fix round 1's 809 passed, plus the three new rows. - **Shape doctor and scaffold leg-path collisions: 369 passed, 3 skipped** (the PowerShell line exists only on a Windows host). - **Mutation probes**, each one line changed in a throwaway worktree, over `tests/test_adopt_pins.py` and `tests/test_pin_source_unreadable.py` less the five end-to-end adoptions: - the `args.pin and` guard dropped: 2 failed, both n1 rows; - `plan`'s `pin` line `_spelled`: 1 failed, the n2 test; - `check`'s `write` line `_spelled`: 2 failed, the n2 test and `test_an_upper_cased_hand_edit_is_written_lower_cased`; - `plan`'s `pin` line raw: 1 failed, the no-break-space test that already guarded that hunk. **On the merged tree, 73d65c6** (the merge f9ed46f and the `_read_pins` commit), with the same harness: - **The pin set** above, with #445's `test_adopt_late_failure.py`, `test_adopt_git_reads.py` and `test_adopt_filter_repo_failure.py` and #387's `test_adopt_empty_symlinks.py` and `test_adopt_root_manifest.py`: **988 passed, 2 skipped**. The skips are the two above. - **Shape doctor, scaffold leg-path collisions and the adopt suites:** **967 passed, 3 skipped** (the PowerShell line). The adopt suites are e2e, execute refusals, verify, root shape paths (#387's too), leg-path collisions, clone template, ignored shape files and submodules. - **On the merge alone, f9ed46f** (the same tree as before the `_read_pins` commit): `tests/test_adopt_late_failure.py` gave 3 failed, 83 passed, the three `AttributeError` rows above. - `render-cli-reference.py --check` is ok on the merged tree. pyflakes reports the same four warnings on `adopt-project.py`, `scaffold-project.py` and `scripts/shape_materialize.py` as before the merge. **Fix round 1, on 421e4e5.** Each run had `git-filter-repo` 2.47.0 on PATH and the identity blanked with `GIT_CONFIG_COUNT`. A `gh` stub that logs and exits 97 was first on PATH, with `GH_TOKEN` bogus and an empty `GH_CONFIG_DIR`, and the session `gh` guard came first inside each run. - **The pin set:** 809 passed, 2 skipped (the same files and skips as above). - **Doctor and scaffold:** 782 passed, 3 skipped (the PowerShell line exists only on a Windows host). It covers shape doctor, scaffold leg-path collisions, scaffold e2e, referent chain links, naming policy, repo hygiene and #351's free-text values. - **The adopt suites:** 882 passed. It covers e2e, execute refusals, verify, spec-only, leg paths, root shape paths, plan escapes, leg-path collisions, clone template and submodules. - **Before the #314 merge,** on 74ce4d9's tree: the first two sets gave 577 passed, 2 skipped and 782 passed, 3 skipped. On 2bcfa69's tree (the merges alone, before the fix) they gave 542 passed, 2 skipped and 782 passed, 3 skipped. After the 45e01f4 merge, `tests/test_agents_md_codes.py`, repo hygiene and CLI reference: 110 passed. - **Lane 5's second review** also ran 421e4e5 merged with main cb7063f and with 53bee5b: the pin set, the manifest-value set, and the doctor set with #357's `tests/test_doctor_validator_rows.py`. All passed, with only the skips above. Proof that fix round 1's tests test something: - **On 2bcfa69's tree,** with this head's two test files laid over it, 31 of the new rows fail: - S1: 17; - N2 and its report spelling: 7; - S2: 3; - the `pin_product_key` port: 2; - `check`'s S3 line: 1; - the not-UTF-8 `adopt plan` row: 1. The N3 rows pass there, as they must: they guard hunks this branch already had. - **Mutation probes.** Each hunk was reverted alone, in a throwaway worktree, and both test files were run: 18 of 18 were caught. - **The port:** the casefold keying (2 fail); the casefold lookup alone (2). - **S1:** the path named raw (13); git's reason named raw (2); every path `repr`'d, printable or not (27). - **S2:** no `except` (3). - **N2:** no `plan` check (11); only #270's question asked (1); the formatting-character reason said otherwise (1); the report's `pin` line raw (1); `check`'s `write` line raw (1). - **S3:** the line dropped (1). - **N3:** the `_pin_fix` name guard (1); the `_read_pins` path guard (1); the lower-casing (1); the `digest_source` check (1); the not-text check (1); the gitlink filter (1). - **The issue rows reproduced.** S1, S2 and N2 were reproduced on 2bcfa69's tree and shown gone on the fix (a probe driving both tools; `gh` was called 0 times). **Before fix round 1.** Runs were on 7a94b6b, with `git-filter-repo` 2.47.0 first on PATH and the repository-local `user.name` blanked with `GIT_CONFIG_COUNT` (a workstation whose standard checkout sets one fails `test_the_scaffold_refuses_with_no_elector` on main itself; CI sets none). 7a94b6b differs from ffd1297 in `tests/test_pin_source_unreadable.py` only, so the three suites that do not read that file ran on ffd1297: - the new file: 46 passed; - adopt pins, adopt plan, git output decoding: 172 passed, 1 skipped (the MedxEHR clone is absent); - scaffold pin and reuse, pin collisions, e2e, referent chain links, neutral pin and pin validators, #275's tests, naming policy, repo hygiene, CLI reference, scaffold leg-path collisions: 571 passed; - shape doctor: 254 passed, 3 skipped (the PowerShell line exists only on a Windows host); - the new file with repo hygiene, git output decoding and CLI reference, once more after the last edit: 176 passed. Earlier on 5690235 and d806e8f, before the #339 commit: the adopt suites (e2e, execute refusals, verify, spec-only, leg paths, root shape paths, plan escapes, leg-path collisions, clone template) 740 passed, and `tests/test_adopt_template_values.py` with #273's other adopt tests 489 passed, 2 skipped. They were not re-run on 7a94b6b: the #339 diff touches `pin_digest_from_source` and the call in `declared_pin_values`, which those suites reach through `test_adopt_pins.py` only. Nothing was run on macOS or Windows. Proof the new tests test something: - **On d806e8f.** 38 of the 46 fail, each on the traceback and its exit 1. The 8 that pass are the unchanged behaviour: the not-a-clone refusal (4 rows) and the valid-source rows (4). - **Mutation probes.** Ten hunks reverted one at a time, each failing at least one new test: the `OSError` catch (16 fail), the catch narrowed to `OSError` only (21), the missing-path clause (9), the clause narrowed to `FileNotFoundError` (3), the `S_ISDIR` judgement (3), the loop guard (4), no resolve at all (3), the old `.resolve()` call (4), the reason as `str(exc)` (12), and the file sentence as the OS's (3). - #273's own 52 tests still fail on c7c02fa (51 fail or error; the one that passes checks the test's own table), and its 18 mutation probes were each caught. **Re-verified after the merge.** An independent re-verification of f9ed46f and 73d65c6 found the code correct: - #409 is carried whole. A 30,000-case fuzz of `--pin` lists through #409's final refusal and this branch's port differed in 0 cases. - `scaffold-project.py` equals the tested file plus main's own changes that are not #409's (same patch-id). - `_commit_the_split` keeps #445's `try` and #394's `append`. - `_read_pins` reads a loaded plan as before. Its findings were all in this description (the #506 bullet and three stale facts), now fixed. Runs on the head and on its merge with main 48585e6: 988 passed, 2 skipped; and 967 passed, 3 skipped. `git merge-tree --write-tree` was run at 01:48Z on 2026-10-13, comparing hunks per file. Each PR head, fetched from `refs/pull/<n>/head` with no `gh`, was merged with 73d65c6 and with origin/main 9736424 alone. Two sets were asked about: - every PR this description listed before that main does not now contain; - every PR numbered above #468 whose head main does not contain: #473, #474, #490, #491, #493, #494, #495, #496, #497, #503, #504 and #505. **Open state, read with `gh` by the coordinator at 02:00Z and refreshed at 02:25Z after a re-verification on main 48585e6.** Every pull request named below is open. #184, #94, #96, #98, #100 and #161 are closed, and #465 landed as 48585e6. `fix/adopt-plan-quote-yaml-keywords` has no pull request. #506 (`fix/scaffold-reuse-seeded-legs`) opened after this list was made, and the re-verification reads it. - **Landed since the last draft of this list:** #343, #346, #355, #358, #373, #378, #382, #387, #394, #397, #401, #404, #409, #419, #452, #456, #465, #466 and #484. #394's `_commit_the_split` conflict and #401's `docs/cli.md` conflict are both resolved in f9ed46f, above. - **Clean, 20:** #266, #347, #356, #374, #376, #384, #395, #453, #467, #468, #473, #474, #490, #491, #493, #494, #495, #497, #503 and #505. #453, #468 and #491 were updated onto main since the first read and now merge clean. #507 (lane 5's #486-#488) shares only the generated `docs/cli.md` with this branch and merges clean with it. - **Conflicting with main alone, and this branch adds no hunk:** - #402: `shape-doctor.py` 5; - #496: `shape-doctor.py` 10 and `tests/test_shape_doctor.py` 1. **Conflicts this branch adds:** - **#435** (3efcc7a, lane 2's `fix/adopt-plan-field-types`, for #386). - It adds 2 hunks in `adopt-project.py`: 5 here, against 3 with main alone. They are `render_plan`'s `pins:` line and `Plan.pins`. - Keep this branch's `_pin_lines` and `_pin_products`. The rest is under "Where the meaning meets". - **#504** (37ef29a, `fix/adopt-root-kept-symlinks`). - It adds 1 hunk in `adopt-project.py`: 2 here, against 1 with main alone. It is the materializer call in `_commit_the_split`. - Keep both arguments and both comments: `append=append, neutral_pins=_recorded_pins(plan), spell=_spelled`. - **#506** (119d8c4, `fix/scaffold-reuse-seeded-legs`). - It adds 1 hunk in `scaffold-project.py`. With main alone, #506 conflicts only in `docs/handbook.html`. - #506 inserts `LEG_TEMPLATES` right after `declared_pin_products`, which this branch moved out, and rewrites `_reusable_remotes` further down. Keeping both sides breaks the file with an unterminated string literal. - Keep only #506's `LEG_TEMPLATES` block. Drop #506's `declared_pin_values`/`declared_pin_products`, which moved here, and this branch's three-line `_reusable_remotes` opener, which #506 rewrites below. Resolved that way, pyflakes is clean and #506's scaffold tests pass (88). - **`fix/adopt-plan-quote-yaml-keywords`** (ed9968f; a branch with no pull request). - It adds 2 hunks in `adopt-project.py`: 3 here, against 1 with main alone. - One is `render_plan`'s `pins:` line. Keep `_pin_lines`, which already sends product, repository and `digest_source` through `y`. - The other is `_refuse_unholdable_values`' signature. Keep `pins: dict`, with that branch's `_validator_judged` and `_named_as_read` above it. - `fix/adopt-late-failure-notice` is no longer on origin. #445, the late-failure notice, is on main and merged here. **Where the meaning meets:** - **#435 and #386.** The coordinator has said this on #435: #435 (comment) - #435's `_plan_key_findings` (`plan-bad-pins`) keeps only text items in `pins:`, and its `PINS_REMEDIATION` tells a person to write `pins: [openGlass]`. - This branch writes each pin as a mapping, and it makes a name alone `plan-pin-unrecorded`. - Read from #435's code, not run: on a tree with both, the plan `plan --pin` writes is `plan-bad-pins` in `check`. - Whichever lands second reconciles them, with three changes: - `_plan_key_findings` takes this branch's mappings, or leaves `pins:` items to `_read_pins`; - a `pins:` that is not a list gives one finding, not both `plan-bad-pins` and `plan-pin-malformed`; - `PINS_REMEDIATION` says the mapping `plan` writes. - Until that is done, `tests/test_adopt_pins.py::test_a_plan_with_a_whole_pin_checks_ok` fails. - **#314.** It landed, and it is merged here (above). - **`fix/adopt-plan-quote-yaml-keywords`.** Its pin tests are written against the name-only `--pin`. They need the `name@commit` form, and their expected `pins: [...]` line becomes `_pin_lines`'s mapping. - **Two unwritable values are refused in two runs (lane 5's second review, n3).** `plan --elected-by 'A<LF>B' --pin openGlass@<sha> --pin-source <a path holding an ESC>` refuses the elector alone, exit 2. Re-run with the elector retyped, it refuses the source; #270's refusal names every unwritable value at once. Reproduced on 421e4e5, with `gh` called 0 times. One refusal would need a code change, not made here: - `_refuse_an_unrecordable_pin_source` would split into a function that returns the sentence; - `_checked_plan_inputs` would ask it before #270's `unwritable_values` raise, and raise one `adopt-unwritable-value` that names both, with both remediations; - a test would add the two-value row. N2's question is `one_printable_line`, not `unwritable_reason`, so it cannot simply join #270's list. - **Docs.** The README, AGENTS.md and `docs/handbook.html` are unchanged, because none of them calls adopt's `--pin` a name. A sentence saying adopt now takes the scaffold's `--pin` is a follow-up. It costs an AGENTS.md line-cap raise, and a README edit forces the handbook's README blob to change. - **A product spelled `null`, `true` or `false`.** The scaffold already accepts `--pin null@<sha>`: it exits 0 and builds a root whose `validate-pins.py` exits 1 `neutral-pin-entry-malformed`. Adopt's `plan` writes `product: null` through `y`, and `check` then finds `plan-pin-malformed` and refuses it before any leg exists. The one fix for both tools is in `parse_pin`, and it would change the scaffold's behaviour. - **Wording shared with the scaffold.** `digest_source` is written bare into the pin file, so a `--pin-source` path holding ` #` is cut short there. `templates/assembly-root/contracts/neutral-product-pin.yaml` and `validate-pins.py`'s `neutral-pin-missing` remediation name only the scaffold as the writer. Two comments in `scripts/repo_shape.py` still name the scaffold's `pin_digest_from_gh`, in a digest-pinned shipped copy. - **Filed as follow-ups:** - #449: S3, a hand-edited `commit:` or `tree_sha256:` that disagrees, with options (a) and (b); - #450: the scaffold half of N2, a `--pin-source` path no file can hold; - #451: #339's third decision (N5), a `--pin-source` that nothing reads. - **`validate-pins.py` prints a `--pin-source` path raw** in `neutral-pin-source-unreadable`, the twin of S1. It is a shipped template that #409 and other open PRs edit, so it is left for its own change. No issue is filed for it yet. - **Windows and macOS** were not run here; CI ran both (below). Two assertions that depend on what Windows answers for a path (a name past the filesystem's limit, a path beneath a file) check only the refusal and its remediation there. Fix round 1's rows that need a control character or a byte that is not UTF-8 in a path, or a symlink to `git`, are skipped on Windows by a mark, and so is fix round 2's ESC row. The rows for a no-break space and a zero-width joiner run everywhere, and so does the in-process `gh-not-found` test. Reviewed independently by lane 5 at 7a94b6b (3 should-fix, 5 nits: fix round 1) and at 421e4e5 (0 blockers, 2 should-fix, 3 nits: fix round 2). After #409's squash, the re-verification of f9ed46f and 73d65c6 found the code correct; its 1 should-fix and 3 nits were in this description and are fixed. GitHub's update-branch then merged main twice (33bae3f, 2170526), with no hand edit. CI's tests, tests-macos and tests-windows and SonarCloud all passed at 2170526 (main cac6bb2 merged in). On that head merged with main 57c85e9 (#490, #474 and #468, none of which touches this branch's files), the pin and adopt set with `tests/test_update_shape_landing.py` gave 1112 passed, 2 skipped, and the doctor, adopt end-to-end, unfinished-merge and bootstrap set gave 1056 passed, 3 skipped (the doctor's PowerShell line, which exists only on a Windows host). Closes #273 Closes #339 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/test_adopt_manifest_values.py, hunk 1 (the comment above PLAN_EDITS): this branch's pointer to `TEXT_THE_VALIDATOR_REFUSES` and #259, plus main's note that the rows name `said_of`'s finding for the date. tests/test_adopt_manifest_values.py, hunk 2 (the list header): this branch's split stays (`id: "007"` in `TEXT_THE_TEMPLATE_NOW_HOLDS`, `TEXT_THE_VALIDATOR_REFUSES` for the rest), with main's note that `20261011` is a date `election_date` refuses, so its one sentence is #294's and the validator is not asked. tests/test_adopt_manifest_values.py, hunk 3 (the `elected-on-basic-format` row): main's sentence, "the plan's `elected_on:` is the text '20261011'", which is what `check` and `execute` print on the merged tree (`FINDING`/`REFUSED election-date-malformed`), not the validator's `manifest-elected-on: elected_on is '20261011'`. tests/test_adopt_manifest_values.py, hunk 4 (the `..._one_finding_on_this_rule_alone` docstring): this branch's wording, plus main's sentence that a date `election_date` refuses is #294's one finding. tests/test_adopt_manifest_values.py, two lines git merged cleanly that failed on the date row: `test_check_refuses_a_text_value_the_validator_refuses` asks for `said_of(new)[0]` where it asked for the validator's `FINDING`, and `test_where_a_misread_rule_judges_the_rest_the_validator_still_answers` takes #294's one finding from `_plan_field_findings` for the date, as main's `..._on_this_rule_alone` does; the section comment and one docstring say so. tests/test_scaffold_manifest_values.py: the `[branch-tilde]` row loses its `xfail(strict=True)`, which XPASSed (and failed) once #355 landed as 6c2a371, and now asserts `REFUSED scaffold-bad-tracking-branch:`; the comments say #355 is on main. tests/test_bootstrap_spelling.py and tests/test_bootstrap_after_bump.py (#391, new on main): the literals in seven tests that found ` path: spec` and `tracking_branch: main` in a scaffolded `project.yaml` now find the quoted lines the template writes (`path: "spec"`, `tracking_branch: "main"`); their nine cases failed on the merge without this. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…holder to a branch before add, so the test holds once #487's tracking-branch refusal lands (PR #507) `test_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id` runs `family.py init` and then three real `add`s. PR #507 (#487) makes `add`, `bump` and `remove` refuse on the holder's tracking branch as `family-on-tracking-branch`, so the test now switches the new holder to `family/members` first, in the file's own idiom and as #507's own fixtures do. On main this changes nothing: `add` commits on any branch. Neither PR has to edit this test, whichever lands first. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No conflict. Main's two commits since aa4e1f5 are 5e02abe (doctor and validate-pins gitlink repairs) and 56713e2 (adopt plan takes the tracking branch from the source's default branch). docs/cli.md auto-merged. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line the template renders (#259), as main's new test (56713e2) reads the adopted root's project.yaml test_a_source_on_another_branch_opens_its_pull_request_against_it[master] and [trunk] read `git show <split branch>:project.yaml`, the adopted root's project.yaml, which `execute` renders from templates/assembly-root/ project.yaml. This branch quotes that template's `tracking_branch:` line, so the root says `tracking_branch: "master"`, and the test asked for the bare line. It asks for the quoted one; the branch it expects and every other assertion are unchanged. Left as they were, because a PLAN file is written by adopt-project.py's own `emit`/`y`, not by a template, and `y` leaves `main`, `master` and `trunk` bare: test_adopt_github_exits.py:743 reads the plan `plan --out` wrote, and test_adopt_tracking_branch.py:73-76 edits the plan and reads it back. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the source the branch each row tracks, which adopt now requires (#479, 56713e2) test_an_adoption_reads_back_every_value_it_was_given plans, checks and executes with a tracking branch of 2026, 007, 1.5, 20261011, true or null, from `--tracking-branch` or by hand in the plan, against a source that had only `main`. Main's 56713e2 (#479, in #493) makes `check` refuse a tracking branch the source lacks, as plan-tracking-branch-missing, and `execute` ask the remote for it (under --local-remote-dir that remote is the source), so all six rows died at `check`. The source now gets a branch of that name before `plan`. Nothing the test asserts changed. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
…`scripts/siblings.py` is absent, and update-shape.py says the Makefile it copies calls it, so a holder updated without `--add` no longer fails on `can't open file` (#489) (#503) A family holder cut before 2026-09-09 (#76) has no `scripts/siblings.py`. Updating it the way AGENTS.md prescribes, `update-shape.py apply` without `--add scripts/siblings.py`, still copies the changed Makefile, and that Makefile's `siblings`, `park` and `resume` targets run the script. `apply` exited 0, `validate-family.py` was green, only `note ... not added` was printed, and then all three targets died on `python3: can't open file '.../scripts/siblings.py'`. This takes the issue's second option, which keeps per-file consent and never leaves a dead target. `apply` still refuses nothing, copies the Makefile and adds nothing it was not told to. **What changed** - `templates/family-root/Makefile`: each of the three targets asks make itself, `$(wildcard scripts/siblings.py)`, whether the script is there, and when it is not it refuses by name with `$(error)`: exit 2, one shared message, and no shell involved (no `test -f`, no `echo`), so the answer is the same wherever make runs. The message says what is missing, that `update-shape.py` adds a file only on the human's word, `python3 update-shape.py check --root "<this holder>"` to read the verdict, then the apply command it prints with `--add scripts/siblings.py`, and, for a holder that took the script and then deleted it (where `check` says `copy-missing` and `--add` refuses a pinned path), to restore it from the holder's history instead. With the file there, each recipe is what it was: same command line, same arguments. - `update-shape.py`: `check` and `apply` say, on the line that already reports the file, that the Makefile calls it. `check` appends it to the `upstream-added` detail and `apply` to its `note ... not added` line. It is a table (`CALLED_BY_A_COPY`) with one row, and it speaks when the calling copy is `upstream-changed` (the one verdict `apply` writes on its own: "the Makefile that `apply` copies calls it, so `make siblings`, `make park` and `make resume` cannot run until it is added") or `unchanged` (the holder after that first `apply`, pinned at the target, where every later `check` and `apply` still finds the script `upstream-added`: "this root's Makefile already calls it, so ... cannot run until it is added"). A `locally-modified` or `both` Makefile says nothing. Nothing is refused and the exit codes are unchanged. - `AGENTS.md`, item 6 of the family section: one clause, in place, with the same line count, so the file stays at its cap (490/490). It keeps "per file": `apply --add scripts/siblings.py` takes it on the human's word, per file, and without it `make siblings`, `park` and `resume` refuse. **Evidence** - Reproduced on `main` (7a8b981) with a real holder cut at 0170edd, the commit the issue names, local remotes and a stub `gh`: `check` says `Makefile` `upstream-changed` and `scripts/siblings.py` `upstream-added`; `apply` without `--add` exits 0 with `family ok`; `make siblings`, `make park` and `make resume` each print `can't open file` and exit 2. - On this head, the same holder: `check` and `apply` carry the sentence above, `apply` still exits 0, and the three targets exit 2 with `make <verb> needs scripts/siblings.py, which this holder does not have. ...` and no `can't open file`. With `apply --add scripts/siblings.py` all three exit 0 and run the real script. - `tests/test_family_makefile_siblings.py` (new, 31 tests; skipped only where `make` is absent) cuts a holder from a standard before #76 (a clone with no `scripts/siblings.py`, its copy-list entries stripped and the v0.4 Makefile: built, not read from history, so a shallow clone runs it), updates it to this checkout's bytes with `apply` with and without `--add`, and runs the three targets in each. It also runs the Makefile alone: the refusal is one message for the three targets, it needs no shell (`SHELL=/nonexistent` still refuses), `make -n` refuses too, the present-script path calls the script with the arguments it always had, `help`, `bootstrap`, `validate` and `pins` are untouched, and `CALLED_BY_A_COPY` is held to the template. It also covers a second `check` and a second `apply` on a holder already at the target (the `unchanged` clause), and the `copy-missing` sentence in the refusal. It carries no `WINDOWS_SKIP`, unlike `tests/test_park_resume_targets.py`: the refusal needs no shell, and `windows-latest` ships a GNU make (the runner-images install script copies `mingw32-make.exe` to `make.exe`), so the tests are expected to run on all three runners. CI's logs are quiet-mode, so they do not show whether they ran. - Mutation checks, run in a throwaway clone of the head: with the Makefile reverted to `origin/main`'s, 13 of the 31 fail (the refusal tests, the one-message and no-shell tests, `make -n`, and every `make` run on the updated holder, which print `can't open file` again). With `update-shape.py` reverted, 9 fail (the `check` and `apply` sentence, the `left_dead_by` cases and the table). Taking only the `unchanged` branch out of `left_dead_by` fails 2 (the unit case and the second `check`/`apply`), and taking only the `copy-missing` sentence out of the Makefile fails 3 (the refusal test, once per verb). - No static digest of the family-root Makefile exists in the repository: `git grep` of its sha256 finds nothing, and every `shape-pin.yaml` row is rendered from the bytes at `init` and `apply`. A holder's pinned digest moves with the file, as for any template change. **Tests** All at 49330dc (origin/main 5a25387 merged in), with a stub `gh` first on PATH, a bogus token, an empty `gh` config directory and the git identity blanked: - `tests/test_family_makefile_siblings.py`: 31 passed. - `tests/test_update_shape*.py` (seven files), the new file, `tests/test_park_resume_targets.py`, `tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py` and `tests/test_cli_reference.py`: 438 passed, 3 skipped (the NTFS-junction cases, Windows only). - every `tests/test_family*.py` and `tests/test_shape_doctor.py` (the doctor loads `update-shape.py` in process): 842 passed, 11 skipped (`tests/test_family_init_values.py:242`, which the file documents, and the doctor's PowerShell line, which prints only on a Windows host). At 44000f6, after GitHub's update-branch merged main af01068 (no hand edit), the same sets ran again with the same stubs. The update-shape set, which now has eight files with #374's `tests/test_update_shape_next_quoting.py`: 535 passed, 3 skipped. Every `tests/test_family*.py` and `tests/test_shape_doctor.py`: 858 passed, 11 skipped. That head was then merged with main 47b1e06 (#395, the templates quote their text placeholders) as a local gate commit. On it, the two sets, together with `tests/test_template_text_placeholders.py`, gave 1481 passed, 25 skipped. **Merge-tree.** At 49330dc, which contains `origin/main` 5a25387 (merged as f08129d), `git merge-tree --write-tree` is clean against `origin/main`, against every open pull-request head that touches `AGENTS.md`, `update-shape.py` or `templates/family-root/` (#347, #374, #395, #465, #474), and against lane 5's `fix/family-commands-and-branch` (10c93de, #486-#488). #491 conflicts only in `docs/handbook.html`, as it does with `origin/main`. The trees merged with #474 and with `fix/family-commands-and-branch` were run: the new test file and the neighbouring suites pass on both, and `AGENTS.md` stays at 490 lines. **Review.** An independent review at 8643774 gave APPROVE WITH NITS (0 blockers, 0 should-fixes, 5 nits). It reproduced the defect on `main` and the fix on this head, and ran 10 mutations, all of them caught. Round 2 (49330dc) took four of the nits: the test file's docstrings say where it runs; the refusal also covers a holder that deleted a pinned `scripts/siblings.py`; AGENTS.md keeps "per file"; and a later `check` on a holder already at the target names the Makefile that calls the script. The fifth nit was this description. Re-verified at 49330dc: APPROVE WITH NITS (0/0/1). That nit was this description's merge-tree paragraph, now as above. **Left out** - `apply` does not refuse to take the Makefile without the script (the issue's first option), and it does not take the script on its own: both break per-file consent. - The holder's `AGENTS-shape.md` and `README.md` are pinned copies and say nothing about this; editing them moves every holder's digests for a sentence the Makefile's own refusal already carries. README and the handbook are untouched. - The doctor's `shape currency` row does not carry the sentence and is unchanged; the per-file detail line of `check` and `apply`'s note do, and the Makefile's own refusal covers the moment of use. - The refusal is written for GNU make. macOS ships 3.81: the guard uses only `$(if)`, `$(call)`, `$(wildcard)` and `$(error)`, which 3.81 documents. It was run on 4.3 here. CI's `tests`, `tests-macos` and `tests-windows` and SonarCloud all passed at 8643774, and again at ca4778e (49330dc with main 48585e6 merged in by GitHub's update-branch), at 6fe17be (main 57c85e9 merged in the same way, after #474 changed `update-shape.py`; no hand edit) and at 44000f6 (main af01068 merged in the same way, after #374 changed `update-shape.py` and #491 and #505 changed `AGENTS.md`; no hand edit). Reviewed independently at 8643774: APPROVE WITH NITS (0/0/5). Round 2 (49330dc) took the four code nits, and its re-verification gave APPROVE WITH NITS (0/0/1), the description's merge-tree paragraph, now updated. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 44000f6 (main af01068 merged in by update-branch). Main 47b1e06 (#395), which landed after, touches none of its files, and the combined tree passed the family, doctor, update-shape and hygiene tests (1481 passed, 25 skipped). Closes #489 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged by sha, 631f087, fetched at 13:05Z. It brings af01068 (#505, README.md and AGENTS.md), 47b1e06 (#259, PR #395: the templates quote their text placeholders) and 631f087 (#489, the family Makefile). One file conflicted, tests/test_adopt_manifest_values.py, in six hunks, all where this branch's composition with the validator (f38d19a, rv181f's G1) met 47b1e06's split of `TEXT_THE_READER_MISREADS` into `TEXT_THE_TEMPLATE_NOW_HOLDS` (`id: "007"`) and `TEXT_THE_VALIDATOR_REFUSES`. Both sides are kept. This branch has no `misreads_elsewhere` (f38d19a asks the validator about every value, first, and #181's rule leaves to it every value it judges), so each of main's calls with it becomes this branch's question, `_misread_findings(plan) == []`: - `test_a_text_value_the_template_now_holds_is_no_finding_either_way` (main's): no finding of the validator's, and none of #181's. - `test_where_a_misread_rule_judges_the_rest_the_validator_still_answers` (main's), over `TEXT_THE_VALIDATOR_REFUSES`, with this branch's `test_where_the_validator_judges_it_the_misread_rule_says_nothing` folded into it: that test asked the same thing of the same rows, and main's adds the remediation and the finding's start. - `test_every_value_is_asked_and_the_misread_rule_leaves_each_it_refuses` (this branch's name) takes main's count: `id: "007"` is no finding now, so two, and #181 names none. - The docstring of `test_a_text_value_the_validator_refuses_is_one_finding_in_the_list_check_prints` says both: the validator first, and #181 silent on a quoted line. The rest of the tree is `git merge-tree`'s. No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py or scripts/family.py (imports not counted; shape_materialize.py imports `YamlError` and `parse_yaml` twice, as on main). On this merge, as it stands, 76 tests fail: 75 of tests/test_adopt_plan_yaml_quoting.py's 491 and this branch's `test_a_value_the_reader_reads_as_another_kind_is_named_as_read[id: 007]` in tests/test_adopt_manifest_values.py. Each asked #181's gate, or the validator, about a value 47b1e06 now writes between double quotes, where it reads back as typed. The commits after this one make each true at the merge. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ype gate on the plan's keys comes first in `check` and `execute`, before the source is opened, and the walk that now follows #273's pin files reads only the pins `_read_pins` takes, so a `pins:` that is no list is named once and never walked Merged by sha d41cc29, the nine commits of main past af01068 (47b1e06 to d41cc29). The brief named 8cd7dbe; main had moved to d41cc29 (#402, shape-doctor.py) by the fetch at 21:19:04Z, so the newest was merged. No conflict: the tree is `git merge-tree --write-tree a79936f d41cc29`'s, d0b610c. The commits that meet this branch's files: - f391e58 (#386 via #435): `Plan` reads `paths:` and `allow_empty_legs:` through `_plan_list`, `source:` through `Plan.source`, and `legs:` only as a mapping in `names()`; `_plan_key_findings` names a `source:` or `legs:` that is no mapping and a `pins:`, `allow_empty_legs:` or `paths:` that is no list (`plan-bad-source`, `plan-bad-legs`, `plan-pin-malformed`, `plan-bad-allow-empty-legs`, `plan-bad-paths`). `cmd_check` prints them, with #293/#294's field findings, before it makes the work directory or opens the source, and exits 1; `cmd_execute` refuses them first (`_refuse_a_mistyped_plan`). - 191105a (#225 via #266) and d52b100 (#462 via #506) change scripts/shape_materialize.py: the leg-path rule refuses a leg path the YAML reader reads back as another type, and `RULESET_HINT` offers one exit. `leg_path_problems` is what `_PlannedRoot._mount` and `_leg_findings` ask, so a plan it refuses has no mounts in the walk, as before; no line this branch changed in the materializer is touched. - 47b1e06 (#259 via #395): the templates quote their text placeholders. - 8cd7dbe (#286 via #347): update-shape.py refuses a pinned copy, the pin or the manifest kept as a symlink as `update-rewrite-symlink`, and shape-doctor.py calls such a copy `copy-is-link`. Neither is this branch's file, and the lines it adds name neither the materializer nor adopt-project.py. - cdcd2f0, fc48854, 631f087 and d41cc29: setup-project.py, tests/conftest.py, shape-doctor.py, the family Makefile and update-shape.py, none of this branch's files. How they compose with this branch: - The walk reads the plan's pins through `_declared_pins`, as `_leg_collision_findings` does: only the entries `_read_pins` takes as pins. A `pins:` that is no list is #435's first finding, so neither command reaches `_entry_findings`, and so the walk, with one; were it reached (the tests ask the walk directly), it adds no path and raises nothing. A list whose entry is no pin passes #435's gate and is `_read_pins`' finding in `_leg_findings`, and the walk adds no path for it. - A `paths:` that is no list reads as no entries (`_plan_list`), and a `legs:` that is no mapping as the default mount paths (`_plan_leg_path`): both are #435's findings first, so neither reaches the walk from `check` or `execute`. The order, as read in the merged code: - `check`: #435's key findings, with #293/#294's beside them, alone and first; else `plan-stale`, #320's split-branch findings, then `_entry_findings`: #196's unsafe paths, #303's empty symlinks, coverage, `_leg_findings` (#273's pin findings last), #166's submodules, `plan-root-symlink`, #255, #261, #479, #190, #293/#294. - `execute`: `_refuse_a_mistyped_plan` (#435) before the work directory is made; then `_check_names`, `_refuse_an_unrunnable_plan`, `adopt-name-too-long`, #255's clone URL, `_refuse_what_check_finds`, `adopt-symlinks-off`, seeding, `adopt-work-dir-occupied`, #479's remote check, `adopt-branch-exists`, #320. All before any leg. Checked on the merged tree: no top-level name bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, update-shape.py or shape-doctor.py but main's own (`YamlError`/`parse_yaml` in the materializer, `sys` in the doctor); no name loaded and bound nowhere; each side's change reappears line for line in the other side's diff to the merge, in every file. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s a runnable `scripts/family.py`, printed commands quote paths, and `add`, `bump` and `remove` refuse the tracking branch and print their branch's push (#488, #486, #487) (#507) Three defects from lane 3's v1.0.0 acceptance pass of AGENTS.md's "Creating a family, and adding a member", one commit each, in this order, then one review commit for #486 and one for #487. All three change `scripts/family.py` and `add`'s NEXT line, so they are one pull request. **What was wrong.** - AGENTS.md spelled the command `python3 scripts/family.py init --org <org> --family <Name>`, a path that runs from the standard's root. `--into` defaults to where the person stands. - So the command, followed exactly, printed `landing <standard>/Orbit/Orbit` with no word, and `make siblings` would then clone every member into the standard's checkout too. - `init`'s NEXT STEPS printed `python3 family.py add …`, built from `Path(__file__).name`. From the root that fails `can't open file`, exit 2. **What changed** (commit 1). - With neither `--into` nor `--work-dir`, `init` refuses a landing inside a checkout of openRepoShape: `family-landing-in-standard`. A checkout is a directory holding `scripts/family.py` beside `templates/family-root/`, found in the landing's ancestry, so another clone or an install counts too. - The refusal comes before anything is created, under `--dry-run` alike. - The remedy names `--into ..` from the root. From deeper inside the checkout it names the relative path that reaches the checkout's parent. - An explicit `--into` or `--work-dir` is not second-guessed. `family-holder-already-here` is still asked first. - NEXT STEPS, and the `family-holder-already-here` remedy, name this file relative to the directory `init` ran in. From the root that is `scripts/family.py`, as setup-project.py, validate-family.py, AGENTS.md and README spell it. It is absolute where no relative path exists. - AGENTS.md's init command passes `--into ..`, as README's already does, and step 1 says why. The edit is line-neutral: step 1 is one line shorter. - `init --into --help` says it too, and docs/cli.md is regenerated. **How tested.** `tests/test_family_init_landing.py`, 9 tests: - the old command from the root is refused, as a dry run, so nothing can land in the checkout; - a real run inside a fake checkout creates nothing; - the remedy from a subdirectory names the right parent; - an explicit `--into .` or `--work-dir .` passes; - outside a checkout the default is unchanged; - AGENTS.md's line, as written, lands beside the checkout; - the printed NEXT runs, both from the root and from elsewhere. On origin/main code 6 of them fail. Removing the refusal turns 3 red, and putting `Path(__file__).name` back turns 2 red. **What was wrong.** `family.py` and the holder's `siblings.py` printed every path in their pasteable commands bare. Under a holder at `…/my work/Sonata/Sonata`: - the `member-git-dir-cached` remedy `rm -rf <store>` split into `rm -rf …/my`, an absolute directory that is NOT the store, and a relative `work/…`; - `init`'s NEXT STEPS and `add`'s NEXT split `--family-root` and `git -C` (`fatal: cannot change to '…/my'`, exit 128); - the `mv` lines for a misnamed family folder pasted as four arguments. **What changed** (commit 2, and the review commit after #487's). - `scripts/repo_shape.py` gains `shell_quoted(value, platform=None)` and `shell_line(words, platform=None)`, in a new section at its end, the library `family.py` already imports. - It is the doctor's `quote_arg` rule (#101/#102): the same two alphabets, and the same two escapes, `sh`'s `'\''` and PowerShell's doubled apostrophe. The dialect comes from `os.name` or a `platform` argument. An ordinary word is left bare. - It does not flatten to ASCII, as the doctor's report does by contract. A command that deletes or moves a directory has to name that directory. So PowerShell's typographic single quotes are doubled too. - A word that begins with `=` is quoted, in both dialects. zsh, macOS's login shell, expands a bare `=word` to a command's path (`=ls` reaches the command as `/bin/ls`). This is the one place it differs from the doctor's rule on ASCII. - `family.py`: - `init`'s NEXT STEPS print the `add` on ONE line. It used to paste only into a POSIX shell, because of its `\` continuation. `cd <holder>` and `make siblings` are two lines, with no `&&`, which Windows PowerShell 5.1 rejects between two statements. - Those lines and the `family-holder-already-here` remedy quote every path. - `add`'s NEXT quotes `git -C <holder>`, and `member-path-taken` quotes each spelling it names. - `member-git-dir-cached`, and `remove`'s note about the same store, print the directory alone on its own line. Then comes `rm -rf` with its one argument quoted for the POSIX shell `rm -rf` belongs to, and the sentence names that shell: Git Bash on Windows. PowerShell refuses `-rf` and deletes nothing. - `siblings.py`: the parent-folder warning's `mv` and `mkdir -p` lines are quoted for a POSIX shell with the standard library's `shlex.join`, and the warning says which shell. It does NOT import the new quoter: a holder that kept its own `repo_shape.py` through `update-shape.py apply --accept-local` takes the new `siblings.py` without it, and an import of a name its copy lacks stopped `make siblings`, `park` and `resume` with an ImportError that `check` and the validator never reported. - `setup-project.py`'s family hand-over quotes the holder in its `add` line (and names the branch first, see #487). It runs before any checkout of the standard exists, so it carries its own copy of the rule, and a test holds that copy to `shell_quoted`. **How tested.** `tests/test_family_printed_quoting.py`, 69 tests: - The quoter round-trips through `shlex.split`, and through a PowerShell reader, for a space and an apostrophe, `é` and a typographic apostrophe, shell syntax, the empty string, and every printable ASCII character alone and before, inside and after a word. - The host's own `sh`, `bash` and `zsh` hand each quoted word back byte for byte through `printf`, a leading `=` among them (skipped where a shell is absent, and on Windows). - It agrees with the doctor's `quote_arg` on every one of those ASCII values but a leading `=`, and its two alphabets are the doctor's two patterns. - Under a holder in `my work` and in `Dana's work`, each of these splits into the argv it means: `init`'s NEXT (with `cd` and `make siblings` on their own lines), `add`'s NEXT, the cached-store remedy and note, the `mv` lines, and setup-project's `switch` and `add`. - The `rm -rf`, run as split, deletes the store, and the decoy `<tmp>/my` survives. - The `mv` lines, run as split, move the holder. - The holder's own `siblings.py` runs `--help`, the misnamed-folder warning and `--make park` beside a `repo_shape.py` that lacks every name this change added. - Three existing tests' expected lines follow the new wording. Mutations, each turning tests red: - a bare quoter: 35 red; - bare `mv` lines in `siblings.py`: 3 red; - a bare `rm -rf`, a bare `add` NEXT, a bare `init` `add` (round 1, on the 85-test file): 2 red each; - `siblings.py` importing `shell_line` again: 1 red (the older-`repo_shape.py` test); - `%` dropped from the POSIX alphabet: 2 red (the every-ASCII agreement and the alphabet test). The round-1 sample test passed it, 36 of 36; - a leading `=` left bare: 10 red, among them the real-`zsh` test; - `cd … && make siblings` back on one line: 2 red; - setup-project's holder unquoted: 2 red. **What was wrong.** - AGENTS.md says each act's ONE commit lands as a pull request, and no step made the branch that pull request needs. - After `init` the holder stands on `main`, and all three acts committed straight onto it. - `add` printed `NEXT git -C <holder> push -u origin <branch> && open a pull request`. No branch is called `<branch>`, so the push failed: `src refspec … does not match any`. - `bump` and `remove` printed no NEXT at all. - `bump-leg.py` refuses exactly this for a leg (`bump-leg-on-tracking-branch`). **What changed** (commit 3, and the review commit after it). - The real run of each act refuses the holder's tracking branch, the one `family.yaml` declares (`family-on-tracking-branch`), and a HEAD on no branch (`family-holder-detached`, which is what `bump-leg.py`'s `bump-leg-root-detached` refuses too). - The check comes after the refusals about the arguments and the holder, and before the member is read, fetched or cloned, and before anything is written. - The remedy is a quoted `git -C <holder> switch -c <branch>`. The branch is `family/add-<project>`, `family/bump-<id>-<sha12>` or `family/remove-<id>`, and a name that is not a plain branch word becomes `member`. - `--dry-run` runs on any branch. On a work branch it prints where the commit would land. On the tracking branch it prints that the real run refuses there, with the refusal's code, and the `switch` command on a line of its own. The module's EXIT CODES paragraph names this as the one refusal a dry run does not make. - Each act that commits ends with ONE pasteable line: `NEXT git -C <holder> push -u origin <the branch it committed on>`, quoted, then `then open a pull request against <tracking>; never push to it directly`. `bump` and `remove` gain that NEXT. - `init`'s NEXT STEPS open with the `switch` before the `add`. - `add`, `bump` and `remove` `--help` say the branch rule, and that each ends by PRINTING the push; docs/cli.md is regenerated. - AGENTS.md's family block gains the `switch` step before the `add`, and step 3 says the tracking branch is refused, as `family-on-tracking-branch`, and that the branch is pushed as its NEXT line says. README's InkRouter block gains the same step, and its paragraph says it. Both edits are line-neutral: AGENTS.md tightens steps 2 and 5, and README tightens the `make bootstrap` paragraph. `docs/handbook.html` mirrors the block and the sentence, and its footer names #487 and the new README blob. - The holder's own docs, `templates/family-root/AGENTS-shape.md` and `templates/family-root/README.md`, open their add/bump/remove block with the `switch` and say the act refuses the tracking branch. `setup-project.py`'s family hand-over prints the `switch` before its `add`. - Test fixtures that committed on `main` now commit on a branch: - `tests/test_family.py`'s module fixture grows the family on `family/members` and pushes that branch to the remote's `main`, as the merged pull request would leave it; - `test_shape_doctor.py`'s holder fixture and `test_family_add_excludes.py`'s `empty_holder` switch to a branch; - `test_family_add_refusals.py` changes in four places: the `NEXT` expectation, `unborn_holder`, the empty-family test, and the corrupt-HEAD test. In that last test, the tag state's branch check is answered in-process, because the test is about the rollback's reading of HEAD. **How tested.** `tests/test_family_tracking_branch.py`, 17 tests: - For each act, on `main`: refused, with the holder byte for byte as it was, and for `add` nothing cloned. Then the printed `switch`, run as split, makes the branch; the act goes through on it; and its printed NEXT, run as split, pushes the branch, which the remote then holds at the act's commit. - A detached HEAD is refused. - Dry runs on `main` and on a branch say what they should. - A family made with `--tracking-branch trunk` refuses `add` on `trunk`; on a branch named `main` the dry run says the commit lands there for a pull request against `trunk`, the real run commits, and its NEXT pushes `main`. - The suggested names pass `git check-ref-format --branch`. - `init`'s NEXT STEPS open with the `switch`. - AGENTS.md, README.md and the holder's two docs show the `switch` in the same block as, and before, the first `add`. Mutations, each turning tests red: the branch check letting `main` through, 4 red; the NEXT line naming `<branch>` again, 3 red; `init`'s NEXT STEPS without the `switch`, 1 red; the tracking branch hard-coded to `"main"`, 1 red (the `trunk` test; every family test passed it before); the holder docs without the `switch`, 2 red; setup-project without the `switch`, 2 red. An independent review at 10c93de gave CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The should-fix was a holder that kept an older `scripts/repo_shape.py` through `apply --accept-local`: its new `siblings.py` could not import the shared quoter, so `make siblings`, `park` and `resume` died. The round at bb11b63 fixes it and all six nits, as described above. Re-verified at bb11b63: APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits), with SonarCloud's quality gate OK and 0.0% new duplication. The nits were the holder README's pointer to the push (now "as its `NEXT` line says"), two Sonar smells (S1192: `refs/heads/` is now `HEADS_PREFIX`; S3415: the two expected-first assertions now put the actual value first, as the file's other 32 do), and this description's list of other pull requests. All three are fixed at 3451a56: commit 11ae0a8, then a clean merge of main 994be39. The family set gave 566 passed, 8 skipped there. GitHub's update-branch then merged main cac6bb2 as 8e1471c, and main aa4e1f5 (#508, after #490, #474 and #468) was merged as 708f3bb. There only the `docs/handbook.html` footer conflicted: it takes main's clauses, #468's (#424) included, with the README blob recomputed, and `docs/cli.md` auto-merged and passes `--check`. Main 47b1e06 (#505 and #395, after #376, #374, #384, #495, #493 and #491) was then merged as fcbc7a2, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves to the end, after (#483), and the README blob is recomputed. Every run used `/usr/bin/python3 -m pytest -q -p no:cacheprovider`, with a stub `gh` first on PATH, a bogus `GH_TOKEN`, an empty `GH_CONFIG_DIR` and git's identity blanked. Run on Linux. At e42ef74 (main d41cc29, #402, merged in by GitHub's update-branch), on its tree 655c927: the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py`, `tests/test_scaffold_pin_and_reuse.py` and `tests/test_doctor_validator_rows.py`, gave **1260 passed, 12 skipped**. The skips are the same as at 5a2776d, below. At 654df49 (main 8cd7dbe merged in, with #506 and #347): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py` and `tests/test_scaffold_pin_and_reuse.py`, gave **1231 passed, 12 skipped**. The skips are the same as at 5a2776d, below. At 5a2776d (main f391e58 merged in, with #266, #509, #496 and #435): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set, plus `tests/test_leg_path_yaml_scalars.py`, gave **1171 passed, 12 skipped** (the four Windows-only skips below, and eight in that file, main's new one, whose cases the quoted templates (#259) leave with nothing to refuse). At 22733ce (main 631f087, #503, merged in by GitHub's update-branch): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set gave **944 passed, 4 skipped**. At fcbc7a2 (origin/main 47b1e06 merged in, with #505 and #395): - every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py`, `tests/test_free_plan_hint.py` and `tests/test_template_text_placeholders.py`: **844 passed, 19 skipped** (`tests/test_family_init_values.py:219` and `:291`, which skip because the template now quotes the placeholder and `id:`). - `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_setup_project_py.py tests/test_setup_sh.py tests/test_template_quoted_values.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_commit_trailers.py`: **944 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only). At 708f3bb (origin/main aa4e1f5 merged in): - every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py` and `tests/test_free_plan_hint.py`: **718 passed, 8 skipped** (`tests/test_family_init_values.py:242`). - `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_update_shape_landing.py tests/test_doctor_unfinished_merge.py tests/test_template_values_single_pass.py tests/test_stdout_encoding.py tests/test_windows_paths.py`: **813 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only). Earlier, with origin/main 48585e6 merged in: - `tests/test_family*.py tests/test_validate_family*.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_park_resume_targets.py tests/test_bootstrap_spelling.py tests/test_free_plan_hint.py tests/test_template_values_single_pass.py`: **1303 passed, 9 skipped**. - `tests/test_shape_doctor.py tests/test_update_shape*.py tests/test_commit_trailers.py tests/test_tracking_branch_ref.py tests/test_scaffold_ignored_shape_files.py tests/test_ruleset_hint_tracking_branch.py tests/test_bump_leg_dry_run_tracking.py tests/test_setup_project_py.py`: **999 passed, 6 skipped**. - The new files alone: `test_family_init_landing.py` 9, `test_family_printed_quoting.py` 69, `test_family_tracking_branch.py` 17, all passed. The full suite was not run locally. CI's three jobs are the check. #505 and #395 have since landed, and both are merged here at fcbc7a2. Each of #505's family sentences was read against this branch's code, and none is false: AGENTS.md 3 and 233; README 569, 822, 1152, 1269 and 1348; the handbook 1126 and 1506. #395 landed with its test switching to `family/members` before its real `add`, as the last bullet below says, and the first run above includes it. #503 then landed as 631f087, and GitHub's update-branch merged it here as 22733ce with no conflict. Main f391e58 (#266, #509, #496 and #435) was then merged by hand as 5a2776d, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves after #509's (#498), and the README blob is recomputed; `git show --remerge-diff` shows nothing else. #509's reworded sentences in AGENTS.md, README.md and the handbook were read beside this branch's, and none contradicts or repeats another. Its `setup-project.py` changes (the clone-path check, the plan line, the step-(7) backstop and `_main`) and this branch's `shell_word` lines in the hand-over do not meet, and `docs/cli.md` passes `--check`. Main 8cd7dbe (#506 and #347) was then merged by hand as 654df49, again with only the footer in conflict: this branch's (#487) moves after #506's (#462), the README blob is recomputed, and `docs/cli.md`, which both sides changed, still passes `--check`. #506's two `scripts/family.py` hunks (the `FAMILY_RULESET_HINT` import, and its print when the holder's push is refused) sit unchanged inside this branch's code, and that hint prints no path. Under `--reuse-empty-repo`, `init` still refuses its default `--into` inside a checkout of this standard (`family-landing-in-standard`) before it reads the remote, as a dry run with a stub `gh` showed. #506's sentences in AGENTS.md, README.md and the handbook are still true beside this branch's. Main d41cc29 (#402, for #277), which rewrote the doctor's NEXT lines and `tests/test_shape_doctor.py`, was then merged by GitHub's update-branch as e42ef74, with no conflict; the `holder` fixture still switches to `family/members` before its `add`. AGENTS.md stays at 490 lines and README.md at 1422. `git merge-tree --write-tree` of 708f3bb, at 04:47Z on 2026-10-13: - **Clean:** #503 (`fix/family-makefile-without-siblings`) and #395. Each merged tree keeps AGENTS.md at 490 lines and README.md at 1422. - **`docs/handbook.html` footer only:** #505 (`docs/v1-accuracy`) and #491. #468 has landed and is merged here. #506 conflicts there too, and in `scaffold-project.py`, as it does with main. README.md and AGENTS.md merge clean with #505, and `setup-project.py` merges clean with #491. Whichever lands second keeps every footer clause and recomputes the README blob. - **#395 merges clean but one of its tests fails with this branch.** `tests/test_template_text_placeholders.py::test_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id` inits its own holder and runs a real `add` on `main`, which #487 refuses with `family-on-tracking-branch`. Whichever of the two lands second adds `git("switch", "-q", "-c", "family/members", cwd=root)` after that test's `init`; lane 5 is adding it on #395's own branch now, so this branch needs no change for it. (#395's head d526f76 also conflicts with this head in `tests/test_adopt_manifest_values.py`, a file this branch does not touch: that is #395 against main, which the same round resolves.) - On Windows `shell_quoted` speaks PowerShell, the native shell this standard documents (`setup-project.py`), as the doctor's NEXT lines do. `git -C …`, `python scripts\family.py …` and `cd …` are shell-agnostic, so they get the platform's quoting, and no printed line joins two commands with `&&`, which Windows PowerShell 5.1 rejects. - `rm -rf`, `mv` and `mkdir -p` are POSIX commands. They are quoted for a POSIX shell on every host, and the text says "in a POSIX shell (Git Bash on Windows)". PowerShell refuses `rm -rf`'s `-rf` and deletes nothing. - The tests split a host-quoted line with a PowerShell reader on Windows and with `shlex` elsewhere, and split a POSIX-only line with `shlex` everywhere. The real-shell test is skipped on Windows. - On Windows the tests do not execute `rm` or the `mv` lines. They delete the split path with `shutil.rmtree`, and stop after checking the `mv` argv. - Paths: the cached store keeps git's POSIX spelling (`as_posix`), as before. Test paths are `resolve()`d, which expands an 8.3 short name such as `RUNNER~1`. - Nothing here was run on Windows or macOS locally. The `tests-windows` and `tests-macos` CI jobs are the check; on macOS the real-shell test runs the shipped `zsh`. - **A shipped template that imports a newer shared helper can break a holder that kept an older copy.** This change no longer does (`siblings.py` uses `shlex.join`), but nothing stops the next one: `update-shape.py apply --accept-local scripts/repo_shape.py` keeps a `repo_shape.py` that lacks a name a newly copied file imports, and `check` and the validator then read the holder as fine. The general guard is for `apply` to refuse that, as rv456 found for `bootstrap.py` and `spelled` (#456). - **The doctor's `quote_arg` leaves a word that begins with `=` bare**, which zsh expands. `shell_quoted` quotes it; the doctor is left as it is here, and the agreement test excludes that one case. - **The doctor and `update-shape.py` keep their own quoters.** The doctor's `quote_arg` flattens to ASCII by its report's contract, so moving it onto `repo_shape.shell_quoted` is more than an import change. `update-shape.py`'s POSIX `shlex` lines are lane 3's open #374. A test holds `shell_quoted` and `quote_arg` to one answer on every printable ASCII value but a leading `=`. - **`family.py`'s by-hand rollback commands (#365) and its `git config -f .gitmodules` removals (#317, #360) keep `shlex.quote`.** They were already quoted, they are POSIX-only (`printf … | git update-index`), and they are worded for a POSIX shell. - **The holder's `validate-family.py` remedies that name `family.py add`** (`member-gitlink-absent`, `member-gitlink-unnamed`) print no branch step: they are a holder copy, and the `add` they name refuses the tracking branch with its own `switch` remedy. - **`remove`'s store note still computes `.git/modules/<path>`** rather than asking `git rev-parse --git-path`, as `add` does. In a linked worktree it therefore prints no note. That predates this change. - **PowerShell verbs** (`Remove-Item`, `Move-Item`) are not printed for the POSIX-only commands. - **README's `--into` sentence gives the default but not the refusal.** README.md (861) and the handbook (1246) say `--into <dir>` is the PARENT directory, "the directory you are standing in, by default". They do not say that `init` refuses that default inside a checkout of this standard (`family-landing-in-standard`). AGENTS.md item 1 says so, and the README's own example passes `--into ..`. README.md is at its 1422-line cap, so the sentence is on #459's checklist. - **`setup-project.py`'s own `cd <clone>` line and its step-(7) backstop lines print their paths unquoted.** This change quotes only the family lines it prints. Those two predate it and #509, and they are on #459's checklist. Reviewed independently at 10c93de: CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The round at bb11b63 fixed the should-fix and all six nits, and its re-verification gave APPROVE WITH NITS (0/0/3), with SonarCloud's quality gate OK; those three are fixed at 3451a56. Main was then merged by GitHub's update-branch (8e1471c), by hand at 708f3bb and at fcbc7a2, where each time only the handbook footer conflicted, and by update-branch again at 22733ce (main 631f087, #503), with no conflict. CI was green there on Linux, macOS and Windows, with SonarCloud passing, but main had moved on: main f391e58 (#266, #509, #496 and #435) was merged by hand at 5a2776d, where again only the footer conflicted. CI was green there too, but main moved on again: main 8cd7dbe (#506 and #347) was merged by hand at 654df49, where once more only the footer conflicted. CI was green there as well, and main d41cc29 (#402) then came in by update-branch at e42ef74, with no conflict. CI is green on Linux and macOS, with SonarCloud passing, at e42ef74, a clean merge of main. All three OSes and SonarCloud passed at 654df49, and under Brett Heap's landing rule of 22:08:06Z (ruling 27) macOS and Windows carry over from that head. Main e6fbd74 (#515, for #514), which landed after it, touches none of its files, and the combined tree passed `tests/test_adopt_pins.py`, `tests/test_shape_doctor.py`, `tests/test_scaffold_leg_path_collisions.py`, the hygiene, AGENTS-code and CLI-reference tests, `tests/test_family_printed_quoting.py` and `tests/test_family_tracking_branch.py` (777 passed, 3 skipped). Closes #488 Closes #486 Closes #487 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y, 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) `scaffold-project.py --pin 007@<commit>` was accepted and exited 0. The manifest listed the product bare, ` - 007`, which the project's own YAML reader reads back as the number 7, and `2026` and `1.5` as numbers too. In a recursive clone of the root, `validate-manifest.py` passed and `validate-pins.py` exited 1 with `neutral-pin-entry-malformed`. The root's `validate.yml` runs it, so the new project's first CI run was red. The pin file said `product: 007`, which also read back as 7, from either tool, because both render that template. `_scaffold`'s table now takes the list from `_neutral_product_pins`. It keeps the order the pins were given, and `[]` when there are none. `_neutral_pin_entry` writes an entry bare when the reader reads that bare list entry back as the same text, and `double_quoted` when it does not. A double-quoted entry always reads back. The reader itself is asked, as `family.py` asks it of a member id (#259). It is asked of the list entry, because ` - -` reads as a list of its own. `openGlass`, and every name that reads back bare, keeps its line byte for byte. `007`, `2026`, `1.5` and `true` become `"007"`, `"2026"`, `"1.5"` and `"true"`. The neutral pin template now says `product: "{{PIN_PRODUCT}}"`, the way #395 quoted the other text placeholders. `render` spells the product with `double_quoted` for both tools. It is the only line of a pin file that changes, and it changes for `openGlass` too. No reader compares that field, and the file is templated rather than a shape copy, so no shape pin row changes. The tests scaffold `007`, `2026`, `1.5` and `openGlass` into local bare remotes and check a recursive clone. Each manifest line, read from git, is checked, and the list and the pin file's `product:` must read back as the text given through the reader the root ships. The root's own `validate-manifest.py --root .` and `validate-pins.py --root .` must exit 0. Two pins given out of sorted order are listed as given. A unit table checks the scaffold's spelling for numbers, keywords, plain names and spellings the reader refuses. The pin template's `product:` joins the text-field table, and an `adopt-project.py` run checks its pin file's `product:` for the same four names. Closes #516 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #259 (comment)
What changed
Brett Heap ruled on 2026-10-12: "quote #259". The templates now write each text field a tool fills from a value it was given between double quotes:
templates/assembly-root/project.yamlid,elected_on,visibility,tracking_branch, the spec leg'spath, the code leg'spathtemplates/assembly-root/contracts/spec-pin.yaml,code-pin.yamlsubmodule_pathtemplates/family-root/family.yamlid,org,created_on,visibility,tracking_branchrenderalready spells a value between a YAML template's double quotes withrepo_shape.double_quoted(#284). The single-passrenderfrom #316, now on main as 34ecbc2, does the same throughYAML_PLACEHOLDER_RE. So each value reads back exactly. An ordinary value renders as it did before, plus the two quote characters.family.yaml'sorg:is quoted along with the four fields #259 names. It is the same defect in the same file:family.py init --org 2026and--org nullpasschecked_value, and on c7c02fa the holder then failsfamily-org(#307 lists the--org nullrow). Takingorg:back out is not a one-line revert. It fails 14 of the new tests (the 8family.yaml:orgin-memory cases, and all 6family.py initrows, because every row ofFAMILIESpasses a misread--org), and it would also take theORGrow out ofTEXT_FIELDSand put plain--orgvalues intoFAMILIES. The lane keepsorg:quoted, as the same defect in the same file, unless Brett says otherwise.scripts/family.py:_member_lineswrites each member's ownproject.yamlid into the holder'smembers:block. That id now reads back exactly, so a bare007in the row came back as 7, andvalidate-family.pywould reportmember-id-mismatchfor a member it had just pinned._member_idquotes an id only when the reader would not read the bare text back as the same text. No row whose id reads back as text moves:irrsis stillid: irrs. Two edges:family.py addof a member scaffolded with--id 2026, holdsid: 2026. The nextbump,addorremoveof that holder rewrites it asid: "2026". That is harmless:validate-family.pycompares withstr()and passes before and after.family.pyfrom before this change, run against a holder this branch wrote, turnsid: "007"back intoid: 007, andvalidate-family.pythen failsmember-id-mismatch(the member declares007,family.yamlrecords7). A currentfamily.pythat rewrites the holder next then writes that row asid: "7". Nothing in this branch can prevent it: runfamily.pyfrom a current checkout on a holder that has such a member.Left unquoted on purpose:
topic(xf-project-<id>), everyrepository(<org>/<Name>; the slash keeps it text),SHAPE_REPOSITORYandDIGEST_DEFINITION(constants), and the block or list placeholders (NEUTRAL_PRODUCT_PINS,*_NAMING,SHAPE_FILES).Why
Every reader of these files uses
repo_shape's YAML subset. It reads a bare007as 7,2026and20261011as numbers,1.5as a float,trueas True andnullas None. The checks on the flags (PROJECT_ID_RE,checked_value, the leg-path rule) accept every one of these. Reproduced on c7c02fa, with local bare remotes only:scaffold-project.py --id 2026|007|true|null|20261011exits 0, and the project's ownvalidate-manifest.pythen failsmanifest-id.--spec-path 2026 --code-path 007failsmanifest-leg-path, andvalidate-pins.pyexits 2.--tracking-branch 007pushes each leg on007, and the root'sbootstrap.pythen puts the legs on a local7withfatal: no upstream configured.trueandnulldo the same onTrueandmain.adopt-project.py executewith--tracking-branch 007does the same;--id 007|20261011givesmanifest-id; leg paths2026/007givemanifest-leg-path.family.py init --id 2026|007|true|null|20261011givesfamily-id;--org 2026|nullgivesfamily-org;--tracking-branch 007|true|nullreads back as 7/True/None.On this branch every one of those rows reads back as typed, bootstraps onto the typed branch with
origin/<branch>as upstream, and passes the root's own validators.Every reader checked
Every reader reads these fields through
load_yaml/parse_yamlinto a dict. None uses a line regex,split(":"),startswith("<field>: ")or a whole-line rewrite of them:templates/assembly-root/scripts/bootstrap.py:tracking_branch, legpath.validate-manifest.py:id,elected_on,visibility, legpath.validate-pins.py:submodule_pathagainst the manifest'spath. The pin side is read as it stands and the manifest side goes throughstr(), so the two differ in type exactly when a bare value is misread (that is thepin-path-mismatchrow above). With both lines quoted they are the same text.validate-repository-naming.py:str(id)for the topic.templates/family-root/scripts/validate-family.py:id,org,created_on,visibility,str()member-id compare.siblings.py, familybootstrap.py.scripts/bump-leg.py:tracking, legpath. Itsrewrite_pinrewrites onlycommit:andtree_sha256:.update-shape.py:tracking_branch_of.rewrite_manifesttouches only theshape:block.scripts/family.py:tracking(), the member's id, and themembers:rewrite. The member-id writer is the one fixed here.shape-doctor.py: legpath, member rowpath, working-clonestr(id)compare.adopt-project.py: plan reads and_template_values.setup-project.py:family.yamlname/kindonly.scripts/shape_advisory.py: the single-repository record only.The
str()fallbacks (bootstrap.py,adopt-project.py'schecked_value) stay, because projects made before this keep their bare lines.Pins
The four files are TEMPLATED, not COPIED, so no row of any project's
contracts/shape-pin.yamlnames them. I scaffoldedAtlasand ranfamily.py init Contosoon c7c02fa and on this branch. Eachcontracts/shape-pin.yamldiffers only incommit:anddigests.tree_sha256, which identify the standard itself and change with every commit; every per-filesha256row is identical.update-shape.py checkof the c7c02fa project and holder against this branch reports every copied fileunchanged(11 and 10 rows): "no copied file differs;applywould move the pin alone".Tests
New:
tests/test_template_text_placeholders.py, 123 tests:2026 007 1.5 true null 20261011 ~ False), and must read back exactly.scaffold-project.pyruns, then a recursive clone,bootstrap.py, the three validators, andbump-leg.py, which refuses on the tracking branch and finds the leg at its path on a branch of its own.adopt-project.pyplan/check/executeruns into local bare remotes, then a clone,bootstrap.pyand the validators.family.py initruns.family.py addof members whose ids are007andtrue.true/nullthrough adopt:planwrites them bare into the plan, and the plan's reader then misreads them. That belongs to the plan writer, adopt-project.py plan writesnull,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181 (lane 1). Those two rows quote the value in the plan by hand, as adopt-project.py plan writesnull,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181's writer will, and testcheck/execute.Mutation probes: reverting any one of the 13 quoted lines fails 8 cases of its field. Reverting
_member_idfails the member test (the rows read 7/True).The
try/except YamlErrorin_member_idis guarded by the follow-up commit. The review found that calling the parse directly left the member test passing. Ids such as[,{,&a,*aand!treach theexcept(they raiseYamlErrorbare), and_member_idreturns each one exactly, as it does 34 adversarial inputs in all (the review's in-process probe).test_a_member_id_the_reader_refuses_bare_is_rewritten_between_quotes(7 cases:[,{,&a,*a,!t, an unclosed'and an unclosed") opens a hand-edited holder whose row holds the id between double quotes, rewrites themembers:block withFamily.rewritten, and asks that the id read back exactly and the text come out byte for byte. With thetry/exceptremoved all 7 fail with the reader'sYamlError, and with theexceptreturning the bare text all 7 fail too; the old member test still passes under the first of those mutants.14 existing tests that asserted or edited a bare rendered line now spell it quoted:
test_manifest_validator×5,test_pin_validator,test_reference_default,test_scaffold_e2e×2,test_setup_project_py,test_setup_sh,test_family,test_shape_doctor×2.Targeted runs on this branch:
test_template_quoted_values,test_scaffold_e2e,test_scaffold_pin_and_reuse,test_scaffold_leg_paths,test_scaffold_leg_path_collisions,test_reuse_visibility,test_reference_defaulttest_family,test_family_add_refusals,test_family_member_path,test_manifest_validator,test_pin_validator,test_bump_leg,test_update_shape,test_update_shape_pr_base,test_setup_project_py,test_park_resume_targets,test_repo_hygiene,test_cli_reference,test_setup_shtest_shape_doctorReview
An independent opus review at 233dc71 found no code defect. It asked for this body and its composition counts to be corrected, and they are. Its evidence:
load_yaml/parse_yaml, and no line regex or whole-line rewrite touches them. It readbump-leg.pyrewrite_pin,update-shape.pyrewrite_shape_pinandrewrite_manifest,family.pyrewritten, the three validators,siblings.py, bothbootstrap.py, the doctor,setup-project.py, the workflows, the Makefile andsetup.sh. No test among main's 17 new commits since c7c02fa edits one of these lines bare._member_id, the one rewriter changed. A mutant that writes every id bare fails the member test (the rows read back as{'IRRS': 7, 'IRSS': True}). A mutant that quotes every id failsb" id: irqs" in lines. So the minimal-churn choice is tested both ways.update-shape.py checkandshape-doctor.pyoutput, up to the shas.check --at 233dc71reports all 11 project files and all 10 holder filesunchangedand saysno copied file differs. The doctor's verdict isCOMPLIANT, SHAPE BEHIND (... no copied file differs)for the control and for this head. This branch'sfamily.py addandbumpon a c7c02fa holder leave the oldatlasrow and the header byte for byte, apart from the legacy-id edge under "What changed".123 failed) and pass on 233dc71 (123 passed). The 14 changed existing tests pass (49 passed).test_shape_doctor.pyandtest_family.pythe review ran only the changed tests.Follow-up commit
A second commit,
3894aa8, answers two nits from the review, and changes no behaviour. The_member_iddocstring said "no existing holder'smembers:block moves by a byte", which was false: the nextbump,addorremoveof a holder written before this change rewrites a legacyid: 2026row asid: "2026", harmlessly, becausevalidate-family.pycompares withstr(). The docstring now says exactly that, and a grep of every comment and docstring the branch added for "by a byte" and "moves" found one more copy, in the member test's docstring, corrected the same way. Thetry/except YamlErrorin_member_idis guarded by a new test (see Tests), so the new file holds 130 tests. Every count above that says 123 (123 new tests, all failing on c7c02fa and passing on 233dc71) is for the first commit; the follow-up commit adds the 7. Theexceptalso carries a comment saying which ids reach it.Composition
At the landing head (4984651).
49846518is GitHub's update-branch merge of origin/maincc8f2da4(#376, a--trailervalue carried into the commit as typed) into3220c9ee. Git reported no conflict, and the merge has no hand edit. #376 changedscripts/family.pyandtests/test_family.py, which this branch also changes. None of the lines it added undertests/names a key the templates now quote. The coordinator ran targeted tests on this head, Linux only, before CI:tests/test_commit_trailers.py,tests/test_family.py,tests/test_bump_leg.py,tests/test_update_shape.py) withtests/test_family_add_member_id.py: 421 passed, 1 skipped (a case-insensitive disk only);tests/test_template_text_placeholders.py,tests/test_template_quoted_values.py,tests/test_scaffold_manifest_values.py,tests/test_bootstrap_spelling.py,tests/test_bootstrap_after_bump.py) with the hygiene, CLI-reference and AGENTS.md-codes tests: 652 passed.At 3220c9e.
3220c9eeis GitHub's update-branch merge of origin/main70d0d1b3intoe5e768bc. It brings in three commits: #491 (da19735), in whichsetup-project.pyasks at the person's own terminal; #374 (7ab11cf), update-shape's NEXT quoting; and #384 (70d0d1b), adopt's_spelledplan values. Git reported no conflict, and the merge has no hand edit. #491 changedtests/test_setup_project_py.pyandtests/test_setup_sh.py, which this branch also changes. A grep of the lines main added undertests/for the keys the templates now quote found two hits, and neither reads a rendered template line:tests/test_update_shape_next_quoting.pywrites its ownproject.yaml, andtests/test_adopt_question_and_submodule_spelling.pyedits a plan's- path:/question:entry, whichadopt-project.pywrites bare.The coordinator ran targeted tests on the same tree (
e5a74d19), Linux only, before CI:openRepoShapecommand and doctor-row tests): 365 passed;tests/test_adopt_github_exits.pyand nine more files of this branch's sixteen-file set: 943 passed.Refreshed again 2026-10-13 (head e5e768b).
6d108d18merges origin/main56713e21into6cbbac60. That brings in two commits: 5e02abe, the doctor's andvalidate-pins.py's gitlink repairs, and 56713e2 (#479), in which adopt'splantakes the tracking branch from the source's default branch. Git reported no conflict anddocs/cli.mdauto-merged; the merge has no hand edit. Two kinds of test failed on it, and two commits fix them:7968f902changes one expected literal, attests/test_adopt_github_exits.py:675, fortest_a_source_on_another_branch_opens_its_pull_request_against_it[master]and[trunk].project.yaml, which this branch's template renders astracking_branch: "master", so it now expects the quoted line.:743andtests/test_adopt_tracking_branch.py:73-76read a plan, whichadopt-project.pywrites bare, and they stay as they are.e5e768bcaddsgit("branch", branch, cwd=source)totests/test_template_text_placeholders.py::test_an_adoption_reads_back_every_value_it_was_given.checkwithplan-tracking-branch-missing: adopt-project.py: a source whose default branch is notmainis adopted with both legs pushed, then its pull request is refused (--base main), and the printed way out repeats--base main#479 refuses a tracking branch (2026,007,1.5,20261011,true,null) the source does not have, and the test's source had onlymain.Targeted runs, Linux only:
render-cli-reference.py --checkis ok.Refreshed 2026-10-13 (head 6cbbac6).
d651b594merges origin/mainaa4e1f59(#508) intod526f768.tests/test_adopt_manifest_values.py, in four hunks, all against adopt-project.py:checkfinds, andexecuterefuses before any leg exists, a planfollow_ups:that is not a list of text and anelected_on:thatelection_datedoes not read, with or without areference:, sofollow_ups: 5is no longer a traceback andelected_on: somedayis no longer refused under a flag neither command takes (#293, #294) #356 (965420b, adopt-project.py:checkandexecuteiterate a plan'sfollow_ups:without asking for a list, so a hand-edited plan withfollow_ups: 5(ortrue) endscheckin aTypeError: ... is not iterabletraceback, andexecutein the same one after both leg repositories are created and pushed #293/adopt-project.py:checknever reads a plan'selected_on:when the plan has noreference:, soplan okis followed by anexecutethat creates both leg repositories and only then refuseselection-date-malformed, naming--elected-on, a flag neither command takes #294). adopt-project.py:checkfinds, andexecuterefuses before any leg exists, a planfollow_ups:that is not a list of text and anelected_on:thatelection_datedoes not read, with or without areference:, sofollow_ups: 5is no longer a traceback andelected_on: somedayis no longer refused under a flag neither command takes (#293, #294) #356 refuses anelected_on:thatelection_datedoes not read, aselection-date-malformed, before the validator is asked (_manifest_valuesreturns None, adopt-project.py:2258).adopt-project.py plan/check/executeinto local bare remotes:id: "007"checks clean andexecutewrites it as typed, so it stays inTEXT_THE_TEMPLATE_NOW_HOLDS.visibility: "true"gets the validator'smanifest-visibility: visibility is 'true'.elected_on: "20261011"gets adopt-project.py:checknever reads a plan'selected_on:when the plan has noreference:, soplan okis followed by anexecutethat creates both leg repositories and only then refuseselection-date-malformed, naming--elected-on, a flag neither command takes #294's one finding, "the plan'selected_on:is the text '20261011'", and the validator is not asked. That row keeps this branch's list,TEXT_THE_VALIDATOR_REFUSES, with main's sentence.said_of, as main's own rows do:test_check_refuses_a_text_value_the_validator_refusesandtest_where_a_misread_rule_judges_the_rest_the_validator_still_answers.[branch-tilde]intests/test_scaffold_manifest_values.pywas this branch's ownxfail(strict=True)until scaffold-project.py and family.py:--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355 landed as 6c2a371, so it XPASSed. The mark is off, and the row assertsREFUSED scaffold-bad-tracking-branch:.mainprints the manifest'snamewith a plainprintbefore any step runs, soname: "a\ud83db"(a lone surrogate) endsmake bootstrapin aUnicodeEncodeErrortraceback, exit 1 (make: 2), and the validator that names the problem is never reached; a valid name the stdout cannot encode ends the same way #391'stests/test_bootstrap_spelling.pyand templates/assembly-root/scripts/bootstrap.py: a tracking branch behind the pin is fast-forwarded to it, and a fresh clone's branch past the pin with no commit of its own is moved back to it, sobump-leg.pythenmake bootstrap, andgit clone --recurse-submodulesthenmake bootstrap, leave the leg on its branch AT the pin, no longer detached undernot moving an existing branch(#475) #490'stests/test_bootstrap_after_bump.pymatchedpath: specandtracking_branch: mainin a scaffoldedproject.yaml, and nine cases failed on the merge. They now match the quoted lines.initrefuses to land in the standard's checkout and names a runnablescripts/family.py, printed commands quote paths, andadd,bumpandremoverefuse the tracking branch and print their branch's push (#488, #486, #487) #507 interplay.6cbbac60switches the holder tofamily/membersbefore the realadds intest_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id, so the test holds once PR family.py:initrefuses to land in the standard's checkout and names a runnablescripts/family.py, printed commands quote paths, andadd,bumpandremoverefuse the tracking branch and print their branch's push (#488, #486, #487) #507'sfamily-on-tracking-branchrefusal lands.initrefuses to land in the standard's checkout and names a runnablescripts/family.py, printed commands quote paths, andadd,bumpandremoverefuse the tracking branch and print their branch's push (#488, #486, #487) #507's head 708f3bb, that file, all 13tests/test_family*.pyandtests/test_shape_doctor.py::test_a_sibling_at_the_right_origin_with_another_id_is_not_the_membergive 686 passed, 19 skipped.REFUSED family-on-tracking-branch.render-cli-reference.py --check: ok.Refreshed when this PR was opened (2026-10-12T15:28Z, head 3894aa8). Main is now 3ae0109 (#245, #351, #350 and #345 have landed since the results below). The head merges clean with it, and the review's re-verify ran the family tests on that merge (61 passed). #314 is now at a5438a5 and merges clean with main on its own. Its two test files changed since f45edf4 (for #282), so the 45 flips classified below are recounted when #314 next updates. The merge trees below were computed for 233dc71; the follow-up commit 3894aa8 changes only
scripts/family.pycomments and adds tests.git merge-tree --write-tree --name-only, run aftergit -c gc.auto=0 -c maintenance.auto=false fetch originon 2026-10-12:rendersubstitutes in one pass and refuses only a placeholder its table lacks, so--elected-by '{{SPEC_PATH}}'is no longer recorded asspecand{{NOPE}}no longer refuses after the legs are pushed (#260) #316 (34ecbc2), family.py:addrefusesmember-path-registeredwhen a.gitmodulessection under another name already gives the mount path, andvalidate-family.pyfindsgitmodules-path-twice, soaddno longer commits two sections for one path that a recursive clone resolves to the later one, and a holder that already has such a ghost section fails its own gate (#300) #317, family.py:addasks the holder's index for the mount path in any case, so an entryMembers/Atlas/READMEis refused asmember-path-takenbefore anything is cloned instead of leaving the member's 19 files staged under it, and a refusedaddsays the holder is exactly as it was only when the whole index is as it was (#302) #318, shape-doctor.py: a validator's child gets exactly thePYTHONPATHits row's NEXT prints -- the standard'sscripts/alone for the template copy, none at all otherwise -- so an operator whosePYTHONPATHreaches arepo_shape.pyis no longer told a root that lost its ownscripts/repo_shape.pypasses its gate, and the template run no longer keeps the operator's directories behind the standard's (#306) #311 and adopt-project.py: a relative.gitmodulesurl the plan keeps in a leg is resolved from the source's remote and from the leg's, and where the two differcheckfinds it andexecuterefuses before any leg exists, so a recursive clone no longer fails afteradoption verified(#190) #245 are on main now.--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314 (4b8da3d), scaffold-project.py: a leg path the project's own YAML reader would read back as another type (null,~,true,False,2026,1.5) is refused before anything is created, under--dry-runalike, through the one definition adopt-project.py asks, so--spec-path 2026no longer creates three repositories whose first bootstrap failsmanifest-leg-path, andadopt-project.py executeno longer saysadoption verifiedbefore that bootstrap fails (#225) #266 (3410be7), scaffold-project.py and family.py:--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355 (a03abcf), lane 1'sfix/adopt-plan-quote-yaml-keywords(d38e143),fix/adopt-record-submodule-placement(70f4d29) andfix/adopt-late-failure-notice(9f1778d), update-shape.py and bump-leg.py: a pin rewrite keeps each line's own indentation, spacing and trailing comment and reads its result back before writing it, so adigests:child indented four spaces takes the update instead ofupdate-pin-unrecognised/bump-leg-pin-unrecognised, and a# commentoncommit:ortree_sha256:survives every pin rewrite,rewrite_manifestincluded (#323) #380 (73068a3), andfix/family-init-values(14e2d68, family.py init exits 0 and pushes a holder its own validate-family.py refuses for--id null,--created-by ' ',--created-on 20261011or--name 'Contoso Group', and puts--tracking-branch trueon a branch its manifest reads back as True #307). The two that conflict, each in the same file as it does with c7c02fa and with origin/main on its own: lane 1'sfix/adopt-root-kept-symlinks(6c44591,adopt-project.py) and shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358fix/doctor-manifest-kinds-unreadable-pin(ebffb08,shape-doctor.py). This branch touches neither file.--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314 and d38e143 each conflict with origin/main on their own, inadopt-project.py. Merged with this branch, neither gains a conflict.Where another branch's rules meet this one, the tests were run on the merged tree. Quoting changes what some of their tests see, and the changes are not all one kind: some tests expected a refusal the value no longer needs, some are still refused under another rule, and some guard a case quoting does not fix. Whoever lands second must tell them apart, so each branch's flips are classified below.
--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314 (fix/scaffold-manifest-values). Its misread gate (manifest_misreads) reads the template, so once the template quotes a field it refuses nothing there. On the merge with f45edf4,tests/test_scaffold_manifest_values.pyandtests/test_adopt_manifest_values.pygive 45 failed, 140 passed. The head is now 4b8da3d, which is f45edf4 plus a merge of main's ab623dd; both test files are unchanged, and the 45 have not been re-run on it. I did not edit scaffold-project.py and adopt-project.py ask the project's YAML reader and validate-manifest.py about the manifest's values before anything exists, so--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314. The 45 are:test_a_value_the_reader_misreads_is_refused_before_anything_is_made(every id and branch row exceptbranch-tilde), the dry-run (2), remediation (5), derived-id (2) and never-reaches-gh(2) rows for ids and branches that are now accepted, 5 adopt rows (four onid-007, andtest_a_value_that_is_not_a_misread_is_still_asked_where_one_judges_the_rest, which now sees 2 findings, not 3), and 6 tests that need a bare placeholder and have none left (test_quoting_the_template_would_make_the_refusal_go_away,test_a_placeholder_is_what_render_calls_one,test_the_misread_placeholders_are_the_ones_the_misread_rule_refusesand 3 line-break rows).--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314's validator gate, in different words:plan-manifest-invalid: visibility is 'true'andelected_on is '20261011'instead of the misread rule's sentence. These aretest_a_text_value_the_reader_misreads_is_one_finding_on_this_rule_alone[visibility-true]and[elected-on-basic-format], andtest_where_a_misread_rule_judges_it_the_validator_is_not_asked_againfor the same two.--tracking-branch=~. See "Do not flip".null,~,true,False,2026,1.5) is refused before anything is created, under--dry-runalike, through the one definition adopt-project.py asks, so--spec-path 2026no longer creates three repositories whose first bootstrap failsmanifest-leg-path, andadopt-project.py executeno longer saysadoption verifiedbefore that bootstrap fails (#225) #266 (lane 2,fix/leg-path-yaml-scalars). 27 flips, all from one cause: its rule asks a hard-coded bare line,LEG_PATH_LINE = "path: {}\n", so it refuses leg paths the quoted templates now read back. 15 aretest_the_rule_agrees_with_every_template_line_that_holds_a_leg_path(null, Null, NULL, ~, true, True, TRUE, false, False, FALSE, 2026, 007, 0, 1.5, 1.50) and 12 are this branch's own end-to-end tests (6 scaffold rows fail withREFUSED scaffold-bad-leg-path, 6 adopt rows withFINDING plan-bad-leg-path). Counted on the merge with b2b42f8. scaffold-project.py: a leg path the project's own YAML reader would read back as another type (null,~,true,False,2026,1.5) is refused before anything is created, under--dry-runalike, through the one definition adopt-project.py asks, so--spec-path 2026no longer creates three repositories whose first bootstrap failsmanifest-leg-path, andadopt-project.py executeno longer saysadoption verifiedbefore that bootstrap fails (#225) #266's head is now 3410be7, whose rule reads each template line that holds a leg path, renders the value into it asrenderdoes and asks the reader, so it has no hard-coded line left. The merge with this branch is clean (tree 1e348c6); I read that head and did not re-run the 27 on it. At b2b42f8, whichever of scaffold-project.py: a leg path the project's own YAML reader would read back as another type (null,~,true,False,2026,1.5) is refused before anything is created, under--dry-runalike, through the one definition adopt-project.py asks, so--spec-path 2026no longer creates three repositories whose first bootstrap failsmanifest-leg-path, andadopt-project.py executeno longer saysadoption verifiedbefore that bootstrap fails (#225) #266 and templates/assembly-root/project.yaml and the two leg pins write eight text placeholders bare (id,elected_on,visibility,tracking_branch, the two legpaths andsubmodule_paths), so a checked value YAML reads as another type (007,2026,true,null) is misread by the project or refused by the tool; quoting them reads every one back exactly and lets numeric branch names adopt #259 landed second had to drop or re-point the bare rule; 3410be7 re-points it already.null,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181 (fix/adopt-plan-quote-yaml-keywords, d38e143). Its gate reads the template too, soplan --tracking-branch 2026|007|trueexits 0 on the composed tree (exit 2adopt-manifest-misreadon d38e143). On the merge,tests/test_adopt_plan_yaml_quoting.pyand this branch's 6 adopt end-to-end tests give 70 failed, 371 passed:plan_refuses×14,plan_names_an_id×2,check_names_an_id×4,check_reports×14 andexecute_refuses×14 (every value exceptelected_onandvisibility),text_the_loader_reads_as_other_text×7, and the two tests that need a bare placeholder (test_the_gate_follows_the_loader_when_the_loader_learns_a_keyword,test_the_template_line_says_whether_a_value_is_quoted).null,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181 loses. See "Do not flip".execute_refuses[pins='null']and[pins='True']pass alone (2 passed). An earlier case no longer refuses, and itsexecutepushes the split branch.null,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181 (yasking the reader, sotrue/null/Trueare quoted in the plan) is still needed and composes cleanly. With it,plan --tracking-branch truereaches the quoted template intact, and this branch's two hand-quoted adopt rows become flag-driven.~(1, scaffold-project.py and adopt-project.py ask the project's YAML reader and validate-manifest.py about the manifest's values before anything exists, so--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314).test_a_value_the_reader_misreads_is_refused_before_anything_is_made[branch-tilde].--tracking-branch=~is not a branch name git accepts, and scaffold-project.py and adopt-project.py ask the project's YAML reader and validate-manifest.py about the manifest's values before anything exists, so--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314 refused it only as a misread. On the merge it passes the plan and dies atgit init -q --bare -b ~ .../Atlas-spec.gitwithfatal: invalid initial branch name: '~', and against GitHub that is aftergh repo createhas made all three repositories. That is lane 2's scaffold-project.py holds--tracking-branchonly tochecked_value's alphabet, so a name git refuses as a branch (~,a..b,x.lock, a trailing/) passes--dry-runand, against GitHub, dies at the firstgit init -baftergh repo createhas made all three repositories #283 (PR scaffold-project.py and family.py:--tracking-branchis asked ofgit check-ref-format --branchbefore anything exists, so~,a..b,x.lockanda/are refused under--dry-runand no longer die at the firstgit init -baftergh repo createhas made the repositories (#283) #355).~must stay refused, by scaffold-project.py holds--tracking-branchonly tochecked_value's alphabet, so a name git refuses as a branch (~,a..b,x.lock, a trailing/) passes--dry-runand, against GitHub, dies at the firstgit init -baftergh repo createhas made all three repositories #283's ref-name check.null,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181).test_check_names_a_plan_value_that_is_not_text_by_what_was_read[tracking_branch='007'],[tracking_branch='TRUE'],[tracking_branch='1.50'],[tracking_branch='[a]'],[visibility='{a: b}'],[id='007']and[elected_on='20260902']. The plan's reader reads each as something other than text, and on the mergecheckpasses. Measured on the merge:tracking_branch: 007andid: 007are adopted silently as"7"with every validator passing;visibility: {a: b}pushes both legs and then failsmanifest-visibility;tracking_branch: [a]is still refused atexecute(unsafe-value) before any leg exists. On d38e143 alone,checkexits 1plan-manifest-misreadfor each of those four. Re-point the gate at "a plan value that is not a string" and these keep their guard.null,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181).test_check_reports_a_plan_carrying_one[elected_on='20260902']and[visibility='true'], andtest_execute_refuses_a_plan_carrying_one_before_any_leg_exists[elected_on='20260902']and[visibility='true']. The value stays invalid for the validator (DATE_RE, the visibility choices), and adopt-project.py plan writesnull,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181's misread gate caught it only incidentally. On the composed treecheckpasses andexecutepushes the legs, and the gate fails afterwards. That is what main does today, and scaffold-project.py and adopt-project.py ask the project's YAML reader and validate-manifest.py about the manifest's values before anything exists, so--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314's validator gate (plan-manifest-invalid) would catch it.test_adopt_template_values.py(adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270),test_template_values_single_pass.py(shape_materialize.py:rendersubstitutes in one pass and refuses only a placeholder its table lacks, so--elected-by '{{SPEC_PATH}}'is no longer recorded asspecand{{NOPE}}no longer refuses after the legs are pushed (#260) #316) andtest_template_quoted_values.pygive 431 passed, 1 skipped. The skip istest_adopt_template_values.py:223, which readsgit config user.nameand skips where the shared repository's.git/configsets one; it is environmental. The changed tests withtest_family_add_refusals.py(family.py:addrefusesmember-path-registeredwhen a.gitmodulessection under another name already gives the mount path, andvalidate-family.pyfindsgitmodules-path-twice, soaddno longer commits two sections for one path that a recursive clone resolves to the later one, and a holder that already has such a ghost section fails its own gate (#300) #317, family.py:addasks the holder's index for the mount path in any case, so an entryMembers/Atlas/READMEis refused asmember-path-takenbefore anything is cloned instead of leaving the member's 19 files staged under it, and a refusedaddsays the holder is exactly as it was only when the whole index is as it was (#302) #318),test_pin_validator.pyandtest_manifest_validator.pygive 132 passed. adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270'sunwritable_reasonis value-based, not template-relative, so it refuses as it did and nothing flips. Main has since added adopt-project.py: a relative.gitmodulesurl the plan keeps in a leg is resolved from the source's remote and from the leg's, and where the two differcheckfinds it andexecuterefuses before any leg exists, so a recursive clone no longer fails afteradoption verified(#190) #245 (adopt-project.pyandrepo_shape.remote_key): it adds no test that edits a bare quoted line, and I did not run it on the merge.digests:child indented four spaces takes the update instead ofupdate-pin-unrecognised/bump-leg-pin-unrecognised, and a# commentoncommit:ortree_sha256:survives every pin rewrite,rewrite_manifestincluded (#323) #380 (lane 3,fix/pin-rewrite-indentation). It rewritescommit:andtree_sha256:only, and its tests render the pins from the templates. On the merge at dc0fa7b,test_pin_rewrite_layout.pyand the 6 scaffold end-to-end tests gave 121 passed. The head is now 73068a3; the merge is clean and I did not re-run it.fix/family-init-values, 14e2d68, family.py init exits 0 and pushes a holder its own validate-family.py refuses for--id null,--created-by ' ',--created-on 20261011or--name 'Contoso Group', and puts--tracking-branch trueon a branch its manifest reads back as True #307): its tests skip themselves where the template quotes. 137 passed, 19 skipped, 0 failed.bump --dry-runfetches the member, resolves--toand prints theold -> newrow and its trailers without checking the member out or rewritingfamily.yaml, andremove --dry-runprints the row, gitlink,.gitmodulessection and directory it would remove and changes nothing, so the module'sEXIT CODESline is true of both (#326) #345 (run at 875d802; it has since landed as 3ae0109):test_family_dry_run.pyand the member test, 22 passed. Its" id: irrs"assertion holds, because ordinary ids stay bare.Left out
ystill writestrue/nullbare into the plan. That is adopt-project.py plan writesnull,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181 (lane 1).templates/assembly-root/contracts/neutral-product-pin.yamlstill writesproduct,source_repositoryanddigest_sourcebare, andfamily.pywrites a member row'sproject:bare. Neither is a templates/assembly-root/project.yaml and the two leg pins write eight text placeholders bare (id,elected_on,visibility,tracking_branch, the two legpaths andsubmodule_paths), so a checked value YAML reads as another type (007,2026,true,null) is misread by the project or refused by the tool; quoting them reads every one back exactly and lets numeric branch names adopt #259 field.tracking_branch: 007orid: 007, unquoted, in the plan) is read by the plan's reader as the number 7.str()makes it"7", and the quoted template now writes it faithfully. Foridthis is a change: on c7c02fa a bareid: 007failed loudly (validate-manifest: manifest-id, manifest lineid: 7), and on this branch it is adopted silently as id"7"and every validator passes.tracking_branch: 007was already adopted silently as7. The plan side is not this branch's: the guard against it belongs with adopt-project.py plan writesnull,trueand other YAML keywords unquoted, soplan --spec-path nullsilently mounts the leg at the defaultspec#181 (adopt plan writes YAML keywords and non-text values unquoted), whose gate should refuse a plan value that is not a string. The case is narrow, becauseplan --id 007itself writesid: "007"; only a hand edit reaches it.docs/cli.mdis unchanged because no--helptext changed.Closes #259
🤖 Generated with Claude Code
Summary by Sourcery
Quote text placeholders throughout project and family templates so user-provided values remain text across rendering, validation, bootstrapping, adoption, and family updates.
Bug Fixes:
Enhancements:
Tests: