Repository navigation
adopt-project.py: check finds, and execute refuses before any leg exists, a plan follow_ups: that is not a list of text and an elected_on: that election_date does not read, with or without a reference:, so follow_ups: 5 is no longer a traceback and elected_on: someday is no longer refused under a flag neither command takes (#293, #294) - #356
Conversation
…er a file can hold it, in `plan`, in `check` and in `execute` before any leg exists, so a byte that is not UTF-8 in `--elected-by`, `--reference` or a local `--source` directory no longer pushes both legs before `write_lf` raises (#255) Since #247 the plan writer keeps a lone surrogate (a byte that is not UTF-8, as Python decodes argv and paths) as `\udce9`, and the reader gives it back. `plan --elected-by b'Jos\xe9'`, `--reference b'ref-\xe9'` and a source under a directory `caf\xe9` with local remotes therefore passed `plan` and `check`, and `execute` created and pushed both leg remotes before `write_lf` raised `UnicodeEncodeError` (exit 1). Before #247 `plan` raised and nothing was created. A hand-edited `\ud800` in `elected_by`, `elected_on`, `reference`, `visibility`, a pin or a follow-up ended the same way, and a NUL or a line break there went into the manifest. `shape_materialize.unwritable_reason` asks the question, beside `one_printable_line`: one line, no lone surrogate, no control character. It is `one_printable_line` less `Cf`, and it takes the empty value: a Persian name joins with U+200C, which a file holds and `execute` adopted before #247 and since, and an empty `elected_by` is what `plan` records when `git config user.name` names nobody. - `plan` refuses `--elected-by` and `--reference` as `adopt-unwritable-value` and writes no plan. - `check` reports `elected_by`, `elected_on`, `reference`, `visibility`, each pin and each follow-up as `plan-unwritable-value`, through `_entry_findings`, so `execute` refuses the same findings in `check`'s words through `_refuse_what_check_finds`, before `_create_leg_remotes`. - `execute` refuses a local clone URL (the source's own path under `--local-remote-dir`) as `adopt-unwritable-value`, right after `_repository_urls`. `check` cannot know it will be written, and against GitHub it is an https URL, so `plan` and `check` still pass such a directory, as #247 made them. - `Source.open` refuses a `local_path` that does not exist and is not text as `source-unresolvable`, where a NUL or a surrogate reached `print` or `subprocess` as a traceback. - `check`'s WHAT WILL HAPPEN shows the plan's visibility, adopt branch, leg paths and follow-ups through `_shown`, so a surrogate there no longer raises on a strict stdout before the finding is printed. tests/test_adopt_template_values.py covers each argv route, the local source directory, each hand-edited value, `local_path`, the printed values, and controls (`José`, U+200C, a `café` source directory) that still adopt. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…value `execute` writes is refused before anything exists, and the clone-URL refusal's remediation moves the source and plans again, so no README line is shown reversed and following the remediation adopts instead of raising (#255) Fix round 1 on PR #270, answering lane openRepoShape-2's review at aed9190 (F1, F2, F3, and N1's test point). F1. `unwritable_reason` was `one_printable_line` less all of `Cf`, so U+202A..U+202E and U+2066..U+2069 passed `plan`, `check` and `execute` and went raw into `project.yaml`, the README's `Elected by` line and the split commit's message; a viewer showed the rest of that line reversed. Issue #255's question, `one_printable_line`, refuses them. The question is now `one_printable_line` less `Cf` other than those nine (`BIDI_CONTROLS`, by code point), plus the empty value. The joiners U+200C and U+200D, U+00AD, U+FEFF and the marks U+200E, U+200F and U+061C stay writable: a Persian name needs U+200C, and a mark sets no span's direction. `plan` refuses such a flag as `adopt-unwritable-value`, `check` finds such a plan value as `plan-unwritable-value`, `execute` refuses it before any leg exists, and a local clone URL holding one is `adopt-unwritable-value`. F2. The test file held a raw U+202E, which SonarCloud's text:S6389 flags, and other invisible literals a reader cannot see. Each is now an escape; only `é` and `日本` stay literal. The two composite assertions SonarCloud's python:S9073 flagged are four, each with a message. F3. The clone-URL refusal said to clone the source to a UTF-8 path and pass it to `execute` with --source. Followed, that raised: the clone's `origin` is the old path, and `_remote_repository` reads it as UTF-8. The remediation now says to rename or move the source and run `plan --source <its new path>`, `check` and `execute` again, and the test follows it to `adoption verified`, for a byte that is not UTF-8 and for a bidi override in the directory's name. N1. The same-sentence test now asserts both exit codes and that no run raised. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er.name` as `--elected-by (not given, so `git config user.name`)` when it refuses it, so the refusal no longer names a flag the person never passed (#255) Fix round 1 on PR #270, N2 of lane openRepoShape-2's review at aed9190. With no --elected-by, `plan` records `git config user.name`. A TAB there was refused as `--elected-by is 'Jo\tSmith'`, naming a flag the operator never passed. It is now named as #181's `FLAG_DEFAULTS` names a value `plan` filled in, and the remediation says to pass the flag or correct `git config user.name`. A test runs `plan` under `GIT_CONFIG_GLOBAL` holding a TAB name, and skips where a repository's own `user.name` answers first. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the manifest back through the reader instead of matching its line, so it holds on main, which since #250 writes a YAML template's value as `double_quoted` spells it (#255) Fix round 1 on PR #270. Built on the merge with main be2790e, the U+200C control failed: `execute` adopted, but main b30595c (#250) renders a value between a YAML template's double quotes through `double_quoted`, so `project.yaml` holds `"AliReza"` with the character spelled as an escape, and the test looked for it raw. The reader gives back U+200C either way, so the control now asks the reader, and also asks for the name in the shape's README, which is not YAML and holds it as it stands on this branch and on main. GitHub's checks run on the pull request's merge with main, so they would have failed here. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lvable` before anything runs, so `gh repo clone` is no longer handed it as its first argument, where gh's own parser read it as one of its options (#255) SonarCloud's S8705 on #270 asked whether a value reaches a command as an option. The lane's re-verify found one that does: `plan --source=-u/x` is no path that exists and holds a `/`, so `Source.open` took it for an `org/repo` name and ran `gh repo clone -u/x <dir> -- --quiet`. gh read `-u` as `--upstream-remote-name` with the value `/x`, and `--source=--upload-pack=x/y` as a flag it does not know. A hand-edited `local_path:` or `repository:` reaches the same call from `check` and `execute`. It cannot reach git as an option (gh hands git only what follows its `--`), and main does the same. The guard sits in the condition this PR already edits. No GitHub owner name begins with `-`, so nothing gh could clone is refused, and an existing path that begins with `-` is still taken as a path. The tests put a stub `gh` first on PATH that records each call: `plan` with either value, and `check` and `execute` on a plan whose `local_path:` holds it, each exit 2 `source-unresolvable` with no traceback and no leg, and the stub records nothing. Against 7e9cdc3's code all four fail with the stub's record of `repo clone -u/x ...` and `repo clone --upload-pack=x/y ...`. A control shows the stub records the clone of `testorg/Thing`. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `_entry_findings` composed with #255's, and this PR's pair renamed `unwritable_findings`/`unwritable_remediation` #231 (6f45bf9) puts each entry's path first in `_entry_findings` and returns `unsafe_remediation` ahead of the rest; this branch appends each value `execute` writes into a file that no file can hold. The two hunks are its docstring's order sentence and its return. The sentence keeps main's order and appends this PR's clause; the return is `(findings + submodule_findings + unwritable_findings, unsafe_remediation + remediation + unwritable_remediation)`, so the paths still come first, as #196 needs. This PR's local pair `value_findings, value_remediation` is renamed `unwritable_findings, unwritable_remediation`. #261's branch (ca21b57) binds `value_findings, value_remediation` in the same function, from `_manifest_findings`: a resolution keeping both lines under one name would silently drop one PR's findings. With the rename it cannot. The rest of main (#254/#281, #238/#287, #203/#236, #204/#227, #249/#250, #241, #215 and the earlier merges) merges with no conflict. The resolution is the lane's re-verify one (`resolve-main.py`, 385 passed on 6f45bf9), rename aside. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s `_entry_findings` order sentence composed with #255's #313 (71b58a1, #262 and #263) makes `check` find a kept root path where the shape writes a directory or its `shape/` copy, through `_root_shape_findings` called from `_leg_findings`, and makes `shape_materialize` refuse a copy whose own path is taken. This branch appends each value `execute` writes into a file that no file can hold to `_entry_findings`. The one conflict is that function's docstring, the sentence naming the order of its findings: it keeps #313's words (each entry's `leg:`, the two mount paths and what the root keeps where the shape writes, then the source's own submodules) and appends this PR's clause, ", then each value `execute` writes into a file that no file can hold (#255)". Nothing else in the file conflicts: the return still reads `(findings + submodule_findings + unwritable_findings, unsafe_remediation + remediation + unwritable_remediation)`, and `_leg_findings` carries #313's findings inside `findings`. Main's c0ed615 (#308, family.py) comes with it and touches none of this branch's files. Main has since moved on to c7c02fa (#315, family.py); this merge stops at 71b58a1. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… exists, a plan `follow_ups:` that is not a list of text, so `follow_ups: 5` is no longer a `TypeError` traceback and `abc` is no longer written as three follow-ups (#293) `plan` writes `follow_ups:` as a block list, and its three readers (`_plan_values`, `_print_what_will_happen`, `_commit_the_split`) iterated the value with no test that it was one. A hand-edited `follow_ups: 5`, `true`, `false` or `1.5` ended `check` in `TypeError: ... is not iterable`, exit 1, with no FINDING naming the key. On main `execute` died the same way in `_commit_the_split`, after both legs were pushed; since #270 it dies in `_plan_values`, before any leg, but still as a traceback. `abc` went into the split commit's message as `- a`, `- b`, `- c`, and an item `- true` or `- null` as `True` or `None`. The plan is untrusted input (`cmd_execute`'s own comment), so the value is asked before it is used: - `_plan_follow_ups` is the one read for the three readers: the items of text of a `follow_ups:` list, and nothing for anything else. - `_follow_ups_findings` is a `plan-bad-follow-ups` FINDING for a `follow_ups:` that is present and not a list, naming what it holds (`the number 5`, `the text 'abc'`, `a mapping of 1 key(s)`), and one for each item that is not text. An empty list and a bare key stay valid. - They join `_entry_findings` through `_plan_field_findings`, last, so `check` reports them and `execute` refuses them through `_refuse_what_check_finds`, before `_create_leg_remotes`, under the finding's own code, with a remediation that says how `plan` writes the list. Tests in tests/test_adopt_execute_refusals.py: `check` (a finding, exit 1, no traceback) and `execute` (the refusal, no leg, no split branch) for nine edits, three controls, and one run through `main` with `git filter-repo` that is refused with exit 2 and then adopts from the corrected plan. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted_on:` whether or not the plan has a `reference:`, and `execute` refuses what it finds before any leg exists, so `elected_on: someday` is no longer refused after both legs are created, under `--elected-on`, a flag neither command takes (#294) `check` never read `elected_on:`. `execute` read it in `_template_values` only for a plan with no `reference:`, where `default_reference` asks `election_date`. That runs after `_create_leg_remotes`, so a hand-edited `elected_on: someday` passed `check` (`plan ok`). `execute` then created both leg repositories and only afterwards refused `election-date-malformed`. The refusal named `--elected-on` and said to re-run with it, but only `plan` takes that flag. A second `execute` met `leg-remote-exists`. With `reference:` kept, the date was never read, and `execute` wrote `someday` into the manifest. `_elected_on_findings` asks `election_date` about `str` of what the plan holds. That is the value `_template_values` takes, and the rule `plan` asks of `--elected-on`. It asks whether or not the plan has a `reference:`. A date it refuses is a FINDING `election-date-malformed` that names the plan's `elected_on:` and no flag, with a remediation that says to edit the key in the plan file. It joins `_plan_field_findings` ahead of the follow-ups, in the order the plan file holds the two keys. `execute` refuses it through `_refuse_what_check_finds`, before any leg exists. The rule stays `election_date`'s, unchanged. It reads `2026-W41-6` and `20261011` from Python 3.11, and the project's validator refuses both. That is #261's to rule on (PR #314 holds `election_date` to the validator's `DATE_RE`). Asking the same function keeps `plan`, `check` and `execute` in agreement now, and asks the tighter rule with no edit once it lands. Only the words are this caller's: `election_date`'s refusal names `--elected-on`, which is right for `plan` and wrong here. Only that refusal is a finding about the date. Anything else `election_date` raises is raised as it is, so a date it never judged is not reported as malformed. An example is #314's `manifest-validator-unusable`, when the validator it consults is missing. Tests in tests/test_adopt_execute_refusals.py: - `check` and `execute` for five dates (four with no `reference:`, one with it kept), and four controls; - `check`'s verdict equals `election_date`'s on the running interpreter, for `2026-W41-6`, `20261011`, a date and a word; - a stricter `election_date`, stood in by monkeypatch, refuses `2026-W41-6` before any leg; - another refusal from `election_date` is raised as it is; - both findings in one plan; - one run through `main` that is refused with exit 2 and no leg, then adopts from a corrected plan with no `reference:` and records the reference its date chooses. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 10 hours and 52 minutes by commenting |
Reviewer's GuideThe PR moves validation of late-read plan fields into the shared preflight/refusal path: malformed Sequence diagram for execute refusing malformed plan valuessequenceDiagram
participant Plan as Plan file
participant Execute as execute
participant Findings as _entry_findings
participant Fields as _plan_field_findings
participant Date as election_date
participant Refuse as _refuse_what_check_finds
participant Legs as _create_leg_remotes
Execute->>Plan: read plan
Execute->>Findings: validate plan
Findings->>Fields: _plan_field_findings(plan)
Fields->>Date: election_date(str(elected_on))
Date-->>Fields: date or election-date-malformed
Fields-->>Findings: follow-up and date findings
Findings-->>Execute: findings
Execute->>Refuse: _refuse_what_check_finds(findings)
alt finding exists
Refuse-->>Execute: Refusal
Execute-->>Plan: stop before leg creation
else no findings
Execute->>Legs: _create_leg_remotes
Legs-->>Execute: legs created
end
State diagram for execute plan acceptancestateDiagram-v2
[*] --> PlanLoaded
PlanLoaded --> PreflightValidation
PreflightValidation --> Refused : malformed follow_ups or elected_on
Refused --> [*] : no leg created
PreflightValidation --> PlanAccepted : no findings
PlanAccepted --> LegsCreated
LegsCreated --> [*]
Flow diagram for plan validation before leg creationflowchart TD
Plan["Hand-edited plan"] --> Check["check"]
Check --> Findings["_entry_findings"]
Findings --> Fields["_plan_field_findings"]
Fields --> FollowUps["_follow_ups_findings"]
Fields --> ElectedOn["_elected_on_findings"]
FollowUps --> Verdict{"findings?"}
ElectedOn --> Verdict
Verdict -->|"no"| PlanOK["plan ok"]
Verdict -->|"yes"| CheckFinding["FINDING with remediation"]
PlanOK --> Execute["execute"]
CheckFinding --> Execute
Execute --> Refuse["_refuse_what_check_finds"]
Refuse -->|"finding exists"| Refused["REFUSED; no leg created"]
Refuse -->|"no finding"| Create["_create_leg_remotes"]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
#270 (fix/adopt-template-values-utf8) landed on main as the squash f795cf7. Its final head was 3cee853, which had merged main e43fa95. This branch was stacked on #270 and carries an older copy of #270's commits, so a plain merge of main would conflict in adopt-project.py, where the squash and those commits touch the same lines. Merging #270's final head comes first so the next merge, of origin/main, can take this branch's side of the files #270 changed. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ter #270 landed as the squash f795cf7 #270 landed on main as the squash f795cf7, and this branch was stacked on it. The previous merge, ead7716, brought #270's final head 3cee853 into this branch. This merge brings in main. adopt-project.py conflicted, and this merge takes this branch's own version of the whole file. That is right because the squash's content of adopt-project.py (and of tests/test_adopt_template_values.py) is 3cee853's, which the previous merge already brought in: `git diff 3cee853 f795cf7` over those files is empty. Of the main commits not in 3cee853, only f795cf7 touches adopt-project.py, tests/test_adopt_template_values.py or scripts/shape_materialize.py. So main adds nothing to those files beyond what this branch already carries, and no other file conflicted. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…landed, with one guard in _manifest_values and #314's 5 elected_on: rows retargeted to election-date-malformed #314 (scaffold-project.py and adopt-project.py ask validate-manifest.py about the manifest's values) landed on main as 3ef059c. Its adopt-project.py is a5438a5's merged with 92e29d9, the composition pr-body-v3.md planned. adopt-project.py conflicted in 2 hunks of _entry_findings, both kept: - the docstring lists #261's validator findings, then #190's relative urls, and last this PR's plan values (#293, #294); - the return is findings + split + unwritable_findings + value_findings + moved + field_findings, the remediations in that order, and notes. Main's auto-merged paragraph said the relative urls of #190 come "last"; it now says the plan values come after them. The guard: _manifest_values returns None when _elected_on_findings(plan) has a finding, right after its _check_names refusal. Without it, a plan with no reference: had check exit 2 on election_date's own refusal, raised from _template_table through default_reference and naming --elected-on, before this PR's finding was listed; with reference: kept, the date was two findings, the validator's and this PR's. In plan the guard is a no-op: _checked_plan_inputs asks election_date of --elected-on first. #314's 5 rows for an elected_on: that election_date now refuses get this PR's one election-date-malformed finding, so they are retargeted to it in tests/test_adopt_manifest_values.py (said_of gives each row's finding, REFUSED prefix and remediation), and none is dropped: - elected-on-iso-week in test_check_finds_a_value_the_validator_refuses, test_execute_refuses_it_before_any_leg_exists and test_check_finds_it_from_a_tree_with_no_git_as_in_a_checkout; - elected-on-basic-format in test_execute_refuses_a_text_value_the_reader_misreads_before_any_leg and test_a_text_value_the_reader_misreads_is_one_finding_on_this_rule_alone, where _manifest_findings is now ([], []) and _plan_field_findings holds the one finding. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is refused and a name holding a lone surrogate is a finding, so `pins: [a, b]]` is not the pin `b]` and `name: "a\ud83db"` no longer kills `make validate` (#301, #280) (#390) Two reader-side defects the lane filed, in one commit on `fix/yaml-flow-brackets-surrogates`, cut from main c7c02fa. Below, "this head" is that commit, 21fbea4, the one the review approved; main was merged into the branch afterwards (last paragraph). **#301, `scripts/repo_shape.py`.** In YAML a plain scalar inside a flow collection ends at any of `[ ] { } ,`. `_split_flow` counts brackets to find its commas and never asked whether the count went below zero or ended above it, so a stray bracket was kept as text: - `pins: [a, b]]` read `['a', 'b]']`; - `k: [x [a, b]]` read `['x [a, b]']`; - since #284, `k: {a"]:}` read the key `a"]`. PyYAML refuses each. A new `_refuse_flow_indicator` refuses a PLAIN item or value (called from `_flow_value`) and a PLAIN key (called from `_split_key_flow`) of a flow collection that holds `[`, `]`, `{` or `}`. The refusal is a `YamlError`, so `load_yaml` turns it into `REFUSED yaml-unparsable` and names the scalar: ``a plain scalar in a flow collection holds `]`, which ends it in YAML: 'b]'``. These still read as before: - a quoted scalar's brackets (`k: ["a]", {"b}": "c]"}]`); - nested and empty flow collections; - a bracket in a BLOCK plain scalar (`k: a[b]`, `- see [1]`); - a bracket in a comment after a flow. No other function changed. **#280, `templates/assembly-root/scripts/validate-manifest.py`.** `_name_finding` now reports a display name holding any of U+D800 to U+DFFF as `manifest-name`: ``` FINDING manifest-name: name is 'a\ud83db', which holds U+D83D, a lone surrogate: no UTF-8 file, commit message or console can hold it. Write each character as itself, or as one `\U` escape (`\U0001F600`, not the pair `\uD83D\uDE00`). ``` The finding goes to stderr with the name escaped by `repr`, before the `manifest ok` line could raise. **Why the #280 refusal is in the validator and not in the reader.** Two places could refuse a lone surrogate, the reader and the validator. The evidence rules out the reader: - **The writer emits a lone-surrogate escape on purpose.** The escape is `\uDXXX`, a `\u` and four hex digits from D800 to DFFF. `double_quoted('caf\udce9')` is `"caf\udce9"`. That is how `adopt-project.py`'s plan keeps a path byte that is not UTF-8 (#199), and `check` must read it back. `tests/test_adopt_entry_paths.py` holds that round trip (120 passed with `test_adopt_plan.py` on this head). Lane 1's pending #189 branch (70f4d29, `submodule_record`) also writes `\udce9` escapes into `project.yaml`'s `submodules:` block, and its docstring relies on the reader resolving them. A reader refusal would break both. - **PyYAML 6.0.3 agrees with the reader.** The pure-Python `SafeLoader` reads `name: "a\ud83db"` as the lone surrogate. Only libyaml (`CSafeLoader`) refuses it, and it refuses even the escape pair `"\ud83d\ude00"`, which the Python loader reads as two surrogates, as this reader does. - **The defect is in a value, not in the YAML.** A display name is text a person chose, and no UTF-8 stream holds a surrogate. On c7c02fa and on this head, with local bare remotes only: | launcher | c7c02fa | 21fbea4 | |---|---|---| | `scripts/validate-manifest.py` | exit 1, `UnicodeEncodeError` traceback | exit 1, the `manifest-name` finding | | `make validate` | exit 2 on that traceback | exit 2 on the finding | | `shape-doctor.py --root .` | `INVALID (manifest)`, the row quotes `UnicodeEncodeError: ...` | `INVALID (manifest)`, the row quotes `FINDING manifest-name: ...` | | a real `bump-leg.py` | exit 0, prints `manifest ok: a\ud83db`, and COMMITS the subject `Bump spec leg to <sha> in a?b` (`errors="replace"`) | exit 2 `bump-leg-validators-red`, every byte rolled back, 0 commits | | `update-shape.py apply` | exit 0, prints `manifest ok: a\ud83db`, commits | exit 2 `update-validators-red`, restored, 0 commits | So all four launchers now agree, and each names the problem. **Readers and callers this changes.** - **#301:** every `parse_yaml`/`load_yaml` caller, and only for a document whose flow collection holds a plain scalar with a bracket. Such a document was read, or misread, before and is refused now. Every tracked YAML file loads the same on c7c02fa and on this head (14/14 raw, 18/18 rendered templates). So do the 6 YAML files of a scaffolded project (`project.yaml`, four `contracts/*.yaml` and `validate.yml`): 6/6 on a c7c02fa scaffold and 6/6 on a 21fbea4 scaffold. No writer emits such a scalar: - every flow sequence written in the tree is bare names or `[]` (adopt's `pins: [...]` and `allow_empty_legs: [...]`, `naming_block`'s `also_matches` and `referent_chain`); - `double_quoted` quotes any value holding a bracket. - **#280:** the project's own `scripts/validate-manifest.py` (a digest-pinned shape copy; it reaches existing projects through `update-shape.py apply`, which reports it `upstream-changed`). Every caller of that copy now stops at the finding: - `make validate`; - the CI step in `validate.yml`; - `shape-doctor.py`'s `manifest` row; - `bump-leg.py` and `update-shape.py apply` (their red-validator refusal and rollback); - a family holder's `make validate`, which runs each member's. `bootstrap.py` runs the same copy in its step (b), but on such a name it dies at its own first `print`, before step (b) (see Left out). With #314 merged as well, the scaffold's pre-check asks this validator about `--name`. In the tree of this head and #314 alone, `scaffold-project.py --name $'caf\xe9' --dry-run` is refused `scaffold-manifest-invalid` with this finding before anything is created (checked at #314's head 4b8da3d). With #351 (landed as d82a83e) and without #314, that `--name` is already refused, as `scaffold-unwritable-value`. **Evidence.** - **Issue rows.** All ten #301 documents reproduce on c7c02fa and are refused on this head. 8 are documents PyYAML refuses. 2 (`k: {a:}#}: b}`, `k: {}#":}`) PyYAML reads only by taking a `#` with no space before it for a comment, and they are refused by design, as `k: "a"#c` is. Every #280 row is in the table above. - **Seeded oracle fuzz.** Corpus: an earlier review's, seed 2490250, 785,646 documents in six contexts. PyYAML 6.0.3 is the oracle only. | | c7c02fa | 21fbea4 | |---|---|---| | read the way PyYAML reads it | 254,629 | 254,629 | | read, where PyYAML refuses | 179,999 | 112,562 | | refused, where PyYAML reads | 48,434 | 49,492 | | read, but not as PyYAML reads it | 2,330 | 1,272 | | the flow contexts alone: read where PyYAML refuses / refused where PyYAML reads | 77,073 / 15,413 | 10,306 / 16,465 | Only two kinds of document change: - 67,437 invalid documents c7c02fa read are refused; - 1,058 documents c7c02fa MISREAD are refused. 1,037 of these are PyYAML's `#` leniency, and 21 are a single-pair mapping in a flow sequence (`k: [a:[]]`), which the subset never read as a mapping. Nothing c7c02fa read correctly changes, nothing is newly read, and there are 0 crashes. Against the issue's two-line `depth < 0` sketch, run on the same corpus, this head refuses everything the sketch refuses, plus 21,631 more: 21,608 invalid documents (the opener class, such as `k: [a[]` and `k: [x [a, b]]`) and 23 misreads. The fuzz ran on 38738cf, whose `scripts/` and `templates/` are byte-identical to 21fbea4's. - **Tests fail on c7c02fa.** With the four test files copied onto c7c02fa: 45 failed, 191 passed. The failures are 31 bracket rows, 10 validator rows and the 4 launchers. - **Mutation probes.** Each hunk was reverted or weakened on its own, and each mutant fails new tests: - M1, no check on items and values: 23 failed; - M2, no check on keys: 8; - M3, no quoted-scalar exemption: 14; - M4, no quoted-key gate: 11; - M5, closers only: 3; - M6, no surrogate finding: 10; - M7, upper bound excludes U+DFFF: 2; - M8, lower bound excludes U+D800: 2. **Tests.** - New: `tests/test_yaml_flow_brackets.py`, with the ten issue rows, sixteen more positions, the refusal's wording, a file refusal, fifteen still-read rows and the writer round trip. - In `tests/test_manifest_validator.py`: five surrogate spellings, each under a pipe and under `PYTHONIOENCODING=utf-8`, plus four still-ok controls on either side of the range. - In `tests/test_validator_child_encoding.py`: a real bump, `update-shape.py apply`, the doctor and `make validate`, each on `name: "a\ud83db"`. - `tests/test_yaml_quote_boundaries.py`: its docstring said `k: "a"#c` was the only PyYAML disagreement. There are eight; seven are `BRACKET_IN_A_KEY` rows refused by design. Targeted runs on 21fbea4, Linux, Python 3.12.3, git-filter-repo 2.47.0, each in the background: - the reader and validator files (`test_yaml_flow_brackets`, `test_yaml_flow_quotes`, `test_yaml_flow_mappings`, `test_yaml_quote_boundaries`, `test_yaml_block_scalar_keys`, `test_load_yaml_refusals`, `test_adopt_plan_escapes`, `test_manifest_validator`, `test_pin_validator`, `test_neutral_pin_validator`, `test_template_quoted_values`): **462 passed**; - `test_validator_child_encoding`, `test_stdout_encoding`, `test_repo_hygiene`, `test_cli_reference`: **165 passed, 1 skipped** (`test_stdout_encoding.py:286`, a Windows-only default); - `test_adopt_plan`, `test_adopt_entry_paths`: **120 passed, 1 skipped** (`test_adopt_plan.py:393`, `SHAPE_MEDXEHR_CLONE` unset); - `test_shape_doctor`: **242 passed, 3 skipped** (`test_shape_doctor.py:5330`, PowerShell on a Windows host only); - this head merged with origin/main 86da808, which carries #316: `test_template_quoted_values`, `test_template_values_single_pass`, `test_yaml_flow_brackets`, `test_yaml_quote_boundaries`, `test_manifest_validator`: **412 passed**; - this head merged with #314 (ab66bc0): `test_scaffold_manifest_values`, `test_adopt_manifest_values`, `test_manifest_validator`, `test_yaml_flow_brackets`: **293 passed**. The full suite and the macOS and Windows legs did not run before this PR's CI. **Merge-tree, as of 2026-10-12T15:12Z** (`git merge-tree --write-tree --name-only`), against origin/main d82a83e and the heads named here; each sha is the one it had then, and main has moved since. - **Clean with this commit:** - origin/main d82a83e, which carries #316 (34ecbc2), #245 (head a0272b2, landed at 14:55Z as 161c6c9) and #351 (landed as d82a83e). #245's one change to `scripts/repo_shape.py` is in `remote_key`, away from the YAML reader, and #351 touches `scaffold-project.py` only; - #314 (4b8da3d). With it merged, the `--name` refusal above still holds (checked at 4b8da3d); - lane 1's `fix/adopt-late-failure-notice` (9f1778d) and `fix/adopt-plan-quote-yaml-keywords` (d38e143); - the sibling `fix/pin-source-env-collisions` (34dbd86, #275); - every other open PR head but one: 28 of the 29 open, all but #358. - **Conflicts that are not this commit's.** Each conflicts the same way with origin/main alone, in a file this commit does not touch: - #358 (ebffb08), in `shape-doctor.py`; - lane 1's `fix/adopt-root-kept-symlinks` (6c44591), in `adopt-project.py`. - **Five heads already conflict with origin/main itself, and merge clean with this commit:** #314, #356 (5a5011d), #382 (7e55d00) and lane 1's `fix/adopt-plan-quote-yaml-keywords` in `adopt-project.py`, and lane 1's `fix/adopt-late-failure-notice` in `tests/conftest.py`. - **One conflict is this commit's:** `fix/adopt-record-submodule-placement` (70f4d29, lane 1, #189), in one hunk, the import block of `tests/test_manifest_validator.py`. Both branches add to it: that branch adds `import os` and `import sys`, and this one adds `import subprocess` between them, so the resolution is all three. Its `repo_shape.py` and `validate-manifest.py` changes auto-merge. Not resolved in either branch. **The review.** An independent opus review at 21fbea4 approved it with nits: 0 blocker, 0 should-fix, 5 nit. All five are in text (this description, the author's report and the commit message), none in the code or the tests. Four were fixed in the description; the fifth, the commit message's spelling of the surrogate escape as a six-digit one, is fixed in this message: the escape is `\uDXXX`, a `\u` and four hex digits, D800 to DFFF. Its own seeded structured fuzz (seed 7301280: 120,000 nested flows in five block contexts, 600,000 documents, PyYAML 6.0.3 as the oracle only) found 0 changed reads: no document c7c02fa read the way PyYAML reads it changed, and nothing crashed. An instrumented reader returned no plain scalar holding `[ ] { }` inside a flow on this head, against 76,955 on c7c02fa. The author's seed 2490250 counts reproduced row for row, and all 13 mutants it tried were killed. **664 passed** on this head merged with origin/main ab623dd (16 test files; 361 on the head alone, 8 files). The one conflict, with lane 1's #189 branch, is the `tests/test_manifest_validator.py` import block, and resolves to all three imports. Three follow-ups it confirmed are not in this change: the bootstrap's raw name print and the flow entries still read (both under Left out), and a block value that opens with `]` or `}`, which c7c02fa and this head both read where PyYAML refuses. **Left out.** - **A plain flow entry that opens with an indicator, and an empty flow entry.** Most of what this head still reads where PyYAML refuses is one of two classes: - an entry opening with `:`, `#`, `?` or `-` (`k: {:":}` reads `{':"': None}`, `k: [:]`; 6,286 flow documents); - an empty entry from a leading or doubled comma (`k: [,a]` reads `['a']`, `k: [a,,b]` reads `['a', 'b']`; 3,858). Neither is a bracket, and #301 does not claim them. - **A single-pair mapping in a flow sequence.** `k: [a: b]` still reads as the string `'a: b'`, where PyYAML reads `[{'a': 'b'}]`. Only the ones holding a bracket are now refused. - **`templates/assembly-root/scripts/bootstrap.py`.** It prints the display name raw (`bootstrap: <name> (<root>)`) before its validators run. On a project named `"a\ud83db"`, `make bootstrap` still dies there with `UnicodeEncodeError`, on c7c02fa and on this head (filed as #391). - **`elected_by` and `reference`.** A surrogate in either is still accepted. Neither is printed by the validator, so nothing raises; the scaffold side of such values is #282. - **The escape pair `"\ud83d\ude00"`.** It is still two surrogates, not one character, as PyYAML's Python loader reads it. The finding asks for the character itself or its one `\U` escape instead of the reader inventing a joining rule. Reviewed adversarially at 21fbea4 (APPROVE WITH NITS: 0 blockers, 0 should-fixes, 5 nits, all in text and none in the code or the tests). The code and the tests needed no fix round; the nits are fixed in this message. Main was merged into the branch after the review with no conflict: at that merge (c1a119d, main 45e01f4 merged in) the branch's own diff against main is the one reviewed, the same 6 files with 422 lines added and 6 removed. CI is green on Linux, macOS and Windows, with SonarCloud passing, at c1a119d (main 45e01f4 merged in). The combined tree with main 3ef059c (#314 landed) was run locally before landing. Closes #301 Closes #280 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check finds, and execute refuses before any leg exists, a plan follow_ups: that is not a list of text and an elected_on: that election_date does not read, with or without a reference:, so follow_ups: 5 is no longer a traceback and elected_on: someday is no longer refused after both legs are created, under a flag neither command takes (#293, #294)check finds, and execute refuses before any leg exists, a plan follow_ups: that is not a list of text and an elected_on: that election_date does not read, with or without a reference:, so follow_ups: 5 is no longer a traceback and elected_on: someday is no longer refused under a flag neither command takes (#293, #294)
|
(lane openRepoShape-2, 2026-10-12T19:39Z) This lane reviewed its own main-merge resolution on this PR at 10323a7 (a2ec12e's merge of #314, then main dd8d0ea), read-only in its own detached worktrees, with nothing authored. MERGEABLE. Land once the three-OS CI is green. Every item below is follow-up.
The Tests at 10323a7, covering every
The report is |
…landed: `_commit_the_split` keeps #445's reported steps and reads `follow_ups:` through `_plan_follow_ups` Main moved from dd8d0ea to 580d7df: decff87 (#267), dba5fb0 (#403), 245d775 (#446), 75052cb (#445), ec8e09a (#406) and 580d7df (#416). Only #445 touches adopt-project.py, and it conflicted in one hunk of `_commit_the_split`. #445 wraps that function's body in a `try` that names each step (cloning the source for the split, mounting the legs, committing the split, pushing the split branch), so a failure once both legs are pushed flushes stdout, prints `REFUSED <step>.`, what failed and one notice of what the run left. This branch reads `follow_ups:` there through `_plan_follow_ups`, the one read of it (#293). The resolution keeps #445's `try`, steps and reports whole, and carries this branch's read into its "committing the split" step: git had kept main's `[str(f) for f in plan.get("follow_ups", [])]` inside the `try`. The two contracts meet without overlap. `plan-bad-follow-ups` and `election-date-malformed` are findings of `_entry_findings`, which `execute` refuses through `_refuse_what_check_finds`. That runs before #445's `_refuse_an_occupied_work_dir`, `_refuse_a_split_branch_left_behind` and `_create_leg_remotes`, so these are refusals before any leg exists: exit 2, `REFUSED <code>:` and the remediation that says no leg repository was created and nothing was pushed, as #445's own refusals before the legs say, and no notice. #445's report covers the failures once a leg exists, which these values no longer reach. The guard in `_manifest_values` is unchanged; #445 does not touch it. #445's comment over `_template_values` in `cmd_execute` named "an `elected_on` that is not a date" as a refusal the table can raise. With #294's finding, `_refuse_what_check_finds` refuses such a date first, so the comment now says the date no longer reaches the table. #445's test of a table refusal stubs `_template_values` and is unchanged. tests/test_adopt_execute_refusals.py: the two end-to-end runs through `main` (#293, #294) now also assert that the refusal says no leg was created and that #445's notice ("NOTHING has been rolled back") does not follow it. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… before its date guard, so `check` and `execute` refuse a plan with a bad `elected_on:` and a bad organisation or id for the same first problem (#294) The guard this PR added to `_manifest_values` (return None when `_elected_on_findings` has a finding, so #314's validator is not asked about a date this PR already reports) returned before the function's `checked_value("org", ...)` and before `_template_table` read `plan.project_id`. Those two are where `check` meets a bad organisation and a bad id; `execute` meets the organisation earlier, in `_repository_urls`, and the id here, through `_refuse_what_check_finds`. So with `elected_on: someday` beside them: - `org: "-evil"`: `check` exited 1 listing only the date, and `execute` exited 2 `REFUSED unsafe-value`; - `id: "North Wind"`: `check` exited 2 `REFUSED plan-bad-id` (from the topic line it prints later), and `execute` exited 2 `REFUSED election-date-malformed`. On main 580d7df both commands name `unsafe-value`, and `plan-bad-id`, for those plans. The organisation is now refused, and the id read, before the guard, so both commands name the same first problem again, as on main, and a date-only plan is still one finding in the plan key's own words, not `plan-manifest-invalid` beside it. The docstrings of `_manifest_values` and `_manifest_findings` now also say they return nothing for such a date. tests/test_adopt_execute_refusals.py: `test_a_bad_date_does_not_change_what_check_and_execute_refuse_first`, for the organisation and the id, with `reference:` kept and removed: `check` exits 2 on the code `execute` refuses with, and no leg is created. All 4 fail on 88c430c's code and pass here. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-2, 2026-10-12T22:16Z) This lane re-verified its own PR at 5982bbe (the main merge 88c430c and the guard-order fix), read-only in its own detached worktrees, nothing authored. MERGEABLE WITH NITS. #435 merges clean onto 5982bbe but conflicts with main 8782333 in
Tests, 0 failed: 2448 passed, 4 skipped (MedxEHR, Windows) on 5982bbe + main d6df36b + #401; 2649 passed, 4 skipped with main 36538fd; 359 passed with main 8782333. merge-tree with d6df36b, 36538fd, 8782333, da27e69 and 3efcc7a: rc 0. Report: |
… exists, a source symlink with an empty target and the source's own `project.yaml` kept in the root, and `plan` no longer offers `root` for that file (#303, #305) (#387) Two ways `check` said `plan ok` to a source `execute` could not adopt, both reproduced on c7c02fa with the issues' own scripts. **#303, a symlink whose target is empty.** A `120000` entry whose blob is the empty blob passed `check`, which never reads a link's target. - With symlinks on (git's default where the disk can hold one), `execute` created both leg repositories, then failed in its first clone of the source: exit 2, nothing on stderr, two empty legs left behind. Linux refuses a symlink to nothing. This happened whatever the plan answered: `docs/empty-link` sent to spec, and a top-level `empty-link` answered `root` or `drop`. - With `core.symlinks=false`, it said `adoption verified`. The spec leg held the empty link, a clone of that leg exited 128, and the assembly's recursive clone exited 1. The top-level link answered `root` or `code` verified the same way. Only `drop` gave an assembly that clones. **#305, the source's own `project.yaml` answered `root`.** `plan` asked about it under `ambiguous-top-level-yaml`, whose question offers `root`. Answered `root`, `check` said `plan ok`. `execute` wrote the shape's manifest beside the source's file as `shape/project.yaml`, said "nothing was overwritten" and `adoption verified`. `shape-doctor.py` then said NOT A SHAPE ROOT (exit 2) and pointed at `adopt-project.py plan`. `make bootstrap` failed `naming (exit 2), manifest (exit 1), pins (exit 2)`. Answered `code` or `spec`, the doctor said COMPLIANT and `bootstrap ok`. **What changed.** - **`plan-empty-symlink` (#303).** `_empty_symlink_findings` reports each `120000` row of size 0 in `Source.tree()`. The size `ls-tree -l` gives a symlink is the length of its target, so no target is read; a SHA-256 repository reports size 0 too. - It is listed right after #196's `plan-unsafe-path`, through `_source_path_findings`, on the line `_entry_findings` already had. So `check` prints it, and `execute` refuses it through `_refuse_what_check_finds` before any leg exists (#168). - `EMPTY_SYMLINK_REMEDIATION` is said once after the findings, in `check` and in `execute`'s refusal. The fix is in the source: give the link a target, or remove it, commit, and re-run `plan`. - `plan` notes each such link in `check`'s words, and still writes the plan. - **`ambiguous-project-yaml` (#305).** A new rule in `contracts/path-classification.yaml`, above `ambiguous-top-level-yaml`, for the file `project.yaml`. Its question asks spec, code or drop, and says `root` is not an answer for this file. `release.yaml` keeps the old question. - **`plan-root-blocks-manifest` (#305).** #313's machinery is extended, not copied: `_shape_blocks`, which `_root_shape_findings` runs over each disk, now refuses the root holding `project.yaml` outright, with no copy question after it. It fires for a kept row at that path (file or symlink), or a kept directory there, on a disk that keeps case or one that ignores it. The finding's remediation is #313's `_fix_the_kept_path`. - A submodule there stays `plan-submodule-root`'s, said once. - `_kept_there` is the at-or-under lookup the copy branch already did, factored out for both. - `execute` refuses it before any leg exists, through the same list. - **Files.** `adopt-project.py` (+205/-13), `contracts/path-classification.yaml` (+28), the two new test files (399 and 366 lines) and `tests/test_adopt_root_shape_paths.py` (+6/-2). **Decisions.** - **#303 is refused whatever the plan answers, and whatever `core.symlinks` says.** - Where symlinks are made, no answer adopts. The run fails at the first clone of the source, which checks the whole tree out before anything is taken out. Measured with the link in `docs/` (sent to spec), at the top level answered `root` and `drop`, and with every path rooted or dropped and both legs seeded: there the seeded legs were pushed and the assembly's clone exited 128. - `check` cannot know where `execute` will run. A check that passed `drop` with symlinks off would pass a plan the next Linux machine cannot adopt, and the root's own `validate` workflow runs on `ubuntu-latest`. - The remediation costs one commit in the source. - **#305 covers the manifest only.** The issue asked whether other rendered root paths need the same rule. Measured on c7c02fa, with the source's own file kept in the root at each of the 18 `assembly_root_paths()`: - `check` passes all 18. - These verify over a broken assembly: `contracts/spec-pin.yaml` and `contracts/code-pin.yaml` (DRIFTED, pins fail); `scripts/validate-pins.py`, `scripts/validate-manifest.py`, `scripts/repo_shape.py` and `contracts/repository-naming.yaml` (INVALID); `scripts/validate-repository-naming.py` (COMPLIANT, naming fails); `scripts/bootstrap.py` (COMPLIANT, and `make bootstrap` runs the source's own script). - `contracts/shape-pin.yaml` fails verification after both legs exist. - README.md, AGENTS.md, CLAUDE.md, .gitignore, .gitattributes, AGENTS-shape.md and the workflow come out COMPLIANT with the copy beside them. `Makefile` is rooted by default, and the follow-up merge is its designed exit. - `project.yaml` is the one path `plan`'s own question offered `root` for. The others are reached only by hand-editing a `contracts/` or `scripts/` answer to `root`. They are left for a follow-up issue (under Left out), not folded in here. - **A symlink at `project.yaml` is refused too.** On c7c02fa: - a link to the source's own file verified and was NOT A SHAPE ROOT; - a link to nothing had the manifest written through it, and came out COMPLIANT on Linux. A clone that makes no symlinks holds that link as a file naming its target, so it is refused anyway, and #183 refuses a link to nothing at a shape path regardless; - a directory `project.yaml/` verified and was NOT A SHAPE ROOT. - **A case twin `Project.yaml` kept in the root is refused, with the case aside.** It adopts COMPLIANT on Linux. On a disk that ignores case, `place` writes the manifest beside it. This is the trade #172 and #313 already make. **Evidence.** In this section and in the two below it, "this head" is 69c54c9, the branch's own commit and the one the review approved; main and #314 were merged into the branch afterwards (last paragraphs). - **Reproductions, on this head** (exits as printed): - #303 run 1: `check exit 1`, one `plan-empty-symlink` finding; `execute exit 2`, `REFUSED plan-empty-symlink`, `leg repositories left:` empty. - #303 run 2 (`core.symlinks=false`): the same finding and refusal; no leg, no verification. - Top-level `empty-link`, symlinks off and answered root, code or drop, and symlinks on and answered root or drop: each `check exit 1`, `execute exit 2`, no leg. - #305 root: `check exit 1`, one `plan-root-blocks-manifest` finding; `execute exit 2`, `leg repositories:` empty. - #305 code, spec and drop: `plan ok`, `adoption verified`, the root's `project.yaml` begins `schema_version: 1`, doctor COMPLIANT, `bootstrap ok`. - The plan's entry for `project.yaml` now reads `rule: ambiguous-project-yaml`, with no `(root)` in its question. - `project.yaml` as a link to a file, a link to nothing, a directory, and the twin `Project.yaml`: each `check exit 1` and `REFUSED plan-root-blocks-manifest`. - No row needs real GitHub or a cloud tool. - **New tests.** `tests/test_adopt_empty_symlinks.py` (28) and `tests/test_adopt_root_manifest.py` (27). - The symlinks are made with `git update-index --cacheinfo`, so no disk symlink is needed. The one test that checks a link out (a link given a target, end to end) is skipped on Windows by its marker, before it runs. - On c7c02fa: 37 failed, 18 passed. The 18 are the controls that must pass on both sides: a symlink with a target, the remediations adopting, `project.yaml` sent to a leg or dropped, a submodule there, other shape paths keeping their copy, and `release.yaml`'s question. - **One existing test changed.** `test_a_file_kept_at_a_path_the_shape_writes_is_not_a_finding` in `tests/test_adopt_root_shape_paths.py` held that a kept file at any shape path is no finding. It now holds that for every path but the manifest, which is `plan-root-blocks-manifest`. - **Mutation probes:** 14 of 14 hunks reverted one at a time, each killed by at least one new test. - **Targeted runs.** The adopt list and the doctor ran on a local state of this branch (7a22a21) whose files were this head's but for `tests/test_adopt_root_shape_paths.py`. That file was re-run on this head, so the adopt list is two runs, not one sum. - The 15 adopt files (`test_adopt_empty_symlinks`, `_root_manifest`, `_root_shape_paths`, `_leg_path_collisions`, `_execute_refusals`, `_plan`, `_e2e`, `_submodules`, `_verify`, `_mount_ignored`, `_ignored_shape_files`, `_entry_paths`, `_finding_spelling`, `_history_gitlinks`, `_spec_only`) on 7a22a21: 837 passed, 1 failed, 1 skipped (a MedxEHR clone the plan test needs). The one failure is the #313 test this head changes. - `tests/test_adopt_root_shape_paths.py` alone, on this head: 120 passed. - `tests/test_shape_doctor.py`, as its own stream: 242 passed, 3 skipped (PowerShell lines only a Windows host prints). - `tests/test_scaffold_leg_path_collisions.py`, `tests/test_repo_hygiene.py` and `tests/test_cli_reference.py`: 167 passed. - This head merged with origin/main at 87373a6, which brings `tests/conftest.py`'s new `git()` identity: the two new files, `tests/test_adopt_root_shape_paths.py` and the three above, 342 passed. **The review.** An independent opus review at 69c54c9 approved with nits: 0 blocker, 0 should, 8 nit. It reproduced every row of both issues on c7c02fa and on this head, and killed 6 of 6 mutants: two of the author's, re-run, and four of its own. Its targeted runs on this head merged with origin/main ab623dd (`git merge-tree` clean, with #349's session `gh` guard in place): - the new files, `tests/test_adopt_root_shape_paths.py`, `tests/test_scaffold_leg_path_collisions.py`, `tests/test_adopt_execute_refusals.py`, `tests/test_repo_hygiene.py` and `tests/test_cli_reference.py`: 379 passed; - the other adopt files, in two streams: 303 passed, 1 skipped (the MedxEHR clone), and 323 passed; - the clone-template, literal-names, template-values, plan-escapes, stdout-encoding and Windows-path tests, which cover #268 and #270: 266 passed, 2 skipped (an operator `user.name` set; Windows only); - `tests/test_shape_doctor.py`: 254 passed, 3 skipped (PowerShell lines only a Windows host prints). c7c02fa's own `tests/test_adopt_root_shape_paths.py` against this head's code gave 1 failed (the `project.yaml` row, `:191`) and 119 passed, so no other #313 behaviour changed. - **A cross-lane composition, with lane 1's #183.** On the tree merged with lane 1's `fix/adopt-root-kept-symlinks` (#183, 6c44591), `tests/test_adopt_root_symlinks.py::test_a_link_with_an_empty_target_is_fixed_in_the_source` fails: `AssertionError: assert 'plan-empty-symlink' == 'plan-root-symlink'`. An empty link at or above a shape path is reported by both findings, and `execute` refuses with the first, which is this branch's. The two remediations agree (fix it in the source). Whichever of the two lands second removes the duplicate report. The same file also fails 3 other tests on origin/main with #183 and without this branch (`[a-link-at-the-copy-itself]` and `test_the_materializer_refuses_before_it_writes`, two cases), because #313's `plan-root-blocks-shape-copy` and `materialize-copy-blocked` answer first; those are not this branch's. - **Two known nits, left for a later round.** - An empty link AT `project.yaml` (or at a #313 copy path) gets `plan-root-blocks-manifest`, whose fix offers `drop`, and `plan-empty-symlink`, which says `drop` is no fix. - `plan` notes every empty link (25 links print 25 NOTEs), where `check` names 20 and counts the rest. - The other nits are smaller. A symlink target that begins with a NUL byte passes `check` and fails like #303, and the issue's `_verify` bullet, are follow-up issues; the case twin `Project.yaml` is under Left out. The new tests' `SYMLINKS_OFF` replaces the Windows job's own `GIT_CONFIG_COUNT` settings (`core.autocrlf=false`, `core.longpaths=true`) for those child processes, which is harmless while none of them checks anything out. **Merge-tree, as of 2026-10-12T15:14Z.** `git merge-tree --write-tree --name-only` of this head (69c54c9), against heads fetched then; every sha below is the one it had then, and main has moved since (see the merge paragraph below). It is a textual merge only; the semantic meeting with #183 was tested by the review (the first item above). - origin/main: clean at 0f67f7b. That includes #316 (34ecbc2), #268 (e43fa95), #270 (f795cf7) and #245 (161c6c9), which have landed since this branch was cut. It was clean at 161c6c9 and at ab623dd, the review's merge, too. - Open PRs touching `adopt-project.py`: #184 (f416e13), #353 (dcbc37b), #356 (5a5011d), #371 (34b9500), #373 (5ac03de), #377 (c35c896), #378 (2da8691), #382 (7e55d00) and #384 (a96297e) are all clean. #356 and #382 conflict with origin/main alone (2 and 5 blocks, from #245's landing), not with this head. - #314 (4b8da3d) was NOT clean: one block, in `_entry_findings`' docstring, where each side adds a paragraph after the same one (this head's "THEN THE SOURCE'S EMPTY SYMLINKS (#303)" and #314's "A VALUE NO FILE CAN HOLD"). Keep both. It had been clean at f45edf4, and it had 3 blocks against origin/main alone since #245 landed, so it needed a merge of main in any case. - Lane 1's branches without a PR: `fix/adopt-late-failure-notice` (9f1778d), `fix/adopt-record-submodule-placement` (70f4d29) and `fix/adopt-plan-quote-yaml-keywords` (d38e143) were clean. `fix/adopt-plan-quote-yaml-keywords` was 3 blocks against this head at e337f44, and conflicts with origin/main alone in 4. - `fix/adopt-root-kept-symlinks` (6c44591, #183) conflicts in 3 blocks of `adopt-project.py`: the module docstring's `check` paragraph, `_entry_findings`' docstring and its return line. They are the same 3 that c7c02fa itself has with that branch, and this branch's one added docstring line sits inside the first. It has 5 blocks against origin/main alone. - Lane 5's `fix/adopt-pin-writes-pin-file` (d806e8f): clean. `docs/adopt-execute-exits` is #371, above. - When #183 lands, an empty symlink kept at or above a shape path is reported twice, by `plan-empty-symlink` and by #183's `plan-root-symlink` (the first review item above). A dangling link kept at `project.yaml` is reported twice too, by `plan-root-blocks-manifest` and `plan-root-symlink`, and an empty one three times. Whichever lands second should keep one. **Brought onto main past #314 (bbbb541).** Since this branch was cut from c7c02fa, main took #245 (relative `.gitmodules` urls), #270 (`plan-unwritable-value`), #371 (`execute` verifies before it exits on a refused `gh pr create`), #377 (`plan` asks about every path `execute` will not extract) and #314 (`plan-manifest-invalid`, 3ef059c), all in `adopt-project.py`. The merge of origin/main 3ef059c has ONE conflict, in `_entry_findings`' docstring, where this branch adds "THEN THE SOURCE'S EMPTY SYMLINKS (#303)" and #314 adds "A VALUE NO FILE CAN HOLD IS SAID BEFORE THE VALIDATOR'S WORD ON IT (#255 before #261)" after the same paragraph: both are kept, this branch's first, because it describes the head of the list and #314's its tail. Nothing in code conflicted. `check` and `execute` read one list, and it composes as each feature says: a source's unsafe paths (#196), its empty symlinks (#303), coverage, `_leg_findings` with `plan-root-blocks-manifest` (#305), #166's split, #270's `plan-unwritable-value`, #314's `plan-manifest-invalid`, then #245's relative urls last. So an empty symlink beside an invalid value is still refused wearing `plan-empty-symlink`, with no leg created; probed on the merged tree with an empty link plus `visibility: secret`, plus a line break in `visibility`, and with `project.yaml` answered `root` plus `visibility: secret`, each listing the findings in that order. No test expectation or order changed: the merged tree was rehearsed before #314 landed (69c54c9 + 45e01f4 + c6f441b) and is the same tree object as the pushed merge, and `git diff 3ef059c bbbb541` is this PR's five files and nothing else (+1004/-15). Targeted runs on bbbb541: the new files, `test_adopt_root_shape_paths`, `test_adopt_manifest_values`, `test_scaffold_leg_path_collisions`, `test_agents_md_codes` and `test_cli_reference` 418 passed; `test_shape_doctor` and #377's, #245's and #270's adopt tests 595 passed, 4 skipped (PowerShell lines only a Windows host prints; one operator `user.name`); the other adopt files 234 passed, 1 skipped (the MedxEHR clone) and the guards, literal-names, clone-template, plan-escapes, gitmodules, leg-paths, stdout and Windows-path tests 1101 passed, 1 skipped. **Left out.** - `_verify` and the materializer do not themselves refuse a root `project.yaml` that is not the shape's manifest. This is the issue's second bullet. Once `execute` refuses the plan before any leg, through the same list `check` prints, nothing reaches them with one. It is a follow-up issue. - A top-level file named `project.yaml` only once case is ignored (`Project.yaml`) is still asked `ambiguous-top-level-yaml`'s question, which offers `root`. `check` refuses that answer with the case aside, before any leg. - #171 (a failed clone exits 2 silently) is lane 1's PR #184. This PR only keeps the empty-symlink run from reaching that clone. - Follow-up issues proposed by this PR (two since filed): - the source's own file kept in the root at one of the other shape paths the root's tools read (the pins, the validators, `scripts/repo_shape.py`, the naming policy, `scripts/bootstrap.py`, `contracts/shape-pin.yaml`), each measured above; - a symlink whose target begins with a NUL byte (`\0spec.md`, size 8), which `plan-empty-symlink`'s size test does not see: it fails like #303 (filed as #388); - `_verify` asking whether the split's root `project.yaml` is the shape's manifest (filed as #389); - an entry git cannot check out where `execute` runs, other than an empty symlink or a NUL-led one (a target longer than the platform allows, say), which is still found only by the first clone, after both legs exist. #303 leaves this as a note. Reviewed adversarially at 69c54c9 (APPROVE WITH NITS: 0 blockers, 0 should-fixes, 8 nits). The review reproduced every row of both issues on c7c02fa and on that head, and killed 6 of 6 mutants. No fix round followed: the branch's own commit is as reviewed, and what was added after it is the merge described above. CI is green on Linux, macOS and Windows, with SonarCloud passing, at fce86fa (main 7a8b981 merged in). Its merge with main a73b4e9 passed the targeted set (the three adopt test files, `tests/test_shape_doctor.py` and `tests/test_scaffold_leg_path_collisions.py`): 597 passed, 3 skipped. Closes #303 Closes #305 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d `open_glass` share one `SHAPE_PIN_SOURCE_<PRODUCT>`, so `--pin` refuses the pair and `validate-pins.py` notes a declared one and reads the variable for neither (#275) (#409) `pin_source_env_name` turns every character outside `[A-Z0-9_]` into `_`, so `open-glass`, `open_glass` and `open.glass` are three legal product names, with three pin files (`contracts/<product lowercased>-pin.yaml`), and one variable, `SHAPE_PIN_SOURCE_OPEN_GLASS`. On ab623dd (this branch's main parent; the same rows reproduce on c7c02fa), with the issue's own probe against local bare remotes and a `gh` that exits 97: | step | ab623dd | this head (6494714) | |---|---|---| | `scaffold-project.py --pin open-glass@<A> --pin open_glass@<B>` | exit 0, three repositories | exit 2 `scaffold-pin-collides`, nothing created | | `validate-pins.py`, no source, on a project that already declares the pair | exit 0, both skips offer `SHAPE_PIN_SOURCE_OPEN_GLASS` | exit 0, one `note neutral-pin-source-collides`; the skips offer `--pin-source <product>=<path>` and a sibling checkout only | | `SHAPE_PIN_SOURCE_OPEN_GLASS=<open-glass>` | `neutral-pin-source-unreadable` for `open_glass`, exit 1 | exit 0, the note; the variable is read for neither pin, so each pin is read from its own source or skipped | | `SHAPE_PIN_SOURCE_OPEN_GLASS=<open_glass>` | `neutral-pin-source-unreadable` for `open-glass`, exit 1 | the same | | `--pin-source open-glass=... --pin-source open_glass=...` | exit 0 | both pins recompute, exit 0, the note | **What changed.** Every variable name is kept: an operator may have set one and the docs name them. What is done instead: - `scripts/repo_shape.py`: `pin_source_env_collisions(products)` is the one pure answer, `{variable: [products]}` for each variable two or more distinct products share. Distinct means distinct PIN FILES (`pin_product_key`, `lower()`, which is what `contracts/<product lowercased>-pin.yaml` is named by), so a product typed twice or in two cases is #241's file collision and never this one. `pin_source_env_shared` is the set of those keys. - `scaffold-project.py`: `_refuse_colliding_pins` (left where it is) names each variable's `--pin` arguments as typed after #241's file sentences, in the same `scaffold-pin-collides` refusal. Dry run and real run say the same no, before any process starts and ahead of the leg-path refusal. #241's wording and remediation are unchanged when only a file collides. A NEW pair is still refused, so the situation is not created anew. - `templates/assembly-root/scripts/validate-pins.py`: one NOTE per variable, `neutral-pin-source-collides`, naming every product and the variable, the keyed `--pin-source <product>=<path>` that reads each pin, and that which declaration stays is the human's decision. It is never a finding and never moves the exit code. A product whose variable is shared does not read it (rule 2 of `resolve_neutral_pin_source`), and its skip hint offers the keyed `--pin-source <product>=<path>` and not the variable or the bare form. A variable one product has to itself reads and is offered exactly as before. - `scripts/repo_shape.py`, the two chain readers: `link_pins_from_trees` does not read a variable two links of one chain share, and the `declared-unverified` hint stops naming it. Only a hand-written chain can have two such links, since scaffold takes every link but the first from the naming policy's alphanumeric `open<Product>` form. Each reader of `pin_source_env_name`, decided: | site | decision | |---|---| | `scaffold-project.py --pin` | refuse (above) | | `validate-pins.py` rule 2 and the skip hint | a note, and the variable is neither read nor offered for a shared product | | `repo_shape.py` `resolve_link_source` (via `link_pins_from_trees`) | skip the variable when another link of the chain shares it | | `repo_shape.py` `_chain_link_findings` hint | omit the variable for a shared link | | `adopt-project.py --pin`, `plan`, `check`, `execute` | not in this PR, see below | **Fix round 1 (from the review of 34dbd86).** **The design change: a declared pair is a NOTE and not a finding. This is the lane coordinator's decision, and Brett may reverse it.** The first round made `neutral-pin-source-collides` a FINDING. Main through ab623dd scaffolds such a project (`--pin open-glass@A --pin open_glass@B` exits 0), and `update-shape.py apply` runs the NEW `validate-pins.py` over the rewritten root, so for a project already declaring the pair the finding ended every apply as `REFUSED update-validators-red`, rolled back, for this update and every later one, until a person deleted a true declaration of descent. AGENTS.md tells an agent never to edit "a pinned file, a digest or a manifest to make a row go green", and the finding's remediation was exactly that, so the person was locked out. The coordinator chose the review's option (b): it is a NOTE now. The guard stays (the variable is read for neither pin), each pin still verifies from its own source, scaffold still refuses a new pair, and `validate-pins.py` says the rest in one line. To put the finding back, restore 34dbd86's `report.finding(...)` call in `_report_pin_source_collisions` (it is not a word swap for `report.advise`: `advise` takes one string, `finding` a code and a detail), give the finding the remediation 34dbd86 had, and turn the tests that expect the note (and `test_a_project_declaring_a_colliding_pair_takes_a_shape_update`) back into exit 1; the first round's versions of all of them are on 34dbd86. - **Proved on a real old project.** Main (ab623dd) scaffolded the pair into local bare remotes; `update-shape.py apply` to the first round's head 34dbd86 exited 2 `REFUSED update-validators-red` and rolled back (`git branch` shows only `main`); to this head it exits 0, the project's validator passes and prints the note, and the doctor's `pins` row is `ok` before and after. The doctor prints `passes` for a validator that exits 0 and does not echo its stdout, so the note itself is visible in `update-shape.py apply`'s output and in `validate-pins.py`'s, and the verdict moves for no reason but the update (`COMPLIANT, SHAPE BEHIND` to `COMPLIANT`). It is now a test: `test_a_project_declaring_a_colliding_pair_takes_a_shape_update`, over local stand-ins beside the project, so no `gh` is asked. - **Collisions by the variable.** Pairs were decided by `casefold()`, but the pin file is `lower()`. `openß`/`openss`, `openſ`/`opens`, `openfi`/`openfi` and `openς`/`openσ` (the letters are U+00DF, U+017F, U+FB01 and U+03C2 beside U+03C3) share a variable and have two pin files, and were never found. They are now, from the pin-file fold (`pin_product_key`). A pool of 31 names is checked pairwise against `neutral_pin_path` and `pin_source_env_name` themselves, and no single product is ever a collision. They are reachable only from a hand-written `project.yaml`. - **Windows.** The guard's only test needed the `#!` `gh` stub and was skipped on Windows. `test_a_shared_variable_answers_for_neither_pin_with_no_gh` puts a checkout of each pin beside the project, names one in the variable, and needs no `gh`: on ab623dd it is `neutral-pin-source-unreadable`, here each pin reads its own sibling. The hint and the non-ASCII rows are tested in-process, with no skip and no filesystem. - **The hint.** For a product two pins share, the skip hint offers the keyed `--pin-source <product>=<path>`: the bare form answers for every pin with no more specific source, so with two pins it gives the other the finding this issue is about. - **Trailers and title.** The trailers of the earlier commits (a30091f and 34dbd86 name Sonnet 5.5) are settled in this message, which carries Opus 5.5. The PR title now names `repo_shape.py`. - `scaffold-project.py` is not touched this round: `fix/adopt-pin-writes-pin-file` moves that code, and the change would only conflict with it. **Left out.** - `adopt-project.py` is untouched. Its half arrives with #273's PR, which moves scaffold's pin machinery into a shared module and routes adopt through `pin_source_env_collisions`. Once it lands, the one place that still keys by `casefold()` (`pin_variable_collisions` in `scripts/shape_materialize.py`) should read `pin_product_key`; it cannot differ for an ASCII `--pin`, which is all `PIN_ARG_RE` takes. - `scaffold-project.py`'s own `_colliding_pin_variables` also keys its argument lists by `casefold()` and one docstring there still says the project's validator reports the pair "as a finding"; both go with the code when #273 moves it. #312's `PinSources` keys `--pin-source` by `casefold()` too, so `openß=` and `openss=` are one key there; that is #312's. - A product in `neutral_product_pins:` and a chain link in `naming.referent_chain` that share a variable are read by two validators in one process, and neither sees both sets. Not detected here. - The doctor does not print a passing validator's stdout, so its `pins` row does not carry the note. Not changed: it would add a file to this PR. - No `--help` text changed, so `docs/cli.md` is untouched. `README.md` is at its line cap and its blob is pinned by `docs/handbook.html`, so the one README sentence this wants needs a regenerated handbook and waits for its own change. - `validate-pins.py` and `repo_shape.py` ship into projects, so every project's `shape-pin.yaml` reads `upstream-changed` for both until `update-shape.py apply`, which now succeeds for a project that declares such a pair. **Evidence.** All on the head (6494714), with git-filter-repo 2.47.0 on PATH, a `gh` that cannot be reached (a stub first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`), and the repository-local `user.name` blanked at command-line scope, as the standard's checkout sets one that two scaffold tests read. - `tests/test_pin_source_env_collisions.py` is 137 tests and passes on this head. Laid over 34dbd86, 50 fail and 87 pass: the note and update tests, the non-ASCII rows, the hint, the sibling guard, and `pin_product_key` itself (which 34dbd86 does not have); the scaffold refusals, the chain readers and the controls hold on both. - Targeted, run on ad6f569, whose tree differs from this head in `tests/test_pin_source_env_collisions.py` only (the last edits made one comparison read the committed blob and wrapped one line): the new file, `test_pin_validator`, `test_neutral_pin_validator`, `test_cli_reference`, `test_manifest_validator`, `test_naming_policy` and `test_repo_hygiene` **445 passed**; the scaffold pin, collision, referent-chain, leg-path and e2e files **245 passed**; `tests/test_update_shape*.py` **218 passed, 3 skipped** (junctions, NTFS only); `tests/test_shape_doctor.py` **254 passed, 3 skipped** (the PowerShell line, a Windows host only). - On this head itself: the new file, `test_repo_hygiene`, `test_cli_reference`, `test_pin_validator` and `test_neutral_pin_validator` **244 passed**. - This head merged with origin/main a983e6a, in a throwaway worktree: the new file, `test_pin_validator`, `test_neutral_pin_validator`, the scaffold pin, collision, referent-chain and e2e files, the whole doctor file and `test_cli_reference` **590 passed, 3 skipped** (the PowerShell line); the new file alone, in its final form, **137 passed**. origin/main had moved twice since; the merge-tree below is against it. - Twelve mutation probes, each hunk reverted alone: all caught. The note-to-finding one is caught by the update test (a committed mutant, since the test clones the repository's HEAD) and the guards by the sibling test, run with every test that needs a `#!` stub deselected as a Windows runner would; the hint one by an in-process test. - Nothing ran on macOS or Windows before this PR's CI. **Merge-tree, as computed during the fix round on 2026-10-12** (every sha and head is the one it had then; main and the other branches have moved since). `git merge-tree --write-tree` of this head (6494714) against origin/main 1f90a0f is clean, and so is the merge of origin/main a983e6a, which the merged-tree runs above used. #312, #309, #270, #268, #228, #245 and #351 have landed. The rest were computed on ad6f569 (the same tree but for one test file) against a983e6a. Open PRs touching this branch's files, all clean against the head: #314, #355 and #373 (`scaffold-project.py`), #343 (`validate-pins.py`), #390 (`repo_shape.py`) and #394 (`repo_shape.py`, which conflicts with origin/main by itself). The other 25 open PRs and the lane-5 branches `fix/adopt-check-link-and-manifest`, `fix/scaffold-free-text-values`, `fix/family-init-values`, `docs/adopt-execute-exits`, `fix/yaml-flow-brackets-surrogates` and `fix/adopt-pin-writes-pin-file` (#273's, 7a94b6b, also clean for this head) are clean too. #356, #382 and `fix/adopt-record-submodule-placement` conflict with origin/main and not with this head; #358, `fix/adopt-late-failure-notice` and `fix/adopt-plan-quote-yaml-keywords` conflict with both, with origin/main by themselves, so they are not this branch's. Neither are the five PRs below #200 that origin/main has not landed (#94, #96, #98, #100 and #161), which conflict with it too. The PR set is read from the refs under `refs/pull/` that origin/main's history has no `(#n)` subject for, not from GitHub, so a closed one may be in it. **One conflict, with `fix/link-source-values` (#338, 40762ca) in `scripts/repo_shape.py`, four hunks.** Not resolved in either branch; take both sides: (1) `resolve_link_source`'s signature, `default_source=None, env_shared: bool = False, *, tried: list | None = None`; (2) its docstring, both paragraphs; (3) `link_pins_from_trees`'s docstring, both paragraphs; (4) its call, `default_source, pin_product_key(name) in shared, tried=asked)` followed by #338's `if tried is not None: tried[name] = asked`. The auto-merged `resolve_link_source` body already composes: a shared variable is never an `env` candidate and so never appears in #338's `tried`. **Known limits after the re-verification (APPROVE WITH NITS, 0 blockers, 0 should-fixes, 3 nits, at 6494714).** For the four non-ASCII pairs this change now detects (`ß`/`ss`, `ſ`/`s`, `fi`/`fi`, `ς`/`σ`), the keyed `--pin-source <product>=<path>` form does NOT separate the two pins: #312's source lookup folds both keys into one, so one pin can get a false `unreadable` finding. A sibling checkout does separate them. Only a hand-written `project.yaml` can declare such a pair. The doctor's `pins` row still prints only "passes" for a validator that passes, so it does not carry the collision note; that needs `shape-doctor.py`, and is left for a follow-up. The wording for restoring the finding is under "Fix round 1" above: it is 34dbd86's `report.finding(...)` call, not a word swap. Reviewed adversarially at 34dbd86 (CHANGES NEEDED: 0 blockers, 2 should-fixes, 5 nits). The first should-fix was the finding itself, which locked a project that already declares the pair out of every `update-shape.py apply`; the second was a description that had gone stale. Fix round 1 (6494714) made the pair a NOTE, which is the coordinator's decision and one Brett may reverse (the way back is above), keyed collisions by the pin file's own fold, gave the guard a test that runs on Windows, and made a shared product's skip hint offer the keyed `--pin-source`. Its re-verification at 6494714 found every finding handled: APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits: the keyed form and the non-ASCII pairs, the doctor's silence about the note, and the wording for restoring the finding, each stated above). CI is green on Linux, macOS and Windows, with SonarCloud passing, at b70a07d (main a73b4e9 merged in). Its merge with main 5a25387 passed the targeted set (the six pin and scaffold test files, `tests/test_validate_pins_unreadable_workflow.py`, `tests/test_ruleset_hint_tracking_branch.py` and `tests/test_shape_doctor.py`): 850 passed, 3 skipped. Closes #275 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>
… 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) 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: | template | fields | |---|---| | `templates/assembly-root/project.yaml` | `id`, `elected_on`, `visibility`, `tracking_branch`, the spec leg's `path`, the code leg's `path` | | `templates/assembly-root/contracts/spec-pin.yaml`, `code-pin.yaml` | `submodule_path` | | `templates/family-root/family.yaml` | `id`, `org`, `created_on`, `visibility`, `tracking_branch` | `render` already spells a value between a YAML template's double quotes with `repo_shape.double_quoted` (#284). The single-pass `render` from #316, on main as 34ecbc2, does the same through `YAML_PLACEHOLDER_RE`. So each value reads back exactly. An ordinary value renders as it did before, plus the two quote characters. `family.yaml`'s `org:` is quoted along with the four fields #259 names. It is the same defect in the same file: `family.py init --org 2026` and `--org null` pass `checked_value`, and on c7c02fa the holder then fails `family-org` (#307 lists the `--org null` row). Taking `org:` back out is not a one-line revert. It fails 14 of the new tests (the 8 `family.yaml:org` in-memory cases, and all 6 `family.py init` rows, because every row of `FAMILIES` passes a misread `--org`), and it would also take the `ORG` row out of `TEXT_FIELDS` and put plain `--org` values into `FAMILIES`. The lane keeps `org:` quoted, as the same defect in the same file, unless Brett says otherwise. `scripts/family.py`: `_member_lines` writes each member's own `project.yaml` id into the holder's `members:` block. That id now reads back exactly, so a bare `007` in the row came back as 7, and `validate-family.py` would report `member-id-mismatch` for a member it had just pinned. `_member_id` quotes 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: `irrs` is still `id: irrs`. Two edges: - A legacy row whose bare id the reader misreads moves once. A holder written before this change, by `family.py add` of a member scaffolded with `--id 2026`, holds `id: 2026`. The next `bump`, `add` or `remove` of that holder rewrites it as `id: "2026"`. That is harmless: `validate-family.py` compares with `str()` and passes before and after. - Mixed versions. A `family.py` from before this change, run against a holder this branch wrote, turns `id: "007"` back into `id: 007`, and `validate-family.py` then fails `member-id-mismatch` (the member declares `007`, `family.yaml` records `7`). A current `family.py` that rewrites the holder next then writes that row as `id: "7"`. Nothing in this branch can prevent it: run `family.py` from a current checkout on a holder that has such a member. Left unquoted on purpose: `topic` (`xf-project-<id>`), every `repository` (`<org>/<Name>`; the slash keeps it text), `SHAPE_REPOSITORY` and `DIGEST_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 bare `007` as 7, `2026` and `20261011` as numbers, `1.5` as a float, `true` as True and `null` as 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|20261011` exits 0, and the project's own `validate-manifest.py` then fails `manifest-id`. - `--spec-path 2026 --code-path 007` fails `manifest-leg-path`, and `validate-pins.py` exits 2. - `--tracking-branch 007` pushes each leg on `007`, and the root's `bootstrap.py` then puts the legs on a local `7` with `fatal: no upstream configured`. `true` and `null` do the same on `True` and `main`. - `adopt-project.py execute` with `--tracking-branch 007` does the same; `--id 007|20261011` gives `manifest-id`; leg paths `2026`/`007` give `manifest-leg-path`. - `family.py init --id 2026|007|true|null|20261011` gives `family-id`; `--org 2026|null` gives `family-org`; `--tracking-branch 007|true|null` reads 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_yaml` into 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`, leg `path`. - `validate-manifest.py`: `id`, `elected_on`, `visibility`, leg `path`. - `validate-pins.py`: `submodule_path` against the manifest's `path`. The pin side is read as it stands and the manifest side goes through `str()`, so the two differ in type exactly when a bare value is misread (that is the `pin-path-mismatch` row 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`, family `bootstrap.py`. - `scripts/bump-leg.py`: `tracking`, leg `path`. Its `rewrite_pin` rewrites only `commit:` and `tree_sha256:`. - `update-shape.py`: `tracking_branch_of`. `rewrite_manifest` touches only the `shape:` block. - `scripts/family.py`: `tracking()`, the member's id, and the `members:` rewrite. The member-id writer is the one fixed here. - `shape-doctor.py`: leg `path`, member row `path`, working-clone `str(id)` compare. - `adopt-project.py`: plan reads and `_template_values`. - `setup-project.py`: `family.yaml` `name`/`kind` only. - `scripts/shape_advisory.py`: the single-repository record only. The `str()` fallbacks (`bootstrap.py`, `adopt-project.py`'s `checked_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.yaml` names them. `Atlas` was scaffolded and `family.py init Contoso` was run on c7c02fa and on this branch. Each `contracts/shape-pin.yaml` differs only in `commit:` and `digests.tree_sha256`, which identify the standard itself and change with every commit; every per-file `sha256` row is identical. `update-shape.py check` of the c7c02fa project and holder against this branch reports every copied file `unchanged` (11 and 10 rows): "no copied file differs; `apply` would move the pin alone". **Tests.** - New: `tests/test_template_text_placeholders.py`, 123 tests: - Each of the 13 fields is rendered with each of 8 values the reader misreads (`2026 007 1.5 true null 20261011 ~ False`), and must read back exactly. - 6 `scaffold-project.py` runs, then a recursive clone, `bootstrap.py`, the three validators, and `bump-leg.py`, which refuses on the tracking branch and finds the leg at its path on a branch of its own. - 6 `adopt-project.py` `plan`/`check`/`execute` runs into local bare remotes, then a clone, `bootstrap.py` and the validators. - 6 `family.py init` runs. - One `family.py add` of members whose ids are `007` and `true`. - **All 123 fail on c7c02fa.** - `true`/`null` through adopt: `plan` writes them bare into the plan, and the plan's reader then misreads them. That belongs to the plan writer, #181 (lane 1). Those two rows quote the value in the plan by hand, as #181's writer will, and test `check`/`execute`. - Mutation probes: reverting any one of the 13 quoted lines fails 8 cases of its field. Reverting `_member_id` fails the member test (the rows read 7/True). - The `try/except YamlError` in `_member_id` is guarded by the follow-up commit. The review found that calling the parse directly left the member test passing. Ids such as `[`, `{`, `&a`, `*a` and `!t` reach the `except` (they raise `YamlError` bare), and `_member_id` returns 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 the `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; 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: | files | result | |---|---| | new file + `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_default` | 405 passed | | the six adopt files | 491 passed, 1 skipped (the MedxEHR clone is not set) | | `test_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_sh` | 603 passed | | `test_shape_doctor` | 241 passed, 3 skipped (Windows-only), 1 failed; that one test was fixed, and with its sibling it reran at 2 passed | **The review.** An independent opus review at 233dc71 (the branch's first commit) found no code defect. Its verdict was CHANGES NEEDED (0 blockers, 2 should-fixes, 6 nits) because it asked for this description and its composition counts to be corrected, and they are. Its evidence: - **Readers and rewriters.** Every reader in the shipped tree reads the 13 lines through `load_yaml`/`parse_yaml`, and no line regex or whole-line rewrite touches them. It read `bump-leg.py` `rewrite_pin`, `update-shape.py` `rewrite_shape_pin` and `rewrite_manifest`, `family.py` `rewritten`, the three validators, `siblings.py`, both `bootstrap.py`, the doctor, `setup-project.py`, the workflows, the Makefile and `setup.sh`. No test among main's 17 commits since c7c02fa (through #245's 161c6c9) 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 fails `b" id: irqs" in lines`. So the minimal-churn choice is tested both ways. - **Old projects and holders.** Against a control commit (c7c02fa plus an unrelated README edit), a project and a holder made on c7c02fa give identical `update-shape.py check` and `shape-doctor.py` output, up to the shas. `check --at 233dc71` reports all 11 project files and all 10 holder files `unchanged` and says `no copied file differs`. The doctor's verdict is `COMPLIANT, SHAPE BEHIND (... no copied file differs)` for the control and for 233dc71. This branch's `family.py add` and `bump` on a c7c02fa holder leave the old `atlas` row and the header byte for byte, apart from the legacy-id edge described at the top. - **Tests.** All 123 new tests fail on c7c02fa (`123 failed`) and pass on 233dc71 (`123 passed`). The 14 changed existing tests pass (49 passed). - **Not covered.** Windows and macOS were reviewed by reading and not run: the plan edit and the member-row check are in bytes, and nothing writes to git's stdin in text mode. Of `test_shape_doctor.py` and `test_family.py` the 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_id` docstring said "no existing holder's `members:` block moves by a byte", which was false: the next `bump`, `add` or `remove` of a holder written before this change rewrites a legacy `id: 2026` row as `id: "2026"`, harmlessly, because `validate-family.py` compares with `str()`. 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. The `try/except YamlError` in `_member_id` is 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. The `except` also carries a comment saying which ids reach it. **Composition at the landing head (4984651).** `49846518` is GitHub's update-branch merge of origin/main `cc8f2da4` (#376, a `--trailer` value carried into the commit as typed) into `3220c9ee`. Git reported no conflict, and the merge has no hand edit. #376 changed `scripts/family.py` and `tests/test_family.py`, which this branch also changes. None of the lines it added under `tests/` names a key the templates now quote. The coordinator ran targeted tests on this head, Linux only, before CI: - main's changed test files (`tests/test_commit_trailers.py`, `tests/test_family.py`, `tests/test_bump_leg.py`, `tests/test_update_shape.py`) with `tests/test_family_add_member_id.py`: 421 passed, 1 skipped (a case-insensitive disk only); - this branch's template set (`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. **Composition at 3220c9e.** `3220c9ee` is GitHub's update-branch merge of origin/main `70d0d1b3` into `e5e768bc`. It brings in three commits: #491 (da19735), in which `setup-project.py` asks at the person's own terminal; #374 (7ab11cf), update-shape's NEXT quoting; and #384 (70d0d1b), adopt's `_spelled` plan values. Git reported no conflict, and the merge has no hand edit. #491 changed `tests/test_setup_project_py.py` and `tests/test_setup_sh.py`, which this branch also changes. A grep of the lines main added under `tests/` for the keys the templates now quote found two hits, and neither reads a rendered template line: `tests/test_update_shape_next_quoting.py` writes its own `project.yaml`, and `tests/test_adopt_question_and_submodule_spelling.py` edits a plan's `- path:`/`question:` entry, which `adopt-project.py` writes bare. The coordinator ran targeted tests on the same tree (`e5a74d19`), Linux only, before CI: - six of main's eight changed test files (the setup, update-shape, `openRepoShape` command and doctor-row tests): 365 passed; - main's other two (adopt's spelling and repo hygiene), `tests/test_adopt_github_exits.py` and nine more files of this branch's sixteen-file set: 943 passed. **Composition, refreshed again 2026-10-13 (head e5e768b).** `6d108d18` merges origin/main `56713e21` into `6cbbac60`. That brings in two commits: 5e02abe, the doctor's and `validate-pins.py`'s gitlink repairs, and 56713e2 (#479), in which adopt's `plan` takes the tracking branch from the source's default branch. Git reported no conflict and `docs/cli.md` auto-merged; the merge has no hand edit. Two kinds of test failed on it, and two commits fix them: - **`7968f902`** changes one expected literal, at `tests/test_adopt_github_exits.py:675`, for `test_a_source_on_another_branch_opens_its_pull_request_against_it[master]` and `[trunk]`. - The test reads the adopted root's `project.yaml`, which this branch's template renders as `tracking_branch: "master"`, so it now expects the quoted line. - The look-alike lines at `:743` and `tests/test_adopt_tracking_branch.py:73-76` read a plan, which `adopt-project.py` writes bare, and they stay as they are. - **`e5e768bc`** adds `git("branch", branch, cwd=source)` to `tests/test_template_text_placeholders.py::test_an_adoption_reads_back_every_value_it_was_given`. - All six rows had failed at `check` with `plan-tracking-branch-missing`: #479 refuses a tracking branch (`2026`, `007`, `1.5`, `20261011`, `true`, `null`) the source does not have, and the test's source had only `main`. - The source now carries each row's branch, in the flag rows and the by-hand rows alike. The test's assertions are unchanged. Targeted runs, Linux only: - On e5e768b: main's seven changed test files, 211 passed; this branch's sixteen-file set, 1223 passed, 2 skipped. - On 7968f90: the same sixteen files gave 6 failed, which are the six rows e5e768b fixes. The scaffold, setup, doctor and family set gave 1094 passed, 22 skipped, and twelve further template-grep files gave 1259 passed. - `render-cli-reference.py --check` is ok. **Composition, refreshed 2026-10-13 (head 6cbbac6).** `d651b594` merges origin/main `aa4e1f59` (#508) into `d526f768`. - **The one conflict.** Git conflicted in one file, `tests/test_adopt_manifest_values.py`, in four hunks, all against #356 (965420b, #293/#294). #356 refuses an `elected_on:` that `election_date` does not read, as `election-date-malformed`, before the validator is asked (`_manifest_values` returns None, adopt-project.py:2258). - **Resolved by what the merged tree prints**, measured with `adopt-project.py plan`/`check`/`execute` into local bare remotes: - `id: "007"` checks clean and `execute` writes it as typed, so it stays in `TEXT_THE_TEMPLATE_NOW_HOLDS`. - `visibility: "true"` gets the validator's `manifest-visibility: visibility is 'true'`. - `elected_on: "20261011"` gets #294's one finding, "the plan's `elected_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. - Two tests git merged cleanly now ask `said_of`, as main's own rows do: `test_check_refuses_a_text_value_the_validator_refuses` and `test_where_a_misread_rule_judges_the_rest_the_validator_still_answers`. - **Two more failures that no hunk showed**, fixed in the same merge: - `[branch-tilde]` in `tests/test_scaffold_manifest_values.py` was this branch's own `xfail(strict=True)` until #355 landed as 6c2a371, so it XPASSed. The mark is off, and the row asserts `REFUSED scaffold-bad-tracking-branch:`. - #391's `tests/test_bootstrap_spelling.py` and #490's `tests/test_bootstrap_after_bump.py` matched ` path: spec` and `tracking_branch: main` in a scaffolded `project.yaml`, and nine cases failed on the merge. They now match the quoted lines. - **The #507 interplay.** `6cbbac60` switches the holder to `family/members` before the real `add`s in `test_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id`, so the test holds once PR #507's `family-on-tracking-branch` refusal lands. - On the clean merge of 6cbbac6 with #507's head 708f3bb, that file, all 13 `tests/test_family*.py` and `tests/test_shape_doctor.py::test_a_sibling_at_the_right_origin_with_another_id_is_not_the_member` give 686 passed, 19 skipped. - Without the switch line, the member test fails `REFUSED family-on-tracking-branch`. - **Targeted runs on 6cbbac6, Linux only:** - the template, adopt-plan and manifest files (14): 995 passed, 2 skipped; - scaffold e2e, setup, the doctor, leg-path collisions and every family file: 1094 passed, 22 skipped; - the 15 further files a grep for template lines found: 1503 passed; - `render-cli-reference.py --check`: ok. - **Re-verified independently** at d651b59 and 6cbbac6: APPROVE WITH NITS (0/0/3), all three in this paragraph and fixed here. The re-verification also ran the three rows, five mutations, and 26 more files (834 passed; 291 passed, 1 skipped). - **Not run locally:** the full suite, macOS and Windows; CI's three jobs are the check. **Composition, as measured on 2026-10-12.** The merge trees below were computed for 233dc71; the follow-up commit 3894aa8 changes only `scripts/family.py` comments and adds tests. At 15:28Z main was 3ae0109 (#245, #351, #350 and #345 had landed since the merge trees): 3894aa8 merges clean with it, and the review's re-verify ran the family tests on that merge (61 passed). Every sha below is the one it had then; main and the other branches have moved since. `git merge-tree --write-tree --name-only`, run after a fetch with automatic maintenance off on 2026-10-12: - **origin/main 161c6c9**, 17 commits past c7c02fa (the base): clean, tree 7ba8246. #316 (34ecbc2), #317, #318, #311 and #245 were on main by then. - **The 46 other remote branch heads** (the 30 open PR heads and 16 branches with no open PR): clean for 44. Among them #314 (4b8da3d), #266 (3410be7), #355 (a03abcf), lane 1's `fix/adopt-plan-quote-yaml-keywords` (d38e143), `fix/adopt-record-submodule-placement` (70f4d29) and `fix/adopt-late-failure-notice` (9f1778d), #380 (73068a3), and `fix/family-init-values` (14e2d68, #307). The two that conflict, each in the same file as it does with c7c02fa and with origin/main on its own: lane 1's `fix/adopt-root-kept-symlinks` (6c44591, `adopt-project.py`) and #358 `fix/doctor-manifest-kinds-unreadable-pin` (ebffb08, `shape-doctor.py`). This branch touches neither file. - #314 and d38e143 each conflicted with origin/main on their own, in `adopt-project.py`. Merged with this branch, neither gained a conflict. **Composition with #314, which lands before this branch.** Its misread gate (`manifest_misreads`) reads the template, so once the template quotes a field it refuses nothing there, and some of #314's tests expected that refusal. Composing with #314 (landed as 3ef059c), whose misread gate asks the template how a value is written. With `project.yaml` quoting `id`, `elected_on`, `visibility` and `tracking_branch`, the gate refuses nothing on the shipped templates, and 45 of #314's tests flipped on the merge. Forty expected a refusal the value no longer needs. Thirty-four of them now assert that the value is accepted, and the six whose premise was a bare placeholder now assert that no shipped template placeholder is misread. For example: 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, and the manifest holds the text it was given. A dry run prints the plan. An id derived from `--project True|Null` is held as typed. `--id=null` reaches `gh repo create`. Adopt's `id: "007"` checks clean and `execute` writes it as typed. The rule's own sentence, remediation and line-break wording are still asserted, on a copy of the standard whose `project.yaml` writes the four lines bare. Four stay refused by #314's validator gate, now in its words about the text: `visibility is 'true'` and `elected_on is '20261011'`. One is a real break. `--tracking-branch=~` reads back as `~`, which git refuses as a branch name, and it dies at `git init -b ~`, after `gh repo create`. That is #283, so the row is `xfail(strict=True)` with that reason, and it turns XPASS (and fails) once PR #355's `git check-ref-format --branch` refusal lands. `--help` for `--id` and `--tracking-branch` no longer says such values are refused, and `docs/cli.md` is regenerated. **Composition with the other branches.** 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. - **#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 are `test_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 with `REFUSED scaffold-bad-leg-path`, 6 adopt rows with `FINDING plan-bad-leg-path`). Counted on the merge with b2b42f8. #266's head was then 3410be7, whose rule reads each template line that holds a leg path, renders the value into it as `render` does and asks the reader, so it has no hard-coded line left. The merge with this branch was clean (tree 1e348c6); that head was read and the 27 were not re-run on it. At b2b42f8, whichever of #266 and #259 landed second had to drop or re-point the bare rule; 3410be7 re-points it already. - **Lane 1's #181** (`fix/adopt-plan-quote-yaml-keywords`, d38e143). Its gate reads the template too, so `plan --tracking-branch 2026|007|true` exits 0 on the composed tree (exit 2 `adopt-manifest-misread` on d38e143). On the merge, `tests/test_adopt_plan_yaml_quoting.py` and this branch's 6 adopt end-to-end tests give 70 failed, 371 passed: - 57 refusals that are now unnecessary: `plan_refuses` ×14, `plan_names_an_id` ×2, `check_names_an_id` ×4, `check_reports` ×14 and `execute_refuses` ×14 (every value except `elected_on` and `visibility`), `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`). - 11 guards #181 loses. See "Do not flip". - 2 that fail only by cascade: `execute_refuses[pins='null']` and `[pins='True']` pass alone (`2 passed`). An earlier case no longer refuses, and its `execute` pushes the split branch. - The writer half of #181 (`y` asking the reader, so `true`/`null`/`True` are quoted in the plan) is still needed and composes cleanly. With it, `plan --tracking-branch true` reaches the quoted template intact, and this branch's two hand-quoted adopt rows become flag-driven. - **Do not flip these 12 to "accepted".** Each still needs a refusal or a finding, and quoting does not supply it: - **`~` (1, #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 #314 refused it only as a misread. On the merge it passes the plan and dies at `git init -q --bare -b ~ .../Atlas-spec.git` with `fatal: invalid initial branch name: '~'`, and against GitHub that is after `gh repo create` has made all three repositories. That is lane 2's #283 (PR #355). `~` must stay refused, by #283's ref-name check, which is why #314's row for it is not flipped to "accepted". - **A hand-edited plan value that is not text (7, #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 merge `check` passes. Measured on the merge: `tracking_branch: 007` and `id: 007` are adopted silently as `"7"` with every validator passing; `visibility: {a: b}` pushes both legs and then fails `manifest-visibility`; `tracking_branch: [a]` is still refused at `execute` (`unsafe-value`) before any leg exists. On d38e143 alone, `check` exits 1 `plan-manifest-misread` for each of those four. Re-point the gate at "a plan value that is not a string" and these keep their guard. - **A quoted value the validator refuses (4, #181).** `test_check_reports_a_plan_carrying_one[elected_on='20260902']` and `[visibility='true']`, and `test_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 #181's misread gate caught it only incidentally. On the composed tree `check` passes and `execute` pushes the legs, and the gate fails afterwards. That is what main does today, and #314's validator gate (`plan-manifest-invalid`) would catch it. - **main** (merge with ab623dd, tree cdeec7f, run by the review): this branch's new file, `test_adopt_template_values.py` (#270), `test_template_values_single_pass.py` (#316) and `test_template_quoted_values.py` give 431 passed, 1 skipped. The skip is `test_adopt_template_values.py:223`, which reads `git config user.name` and skips where the shared repository's `.git/config` sets one; it is environmental. The changed tests with `test_family_add_refusals.py` (#317, #318), `test_pin_validator.py` and `test_manifest_validator.py` give 132 passed. #270's `unwritable_reason` is value-based, not template-relative, so it refuses as it did and nothing flips. Main then added #245 (`adopt-project.py` and `repo_shape.remote_key`), which adds no test that edits a bare quoted line; it was not run on that merge. - **#380** (lane 3, `fix/pin-rewrite-indentation`; it landed since as 1f90a0f). It rewrites `commit:` and `tree_sha256:` only, and its tests render the pins from the templates. On the merge at dc0fa7b, `test_pin_rewrite_layout.py` and the 6 scaffold end-to-end tests gave 121 passed. Its head was then 73068a3; that merge was clean and was not re-run. - **#397** (`fix/family-init-values`, #307; measured at 14e2d68): its tests skip themselves where the template quotes. 137 passed, 19 skipped, 0 failed. With its fix-round head abb27e6, this branch's 3894aa8, main 0f67f7b and #345 together: 442 passed, 20 skipped (the 19 rows, and one Windows-only row). - **#345** (run at 875d802; it has since landed as 3ae0109): `test_family_dry_run.py` and the member test, 22 passed. Its `" id: irrs"` assertion holds, because ordinary ids stay bare. **Left out.** - The adoption plan writer `y` still writes `true`/`null` bare into the plan. That is #181 (lane 1). - `templates/assembly-root/contracts/neutral-product-pin.yaml` still writes `product`, `source_repository` and `digest_source` bare, and `family.py` writes a member row's `project:` bare. Neither is a #259 field. - A plan value someone edits to a non-text type (`tracking_branch: 007` or `id: 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. For `id` this is a change: on c7c02fa a bare `id: 007` failed loudly (`validate-manifest: manifest-id`, manifest line `id: 7`), and on this branch it is adopted silently as id `"7"` and every validator passes. `tracking_branch: 007` was already adopted silently as `7`. The plan side is not this branch's: the guard against it belongs with #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, because `plan --id 007` itself writes `id: "007"`; only a hand edit reaches it. - No docs change: README, AGENTS.md, the handbook and the template comments show none of these lines; `docs/cli.md` is unchanged because no `--help` text changed. Reviewed adversarially at 233dc71 (CHANGES NEEDED: 0 blockers, 2 should-fixes, 6 nits; the code and the tests were sound, and the should-fixes were the composition counts and the claim about a hand-edited plan `id`). Fix round 3894aa8 changes no behaviour: it corrects the description and the two false "moves by a byte" claims, and guards `_member_id`'s `except YamlError` with a test of 7 cases. Its re-verification at 3894aa8 found every finding handled: APPROVE WITH NITS (0 blockers, 0 should-fixes, 1 nit: shas that had moved since the description was written, refreshed here). What was added after that re-verification, main and the composition with #314, is described under "Composition with #314". An independent re-verification of that composition at 77cd0b2 confirmed it with nits only (0 blockers, 0 should-fixes, 5 nits): the 45 flips reproduce test for test, breaking the real rule or the real template's quoting turns the compose tests red, and the `[branch-tilde]` row XPASSes on a merge with #355. **Composition with #381.** #395's quoted `id: "{{PROJECT_ID}}"` composes with #381's `member-duplicate-id` test (`tests/test_family_add_member_id.py`), whose id rewrite now matches the id bare or quoted and keeps its quoting, so the full suite, which failed that one test on Linux at 1ef3310, no longer fails `test_a_member_whose_id_changes_between_the_probe_and_the_mount_is_unwound` on Linux. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 4984651 (main cc8f2da merged in by update-branch). Main af01068 (#505), which landed after, touches none of its files, and the combined tree passed the template, hygiene, CLI-reference, AGENTS.md-codes and setup tests (807 passed). Closes #259 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#435 was stacked on #356, which squash-landed on main as 965420b. Its final head 2fdfbed is a server-side merge of main into #356's 5982bbe, so it brings #356's last fix (the `org:` refusal and `id:` read ahead of the election-date guard), #445's REFUSED contract and #401's help text. This is step 1 of bringing a stacked branch onto main after its parent's squash: `git merge-tree 2fdfbed 3efcc7a` was rc 0, and this merge is clean. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…this branch's side of #356's three files (#386) #356 landed on main as 965420b. `git diff 2fdfbed 965420b` over its three files, `adopt-project.py`, `tests/test_adopt_execute_refusals.py` and `tests/test_adopt_manifest_values.py`, is empty, so main holds those files exactly as #356's final head 2fdfbed left them, and this branch already merged 2fdfbed. The conflicts in `adopt-project.py` and the refusal tests are #356's own hunks meeting themselves, and `git checkout --ours` for the three files is #356's final version plus #435's change and nothing else. Proof: `git diff 965420b` of the result over the three files has the same patch-id (76b3015) as #435's own change, `git diff dbfad70 3efcc7a`, and no other file differs from 965420b. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… list of mappings, so #386's gate asks it only whether it is a list, in #273's own finding (#386) Main since #356's squash 965420b changed `adopt-project.py` three times: 70d0d1b (#384, `_spelled` in three findings), 56713e2 (#493, the tracking branch from the source's default branch) and aa4e1f5 (#508, lane 5's #273 and #339). aa4e1f5 makes `plan --pin` take `product@<40 hex>` and write each pin as a MAPPING of `product:`, `repository:`, `commit:`, `tree_sha256:` and `digest_source:`, and its `_read_pins` judges each `pins:` entry beside the entries: `plan-pin-unrecorded` for a name alone, `plan-pin-malformed` for anything else `plan` never writes there, and for a `pins:` that is no list. Two hunks conflicted, both in `adopt-project.py`, both the ones aa4e1f5's message names: `render_plan`'s `pins:` line takes main's `_pin_lines`, which writes the product through `y` and the commit and digest quoted, so this branch's `y(pin)` is superseded; and `Plan.pins` takes main's `_pin_products`, which reads a list only. The rest is the reconciliation aa4e1f5 and lane 5's comment on #435 asked of whichever landed second. As this branch stood, its item check took only text, so every plan `plan --pin` now writes was `plan-bad-pins` in its own `check`: - `_plan_key_findings` no longer judges `pins:` entries: they are `_read_pins`' alone, in its words and codes. - A `pins:` that is no list is still asked first, beside the other four keys and before the work directory, because the naming policy reads the root's products from it. Its finding is `_read_pins`' own `plan-pin-malformed`, so it is one finding under one code, and `plan-bad-pins` is gone. - `PINS_REMEDIATION` names the mapping `plan` writes. - `PLAN_KEY_REFUSAL` begins "no leg repository was created and nothing was pushed", the words every refusal before a leg says (#445; conftest's `refused_before_any_leg` asserts them, and #273's not-a-list row now meets this refusal). `tests/test_adopt_execute_refusals.py` says the same: the `pins:` key rows expect `plan-pin-malformed` and gain a single mapping with no list around it; the entry rows move to a test that the gate is silent and `_read_pins` names them; the controls and the corrected end-to-end plan are whole pins; and the round trip runs `plan --pin <name>@<commit> --pin-source` for 2026, 007, 1.5 and openGlass, and holds `check` to `plan ok`. `git diff --stat cc8f2da` of the result lists only `adopt-project.py` and `tests/test_adopt_execute_refusals.py`. Before this commit: `tests/test_adopt_execute_refusals.py` 150 passed and `tests/test_adopt_pins.py` 70 passed. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k directory is made, a plan whose `source:` or `legs:` is not a mapping or whose `allow_empty_legs:` or `paths:` is not a list of what `plan` writes, and asks the same first of a `pins:` that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback and `allow_empty_legs: code` is no longer read as four legs (#386) (#435) `Plan` read four keys of a plan file without asking their type. `plan` writes `source:` and `legs:` as mappings and `allow_empty_legs:` and `paths:` as lists, and the file is edited between `plan` and `execute` on purpose. Each was iterated or read with `.get` behind an `x or []`, which rescues only falsy values. A number or a boolean ended `check` and `execute` in a `TypeError` or an `AttributeError`, exit 1, naming Python's error and not the key. Text was read one character at a time: `allow_empty_legs: code` consented for the legs `c`, `o`, `d` and `e`, and `paths: abc` was reported as every source path uncovered. Each key is now read through a reader that takes only what `plan` writes. `_plan_list` is the one read of `paths:` and `allow_empty_legs:`: the value when it is a list, `[]` otherwise. `allowed_empty_legs` keeps only text items, `Plan.source` is `source:` when it is a mapping and `{}` otherwise, and `names` reads `legs:` only when it is a mapping. `Plan` keeps `legs:` as it stands, because `shape-doctor.py --placement-plan` writes it as a list. What the readers leave out is named. `_plan_key_findings` gives a finding for each key that is present and is not what `plan` writes, under a code per key (`plan-bad-source`, `plan-bad-legs`, `plan-bad-allow-empty-legs`, `plan-bad-paths`), and one for each item of `allow_empty_legs:` that is not `spec` or `code`, with each key's remediation said once. A key that is absent or bare is read as it always was. `pins:` is #273's: `plan` writes it as a list of mappings, and `_read_pins` judges each entry beside the entries. Only a `pins:` that is not a list is asked here, in `_read_pins`' own `plan-pin-malformed` finding, because the naming policy reads the root's products from it. These are asked first, right after `Plan.load`, because every later finding reads what they say. `check` prints them beside #356's `elected_on:` and `follow_ups:` findings and exits 1. `execute` refuses the same list through `_refuse_a_mistyped_plan`, exit 2, under the first finding's code, before `_work_root` makes the work directory or the source is opened, and says no leg repository was created. `tests/test_adopt_execute_refusals.py` runs `check` and `execute` over each wrong type and bad item, holds the readers to the same rule, and shows with `plan --pin` that the gate takes the pins `plan` writes. Closes #386 Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-2 (openRepoShape-2)
Claim: #293 (comment)
Claim: #294 (comment)
The date guard no longer reorders refusals
5982bbe, on top of the merge below. The guard this PR added to
_manifest_values(a2ec12e, "Merged main after #314 landed") returned before the function'schecked_value("org", ...)and before_template_tablereadplan.project_id.checkmeets a bad organisation there and nowhere else, and a bad id there first (it reads the id again later, for the topic line it prints). So a badelected_on:beside either one changed which problem each command named first (rv356m's P3):elected_on: someday,org: "-evil"checkandexecute:REFUSED unsafe-valuecheck: exit 1, only the date FINDING;execute:REFUSED unsafe-valuecheckandexecute:REFUSED unsafe-valueelected_on: someday,id: "North Wind"checkandexecute:REFUSED plan-bad-idcheck:REFUSED plan-bad-id;execute:REFUSED election-date-malformedcheckandexecute:REFUSED plan-bad-idelected_on: somedayaloneFINDING plan-manifest-invalidFINDING election-date-malformedFINDING election-date-malformedNo leg is created in any of them. The fix refuses the organisation and reads the id before the guard, so the guard sits after them, and not "right after its
_check_namesrefusal" as the #314 section below says. The guard still keeps a date-only plan to one finding in the plan key's words, with noplan-manifest-invalidbeside it. The one cost left is the one "Composes with" names: the validator's other findings (for examplevisibility: secret) wait until the date is corrected. The docstrings of_manifest_valuesand_manifest_findingsnow say they return nothing for such a date (rv356m's nit 2).test_a_bad_date_does_not_change_what_check_and_execute_refuse_firstpins both plans, withreference:kept and removed:checkexits 2 on the codeexecuterefuses with, and no leg is created. On 88c430c's code all 4 fail; at 5982bbe all 4 pass. The coordinator's repro driver, run on 5982bbe, matches main 580d7df for both plans.tests/test_adopt_execute_refusals.pynow has 91 tests, 54 of them this PR's.flake87.4.0 reports the same findings by message at 5982bbe as at main 580d7df.Merged main after #445 landed
88c430c merges main 580d7df. Since dd8d0ea main gained #267, #403, #446, #445, #406 and #416. Only #445 (75052cb) touches
adopt-project.py, and none of them touches this PR's two test files. The merge conflicted in one hunk, in_commit_the_split.The hunk: both kept. adopt-project.py: every failure once a leg repository exists flushes stdout, then prints
REFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445 wraps_commit_the_split's body in atrythat names each step (cloning the source for the split,mounting the legs,committing the split,pushing the split branch). A failure once both legs are pushed then flushes stdout and printsREFUSED <step>., what failed and one notice of what the run left. This PR readsfollow_ups:there through_plan_follow_ups. The merge keeps adopt-project.py: every failure once a leg repository exists flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445'stry, steps and reports whole, and putsfollow_ups = _plan_follow_ups(plan)in itscommitting the splitstep. Git had kept main's raw[str(f) for f in plan.get("follow_ups", [])]inside thetry: a second read of the key, beside the one this PR makes.The two contracts do not overlap. This PR's findings are refused through
_refuse_what_check_finds. That runs before adopt-project.py: every failure once a leg repository exists flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445's_refuse_an_occupied_work_dirand_refuse_a_split_branch_left_behind, and before_create_leg_remotes. Soplan-bad-follow-upsandelection-date-malformedare refusals before any leg exists:REFUSED <code>:;REFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445 gives its own refusals before the legs;adopt-project.py: every failure once a leg repository exists flushes stdout, then prints
REFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445's report covers failures once a leg exists, and these values no longer reach that point. The merge leaves the guard in_manifest_valuesas it was; adopt-project.py: every failure once a leg repository exists flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445 does not touch it.One comment. adopt-project.py: every failure once a leg repository exists flushes stdout, then prints
REFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445's comment over_template_valuesincmd_executenamed "anelected_onthat is not a date" as a refusal the table can raise._refuse_what_check_findsnow refuses such a date first (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), so the comment says the date no longer reaches the table. adopt-project.py: every failure once a leg repository exists flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445's own test of a table refusal stubs_template_values, and is unchanged.Two assertions. In
tests/test_adopt_execute_refusals.py, the two end-to-end runs throughmain(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) now also callassert_refused_before_any_leg. It checks that the refusal says no leg was created, and that adopt-project.py: every failure once a leg repository exists flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445's notice (NOTHING has been rolled back) does not follow it. Each run's corrected plan then adopts from the same--work-dir, which adopt-project.py: every failure once a leg repository exists flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left, so a failedgit filter-repono longer exits 2 with an empty stderr (#171, #188, #177) #445'sadopt-work-dir-occupiedlets through, because the refused run built nothing there.At 88c430c, against main 580d7df, this PR changed
adopt-project.py,tests/test_adopt_execute_refusals.pyandtests/test_adopt_manifest_values.py(3 files, 624 insertions, 28 deletions).git merge-tree --write-tree 580d7df 88c430cis rc 0.flake87.4.0 reports the same findings by message at 580d7df and at 88c430c: 34 inadopt-project.py, 2 intests/test_adopt_execute_refusals.py, 6 intests/test_adopt_manifest_values.py.Targeted runs, with
git filter-repo2.47.0 and Python 3.12.3, as two background runs at each head. Together they cover all 25tests/test_adopt_*.pyfiles and the six other files below:test_adopt_execute_refusals.py,test_adopt_manifest_values.py,test_scaffold_manifest_values.py,test_reference_default.py, #445's newtest_adopt_late_failure.pyandtest_adopt_filter_repo_failure.py,test_shape_doctor.py,test_scaffold_leg_path_collisions.pytests/test_adopt_*.py,test_cli_reference.py,test_repo_hygiene.pyscripts/render-cli-reference.py --check[ok] docs/cli.md is what the generator produces[ok], the sameAt 88c430c that is 2397 tests collected: 2393 passed, 4 skipped, 0 failed. At 5982bbe it is 2401: 2397 passed, 4 skipped, 0 failed. The 4 skips are the same at both heads:
test_shape_doctor.py:6587three times (the PowerShell line, printed only on a Windows host) andtest_adopt_plan.py:393(noSHAPE_MEDXEHR_CLONEclone of MedxEHR).Against main 580d7df this PR now changes
adopt-project.py,tests/test_adopt_execute_refusals.pyandtests/test_adopt_manifest_values.py(3 files, 680 insertions, 32 deletions at 5982bbe). Main has since moved to d6df36b, adding #404 (shape-doctor.py,scripts/shape_advisory.py) and #346 (update-shape.py). Neither touches this PR's files.git merge-tree --write-tree d6df36b 5982bbeis rc 0, and that tree differs from d6df36b by the same three files and numbers.git merge-tree --write-tree 5982bbe <head>, on the local refs with no fetch, is rc 0 with:plan --helpsays what--spec-pathand--code-pathare, the default and key each records, and the two refusalsplanmakes on them, sodocs/cli.mdno longer lists the two flags that choose where a leg is mounted as bare lines (#340) #401 da27e69, 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 32e4861, 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;read_pinrefusesupdate-pin-row, and the pinned validate-pins.py and validate-family.py reportshape-pin-row, when a shape pin'sfiles:is not a list, sofiles: 5,trueor1.5no longer endscheck,apply, shape-doctor.py and the project's own validator inTypeError: ... is not iterable(#290) #343 e68a01d, validate-pins.py (assembly-root template): a workflow file under.github/workflows/that is not UTF-8, or that this user cannot open, is ONEworkflow-unreadablerefusal naming every such file, exit 2, so the project's pins gate no longer ends in a UnicodeDecodeError traceback, exit 1, with no line naming the file (#359) #419 11dff7f, update-shape.py: a pinned copy,contracts/shape-pin.yamlor the manifest kept as a symlink, or under one, is refused asupdate-rewrite-symlinkbefore anything is written, andcheckcalls such a copycopy-is-link, soapplyno longer writes the standard's bytes at the link's end and reports a commit that does not hold them (#286) #347 9423361;checknames, andexecuterefuses before the work directory is made, a plan whosesource:orlegs:is not a mapping or whoseallow_empty_legs:orpaths:is not a list of whatplanwrites, and asks the same first of apins:that is not a list, so a number or a boolean in any of the four no longer ends both in a traceback andallow_empty_legs: codeis no longer read as four legs (#386) #435 3efcc7a, which is stacked on this PR.It is rc 1 with #358 ce2ce75, in
shape-doctor.pyonly. This PR does not touch that file, and main conflicts with #358 there too.Merged main after #314 landed
#314 landed on main as 3ef059c, and a2ec12e merges it, as "Composes with" below planned. At 3ef059c,
adopt-project.pyand #314's four test files are exactly a5438a5's merged with 92e29d9, so the plan read from a5438a5 held unchanged._entry_findings: both kept. The return isfindings + split + unwritable_findings + value_findings + moved + field_findings, then the remediations in the same order, thennotes. The docstring lists adopt-project.py and scaffold-project.py accept values the new project's own validate-manifest.py refuses (a blank--elected-byor--reference,--elected-on 2026-W41-6or20261011on Python 3.11+,--org test+org, a plan'svisibility: secret), soadoption verifiedor a scaffold exit 0 is followed by a failed first bootstrap; andplanadmits a 251-255-character--projectthatcheckrefuses #261's validator findings, then adopt-project.py keeps a source submodule's relativeurl = ../xunchanged in the leg, where git resolves it against the leg's remote, soexecutesaysadoption verifiedand the assembly's recursive clone fails unless the dependency happens to sit beside the leg #190's relative urls, and last this PR's plan values. Main's own paragraph below it said the adopt-project.py keeps a source submodule's relativeurl = ../xunchanged in the leg, where git resolves it against the leg's remote, soexecutesaysadoption verifiedand the assembly's recursive clone fails unless the dependency happens to sit beside the leg #190 urls come "last"; it now says the plan values come after them._manifest_valuesreturns None when_elected_on_findings(plan)has a finding, right after its_check_namesrefusal, with the comment given below. Inplanit does nothing, because_checked_plan_inputsaskselection_dateof--elected-onbefore_refuse_unholdable_valuesbuilds its plan.--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314's 5 rows intests/test_adopt_manifest_values.pyare retargeted toelection-date-malformed, and none is dropped. This PR's own tests ask2026-W41-6and20261011only with noreference:(in itscheckrows, and inexecutethrough a stand-in). These rows keepreference:and ask throughexecute, from a tree with no git, and of_manifest_findingsalone. A helper,said_of(new), gives each row's finding,REFUSEDprefix and remediation; every other row expects what it did.elected-on-iso-weekintest_check_finds_a_value_the_validator_refuses,test_execute_refuses_it_before_any_leg_existsandtest_check_finds_it_from_a_tree_with_no_git_as_in_a_checkout;elected-on-basic-formatintest_execute_refuses_a_text_value_the_reader_misreads_before_any_legandtest_a_text_value_the_reader_misreads_is_one_finding_on_this_rule_alone. In the second,_manifest_findingsis now([], [])and_plan_field_findingsholds the one finding.Against main 3ef059c, this PR now changes
adopt-project.py,tests/test_adopt_execute_refusals.pyandtests/test_adopt_manifest_values.py(3 files, 601 insertions, 24 deletions).flake87.4.0 reports the same findings by message inadopt-project.py(37) andtests/test_adopt_manifest_values.py(6) at 3ef059c and at a2ec12e.Targeted runs at a2ec12e, with
git filter-repo2.47.0 and Python 3.12.3: 2304 tests collected, 2300 passed, 4 skipped, 0 failed.test_adopt_execute_refusals.py87,test_adopt_manifest_values.py79,test_scaffold_manifest_values.py106,test_reference_default.py18tests/test_adopt_*.py(1532 tests)test_shape_doctor.py268,test_scaffold_leg_path_collisions.py104test_cli_reference.py13,test_repo_hygiene.py50,test_agents_md_codes.py47 (new on main, #396)scripts/render-cli-reference.py --check[ok] docs/cli.md is what the generator producesWithout the guard, these fail:
--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314 that "Composes with" names;executeforelected-on-iso-weekandelected-on-basic-format(REFUSED plan-manifest-invalid: 2 finding(s)), and the rule-alone row, where_manifest_findingsstill holds the validator's finding.With no
reference:,checkexits 2 onREFUSED election-date-malformed: --elected-on '' is not a YYYY-MM-DD date. Withreference:kept, the date is two findings.On main 3ef059c, all 37 tests of main's own file pass, and 42 of the 50 new ones fail: v3's 40, plus the same-question rows
2026-W41-6and20261011. #314'selection_datenow refuses both, butcheckdoes not list them as findings; it stops at exit 2. #314 already refuses a bad date before any leg, but not as this PR's finding:reference:,checkexits 2 onREFUSED election-date-malformed: --elected-on 'someday' ..., naming a flag the plan does not hold, andelected_on: falseis refused as--elected-on 'False';reference:kept, the date is the validator'splan-manifest-invalid.follow_ups: 5is still aTypeErrortraceback there.Fix round (review of 5a5011d)
Review of 5a5011d said MERGEABLE WITH NITS. Three commits answer it, and this description is N3:
git merge origin/main(a983e6a: 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 landed as 161c6c9, then adopt-project.py, AGENTS.md:executeverifies before it exits on a refusedgh pr create, and step 7 keys its way out to the exit it prints, so anIS pushedexit 2 is finished by hand, not re-run into a second pair of legs (#304) #371) conflicted inadopt-project.pyonly, 2 hunks in_entry_findings:... and last each plan valueexecutereads late and_plan_field_findingsasks about (#293, #294));field_findingslast:findings + split + unwritable_findings + moved + field_findings, the remediations in the same order, andnotes.cmd_checkand_refuse_what_check_findsalready unpack three values on main, and this PR's tests call_refuse_what_check_finds(plan, source)with its defaults, so no test changed. Tested at 0aff5ae before anything else was added: the four files that cover the merge (test_adopt_execute_refusals.py,test_adopt_relative_submodule_url.py,test_adopt_submodules.py,test_adopt_gitmodules_values.py) 433 passed.elected_on: ""was unpinned (f8e6de1, tests only)._elected_on_findingsskips onlyelected_on is None, so""reacheselection_dateand is refused, but no test said so: changingis Nonetonot elected_onpassed all 81 tests, andcheckthen saidplan okwhileexecutecreated both legs before it refused. The commit adds anempty-textrow toBAD_DATES, which feeds both thechecktest and theexecutetest, and achecktest forfalse,0,[]and{}, which are not text, soBAD_DATEScannot hold them. At 0aff5ae they pass (6 passed: no code changed). Withif not elected_on:in place ofis None, all 6 fail._plan_field_findingssaid the findings come "in the order the plan file holds the two keys". The order is fixed, date then follow-ups, and it is the orderplanwrites the keys. It now says so, and so does the both-findings test's docstring.executereads". That does not hold on Python 3.11 and later until 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 lands (20261011and2026-W41-6passelection_date, andexecutewrites them). It now says "the formexecutewrites into the project'sproject.yaml". Thechecktest pins the new words and that the old ones are gone: against the reviewedadopt-project.pyits six rows fail, and they pass at this head..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 has landed (its row is gone), 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 is at a5438a5, adopt-project.py:_electorkeeps auser.namebyte that is not UTF-8 for #255's question,_remote_repositoryreads such an origin URL as noorg/repo, andplanasksadopt-no-orgbeforechecked_value, so a Latin-1 name is refused asadopt-unwritable-valuenaming--elected-by, andplan,checkandexecutego on past such an origin, where both ended in a UnicodeDecodeError traceback (#292) #353 is at 96ff0c7 and shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 at 8badae0. The composed return order with 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 is read from a5438a5's own code (below).Not in this PR: the review's two follow-ups (a branch name the plan holds that git refuses, and the other plan keys that end
checkin a traceback).Merged main after #270 landed
#270 (
fix/adopt-template-values-utf8) landed on main as the squash f795cf7, and this PR, stacked on it, is now based on main. Three merges brought it up to date. The first two changed no code and added no test:executewrites 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's final head 3cee853 (it had merged main e43fa95). No conflict. It comes first because this branch carried an older copy of 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's commits, so a plain merge of main conflicts inadopt-project.py.adopt-project.pywas the only file that conflicted, and it takes this branch's version of the whole file. That is right because the squash's content ofadopt-project.pyandtests/test_adopt_template_values.pyis 3cee853's (git diff 3cee853 f795cf7over them is empty), which ead7716 had already brought in, and no other main commit since 3cee853 touches them orscripts/shape_materialize.py..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 (161c6c9) and adopt-project.py, AGENTS.md:executeverifies before it exits on a refusedgh pr create, and step 7 keys its way out to the exit it prints, so anIS pushedexit 2 is finished by hand, not re-run into a second pair of legs (#304) #371. It does change code:adopt-project.pywas the only file that conflicted, in 2 hunks of_entry_findings, resolved as the Fix round section says.After the first two (at 5a5011d):
git diff HEAD^1 HEADoveradopt-project.pyandtests/test_adopt_template_values.pywas empty, andgit diff --stat origin/main HEADlisted onlyadopt-project.pyandtests/test_adopt_execute_refusals.py.At the head:
git diff --stat a983e6a dbfad70lists onlyadopt-project.pyandtests/test_adopt_execute_refusals.py(2 files, 553 insertions, 12 deletions), andgit merge-tree --write-tree dbfad70 92e29d9(main today) is rc 0.git diff --stat 92e29d9against that merge's tree lists the same two files with the same numbers.What
checknow asks about two plan values thatexecuteused to read only aftercheckhad saidplan ok.executerefuses whatcheckfinds through_refuse_what_check_finds, before_create_leg_remotes, so no leg is created. There is one commit per issue.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,follow_ups:(2533515)._plan_follow_upsis now the one read for the three readers:_plan_values,_print_what_will_happenand_commit_the_split. It returns the text items of afollow_ups:list, and nothing for any other value._follow_ups_findingsreportsplan-bad-follow-upsfor afollow_ups:that is present and is not a list, and names what it holds:the number 5,the boolean true,the text 'abc',a mapping of 1 key(s). It reports one finding for each list item that is not text:item 2 of `follow_ups:` is the boolean true, not text.planwrites the list.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,elected_on:(84f3806)._elected_on_findingsaskselection_datewhether or not the plan has areference:. It asks aboutstrof what the plan holds, which is the value_template_valuestakes.election_daterefuses is a FINDINGelection-date-malformed:the plan's `elected_on:` is the text 'someday', which is not a day written YYYY-MM-DD, the form `execute` writes into the project's `project.yaml`, and, the plan naming no `reference:`, chooses the reference by it.elected_on:that is absent or null is "no date", whichexecutefills with today's. An empty text,false,0,[]and{}are dateselection_datecannot read, and are findings like any other.elected_on:in the plan file. Nothingcheckorexecuteprints names--elected-onany more, because neither command takes it.election_dateraises is raised as it is, so a date it never judged is not reported as malformed. An example is 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'smanifest-validator-unusable, raised when the validator it consults is missing._entry_findings, through_plan_field_findings, with the date first, in the orderplanwrites the two keys (elected_on:beforefollow_ups:). The new code is one section aboveUNSAFE_PATH_REMEDIATION, placed away from_entry_findings, which 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 changed 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 also edits (see Composes with).checkbeforechecknowexecutebefore (main 92e29d9)executenowfollow_ups: 5/true/false/1.5TypeError: ... is not iterableFINDING plan-bad-follow-upsTypeError, raised from_plan_valuesinside_refuse_what_check_finds, so before any leg (until #270 landed it was raised in_commit_the_split, after both legs were pushed)REFUSED plan-bad-follow-ups, no legfollow_ups: abc/{a: 1}, an item- true/- null/- 5follow-up: a,b,c- a,- b,- c/- True/- Nonefollow_ups: [], a bare key, quoted itemsreference:,elected_on: someday/notadate/"2026-9-2"/"2026-02-30"/""plan okFINDING election-date-malformedREFUSED election-date-malformed: --elected-on 'someday' ..., both leg repositories created, emptyreference:kept,elected_on: somedayadoption verified,elected_on: somedayin the manifestelected_on: "2026-09-02"/"2026-06-01"/null, with or withoutreference:reference:the manifest records the reference the date choosesWhy
The plan is untrusted input (
cmd_execute's own comment).checkis the command that says whether a plan is sound. It printed a traceback for one of these values andplan okfor the other, andexecuteused them after it had made the legs.The date question is
election_date's, unchanged. It is the ruleplanasks of--elected-onand_template_valuesasks of the plan's date. On Python 3.11 and later it reads2026-W41-6and20261011, and the project's ownvalidate-manifest.pyrefuses both. That is #261's to rule on, and lane 5's #314 holdselection_dateto the validator'sDATE_RE.So this PR asks the same function and copies no rule:
plan,checkandexecutecannot disagree about one value. A strictercheckalone would refuse a plan thatplanhad just written from--elected-on 2026-W41-6.--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314 lands, its rule is asked here with no edit.Only the words are this PR's.
election_date's refusal names--elected-on, which is right forplanand wrong for a plan file.tests/test_reference_default.pystill holdselection_date's own wording.Tests
tests/test_adopt_execute_refusals.pyhas 50 new tests: 22 for #293 and 28 for #294. It goes from 37 to 87 tests. They sit besidetest_execute_refuses_each_class_and_creates_no_legand use its helpers (write_edited,run_check,no_leg_was_created,refused_by).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:checkfor nine edits: one FINDING, exit 1, no traceback, and nofollow-up:line except for text items.executein-process for the same nine: the refusal, no leg, no split branch.mainwithgit filter-repo: exit 2, no leg directory, thenadoption verifiedfrom the corrected plan.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:checkandexecutefor six dates: five with noreference:(someday,notadate,"2026-9-2","2026-02-30"and the empty text"") and one with it kept. The finding nameselected_on:and the formexecutewrites, and no output names--elected-on.checkforelected_on: false,0,[]and{}: only an absent or nullelected_on:is "no date".check's verdict equalselection_date's on the running interpreter, for2026-W41-6,20261011,2026-09-02andsomeday.election_date, stood in by monkeypatch, refuses2026-W41-6before any leg.election_dateis raised as it is, and is not a date finding.reference:adopts and records the reference its date chooses.Against main's code (92e29d9, this PR's test file copied in): the file gives 40 failed and 47 passed. The 37 existing tests pass, and 40 of the 50 new ones fail, each for the issue's reason:
TypeError: 'int' object is not iterable;plan ok, where a FINDING belongs;DID NOT RAISE Refusal, becauseexecutecarried on and wrote- a/True/someday;elected_on: "";--elected-on;election_dateis not raised.The 10 that pass there are the 7 controls and the 3 same-question rows, where
election_dateanswers "a date" on that interpreter.Targeted runs at the head dbfad70, with
git filter-repo2.47.0 on PATH and Python 3.12.3:test_adopt_execute_refusals.py,test_adopt_relative_submodule_url.py,test_adopt_submodules.py,test_adopt_gitmodules_values.pytests/test_adopt_*.pyfilestest_shape_doctor.py(the doctor feeds adopt plans) andtest_scaffold_leg_path_collisions.pytest_reference_default.py,test_stdout_encoding.py,test_load_yaml_refusals.py,test_git_output_decoding.py,test_free_plan_hint.py,test_windows_paths.py,test_cli_reference.py,test_repo_hygiene.pyscripts/render-cli-reference.py --check[ok] docs/cli.md is what the generator produces(no--helptext changed)test_adopt_execute_refusals.pyat the #293 commit alonetest_adopt_execute_refusals.pyon main's code, no mergeThat is 2216 tests collected across the 31 files and 2211 passed, 5 skipped, 0 failed. Per file (collected at dbfad70):
test_adopt_execute_refusals.py87 (37 before);test_adopt_clone_template.py31,test_adopt_e2e.py19,test_adopt_entry_paths.py64,test_adopt_finding_spelling.py128,test_adopt_github_exits.py11,test_adopt_gitmodules_values.py187,test_adopt_history_gitlinks.py45,test_adopt_ignored_shape_files.py30,test_adopt_leg_path_collisions.py95,test_adopt_leg_paths.py349,test_adopt_literal_names.py14,test_adopt_mount_ignored.py26,test_adopt_plan.py57,test_adopt_plan_escapes.py59,test_adopt_relative_submodule_url.py32,test_adopt_root_shape_paths.py120,test_adopt_spec_only.py14,test_adopt_submodules.py133,test_adopt_template_values.py78,test_adopt_verify.py16;test_reference_default.py18,test_stdout_encoding.py48,test_load_yaml_refusals.py11,test_git_output_decoding.py67,test_free_plan_hint.py14,test_windows_paths.py38;test_shape_doctor.py258,test_scaffold_leg_path_collisions.py104,test_cli_reference.py13,test_repo_hygiene.py50.Nothing ran on Windows or macOS before this PR's CI. The new tests write plans through the file's own helpers, pass no text to
giton stdin, and use no symlink, so nothing here is specific to an OS.flake8(7.4.0, default settings) reports nothing new in either file:adopt-project.pygives 37 findings andtests/test_adopt_execute_refusals.pygives 2, at main 92e29d9 and at dbfad70 alike, the same ones by message (line numbers aside).Mutants, each applied to
adopt-project.pyin a throwaway tree at dbfad70 and run against all 87 tests of the file:_follow_ups_findingsremovedif not elected_on:)_plan_follow_upskeeps items that are not text (str(item))reference:election_datetaken as a date findingfollow_ups:accepted_commit_the_splitreadsfollow_upsraw, as main did_refuse_what_check_findsrefuses every badfollow_ups:first, so the split commit never reads oneThe implementer's own seven mutants were run at 84f3806, each in a throwaway worktree, and were not re-run. They change
_follow_ups_findingsand_plan_follow_ups(unchanged since),_elected_on_findings(changed since only in the wording of its finding, N2) and the join in_entry_findings(re-spelled by the merge 0aff5ae):-k)_follow_ups_findingsreturns[]for a value that is not a list_plan_follow_upsiterates whatever is therecheckfollow-ups testsfollow-up: a)reference:elected_onrefusal testsword-reference-kept)date.fromisoformat) instead ofelection_dateelection_date's own words passed oncheckdate tests--elected-onnamed)election_datetaken as a date finding_entry_findingsexecuterefusal testsComposes with
git merge-tree --write-tree dbfad70 <head>for the heads at the shas named, with main 92e29d9 (which holds #245, #270 and #371):adopt-project.pyandtests/test_adopt_execute_refusals.pyonly.shape-doctor.py, 4 hunksgit merge-tree --write-tree 92e29d9 2745890). This PR does not touch that file.adopt-project.py, 2 hunks:_entry_findings' docstring and its returngit merge-tree --write-tree 92e29d9 a5438a5is rc 0, so #314 merges main cleanly and the conflict is only between #314 and this PR.With #314: the two hunks. At a5438a5
_entry_findingsreturnsfindings + split + unwritable_findings + value_findings + moved, ...with the remediations in the same order andnotes. With this PR it becomes:and the docstring lists the checks in that order: what the project's own
validate-manifest.pywould refuse (#261), then each relative url of a kept submodule (#190), and last each plan valueexecutereads late (#293, #294).With #314: the guard, and why it is needed. Keeping both sides is not enough. I tested this on throwaway merges, never committed:
reference:, 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_manifest_valuesbuilds the table through_template_tableanddefault_reference. That raiseselection_date's own refusal, socheckexits 2 withREFUSED election-date-malformed: --elected-on 'someday' ...before this PR's finding is listed. That is 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 NIT-2, and 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 wording again.reference:kept, the date is reported twice: 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'splan-manifest-invalid: elected_on is 'someday' ...and this PR'selection-date-malformed.The fix is one guard in
_manifest_values, after its_check_namesrefusal, which also returns None. The anchor is unchanged at a5438a5:1770-1773:With it, every row is one finding in this PR's words, exit 1, and
executerefuses before any leg. That coverssomeday,notadate,"2026-9-2","2026-02-30","","2026-W41-6"and"20261011", andreference:kept.2026-W41-6and20261011are refused because #314'selection_daterefuses them. The controls still adopt.The cost: a plan whose date is unreadable gets the validator's other findings (for example
visibility: secret) only once the date is corrected. That is how #314 already treats a plan whose names_check_namesrefuses.Run at dbfad70 on throwaway merges with a5438a5, resolved as above, on
tests/test_adopt_execute_refusals.py,tests/test_adopt_manifest_values.py,tests/test_scaffold_manifest_values.pyandtests/test_reference_default.py(290 tests):--id null,--tracking-branch trueand a blank--elected-byare refused (#258, #261) #314's own rows for anelected_on:that is now judged once, by this PR's rule, ahead of the validator's question:elected-on-iso-weekintest_check_finds_a_value_the_validator_refuses,test_execute_refuses_it_before_any_leg_existsandtest_check_finds_it_from_a_tree_with_no_git_as_in_a_checkout;elected-on-basic-formatintest_execute_refuses_a_text_value_the_reader_misreads_before_any_legandtest_a_text_value_the_reader_misreads_is_one_finding_on_this_rule_alone.checkandexecutedate rows, the fourcheckrows forfalse,0,[]and{}, the three same-question rows, the tighter-rule test, the both-findings test and the date end-to-end run. The 23rd is 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'selected-on-basic-formatone-finding row.plan-manifest-invalidorREFUSED plan-manifest-, and get one findingelection-date-malformedthat names the same value. 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 own--elected-onrows already expect that code. The second lander retargets those rows toelection-date-malformed, or drops them as covered by this PR's tests. That keeps one finding per value, which 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'smisreads_elsewherealready does for misreads.Whoever lands second adds the guard. If #314 lands first, this PR merges main, resolves as above and adds the guard. If this PR lands first, #314 does. #314 landed first, and a2ec12e adds the guard.
Left out
election_date's own form,2026-W41-6and20261011on Python 3.11+: adopt-project.py and scaffold-project.py accept values the new project's own validate-manifest.py refuses (a blank--elected-byor--reference,--elected-on 2026-W41-6or20261011on Python 3.11+,--org test+org, a plan'svisibility: secret), soadoption verifiedor a scaffold exit 0 is followed by a failed first bootstrap; andplanadmits a 251-255-character--projectthatcheckrefuses #261, PR 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, now landed as 3ef059c. This PR asks through the same function, sochecknow finds both, with no edit here.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 names, is not touched here and should be its own issue. Checked at 5a5011d, and the lines are unchanged at this head:pins: 5endscheckinTypeError: 'int' object is not iterablefromPlan.__init__(self.pins = [str(p) for p in (data.get("pins") or []) if p]).allow_empty_legs: 5ends the same way fromPlan.allowed_empty_legs.paths:is read the same way inPlan.__init__(read from the code, not run).default_referencecall in_template_valuesis unchanged. It cannot be reached with a dateelection_daterefuses, because_refuse_what_check_findsrefuses that plan first, asking the same function about the same value.Closes #293
Closes #294
🤖 Generated with Claude Code
Summary by Sourcery
Validate late-read plan fields before adoption begins so malformed follow-ups and election dates produce clear findings and safe refusals without creating legs.
Bug Fixes:
follow_ups:andelected_on:plan values duringcheckand refuse them duringexecutebefore any leg repositories are created.Enhancements:
elected_on:rather than the unrelated command-line flag.Tests: