Repository navigation
adopt-project.py: execute records in project.yaml which leg the plan gave each submodule a leg keeps, and shape-doctor.py holds the submodule to that row, so one in the other leg is MISPLACED and a real adoption's placement reads ok (#189) - #394
Merged
Merged
Conversation
…eg and a submodule it registers to another, or keeps either in the root, is now a `check` finding and an `execute` refusal, and `plan` asks the one-leg question on `.gitmodules` itself (#166) A plan answering `.gitmodules -> code, upstream/ -> spec` (or `.gitmodules -> drop, upstream/ -> code`) used to pass `check`, run `execute` to "adoption verified", and leave an assembly whose `git clone --recurse-submodules` exits 128 on `No url found for submodule path 'spec/upstream/dependency'`. Reproduced on main before this change; refused before either leg exists after it. ONE helper, `submodule_plan_problems(entries, tree)`, read by both `cmd_check` (as findings) and `_refuse_an_unrunnable_plan` (as a refusal), so the plan `check` passes is the plan `execute` runs. The leg of a path is the `leg:` of the single entry `_covering` it: - `.gitmodules` and every gitlink (mode 160000): `root` is refused, one `plan-submodule-root` per path. The assembly root has exactly two mounts, `execute` writes it a fresh `.gitmodules` for them, and `validate.yml` reads every URL there as a leg. - When the source has a `.gitmodules`, every gitlink kept in spec or code must share its leg, one `plan-submodule-split` per gitlink; so `.gitmodules` cannot be dropped while one is kept. A gitlink dropped beside a `.gitmodules` that went to a leg is allowed: its registration stays behind inert (verified: recursive clone exit 0, `bootstrap ok`). All dropped together is allowed. - No `.gitmodules`: only the root rule, so an orphan gitlink adopted into a leg still adopts. - An unresolved, invalid, uncovered or doubly covered entry is skipped: it is already its own finding. A source with neither (the common case) returns nothing at once; its plan file and `check` output are byte-identical to main's. `contracts/path-classification.yaml` gains `ambiguous-gitmodules`, asked first with the other named ambiguities, whose question states the constraint: "A source's own `.gitmodules` goes to the SAME leg as every submodule it registers; `execute` writes the assembly root a fresh one for its two mounts", and that `root` is not an answer. Only the top-level file matches. With `root` refused, the root-kept submodule path #165 documents in `_verify` is no longer reachable from `execute`; `_verify` and `_mount_the_legs` are untouched and #165 stays open. Tests: the plan question; the `check` matrix of eleven answers, a gitlink a rule classified silently (`tools/dep`), an orphan gitlink; the refusal through importlib, all without git-filter-repo; and one end-to-end refusal under `needs_filter_repo` asserting no leg repository was made. The two policy tests take `.gitmodules` (its rule id) and a nested `docs/theme/.gitmodules` (spec). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tered in the source's `.gitmodules` with a url, because the assembly's recursive clone dies on one that is not even where the source's own clone skips it, and a `.gitmodules` that `git config` cannot read is refused instead of being read as registering nothing (#166) aaa9099 allowed a gitlink with no registration into a leg on the premise that it "was already unclonable in the source and is no worse in a leg". That premise is false. At the top level, `git clone --recurse-submodules` skips a gitlink that has no URL; once it is inside a leg, the clone recurses with `--init` and dies on it. The adversarial review's reproduction (`upstream/orphan`, no `.gitmodules`, `upstream/ -> code`) gave `check` 0, `execute` 0 "adoption verified", source clone rc 0, and assembly clone rc 128 `No url found for submodule path 'code/upstream/orphan'`. The same happens beside a `.gitmodules` that registers other paths. The sentence is withdrawn from the docstring; this message withdraws it from aaa9099's ("No `.gitmodules`: only the root rule, so an orphan gitlink adopted into a leg still adopts"). The rule, still in ONE helper, so `check` and `execute` cannot disagree. The helper is `submodule_plan_problems(entries, tree, registered)`, and it is now a pure function of the registrations its callers read: - `Source.registered_submodules(tree)` returns the paths that have a `submodule.<name>.path` and a non-empty `submodule.<name>.url`. It reads them with `git config -z --blob <commit>:.gitmodules --list`. It uses `--list` and not `--get-regexp`, because the second exits 1 both for "no key matched" and for "bad config line" (probed on git 2.43); `--list` exits 0 or 128. A key with no `<name>` is ignored, as git ignores it. With no `.gitmodules`, nothing is registered. - A gitlink kept in spec or code that is not registered is `plan-submodule-unregistered`, naming the gitlink and its leg. It is that and never also a split: no leg holds a URL for it. Dropped, it is fine. - A `.gitmodules` that `git config` cannot read is `plan-gitmodules-unreadable` when the plan keeps it, or any gitlink, in a leg. It is never read as registering nothing. - Unchanged: `root` refused for `.gitmodules` and every gitlink; a registered gitlink kept away from `.gitmodules` is `plan-submodule-split`; a dropped gitlink beside a leg's `.gitmodules` is inert. - Problems are listed root, unreadable, unregistered, split: the order to fix them in. `execute`'s refusal takes the first problem's code and names eight details, then "and N more". - The remediation says how to fix an unregistered gitlink: drop it, or register it in the source (an entry with path and url, committed) and re-run `plan`. Text that was untrue where it is printed: - The `ambiguous-gitmodules` question keeps the issue's sentence verbatim and "`root` is not an answer". Its "`check` finds it and `execute` refuses it" becomes "`adopt-project.py` refuses a plan that gives it". `shape-doctor.py --placement-plan` prints this question for already-split projects, whose plan no `check` or `execute` reads. The question also gains: "A submodule it does not register with a path and a url cannot be kept in a leg at all: drop it, or register it first." - Its `reason:` no longer claims that the repository registers submodules, since it is printed for an empty `.gitmodules` too. - The `plan-submodule-root` detail and the remediation said "`execute` writes it a fresh .gitmodules", which is false when `.gitmodules` is itself `root`. They now say that the root's `.gitmodules` "is reserved for its two leg mounts". - The doctor's verdict on the reviewer's adopted assembly is unchanged: `note`, "nothing is in the wrong leg", exit 0. Only the `reason:` and `question:` lines of its placement plan differ. Sonar hygiene: no literal in adopt-project.py or the test file newly reaches three uses. - `_git_path` is the one decoder for `Source.tree()` and the registrations, which must be compared in one spelling. - `GIT_CONFIG` replaces the subcommand literal in `_elector` and in `_mount_the_legs`' url write. That is a one-token change there; `_verify` is untouched. - `SUBMODULE_KEY_RE` parses the keys. - `_submodule_findings` keeps `cmd_check` at main's shape. Tests, tests/test_adopt_submodules.py (28 -> 52 cases): - The orphan test now expects `plan-submodule-unregistered` for code and spec, a pass for drop, and `plan-submodule-root` for root. - New: an unregistered gitlink beside a registered one (code, spec, drop). - New: a registration that is url-less, empty-url, name-less, at another path, or in an empty file is not one. Upper-case keys are one. - New: an unreadable `.gitmodules` (five answers). - The importlib refusal test now compares the refusal with `check`'s findings, detail for detail, across all four codes and four legal plans. - New: a mixed refusal of eleven or ten problems pins the first code and the 8-detail cap. - The `needs_filter_repo` end-to-end refusal is parametrised over the split plan and the reviewer's orphan plan. It asserts that the `--local-remote-dir` does not exist, that the `--work-dir` is empty, and that the source's HEAD, refs and status are unchanged. - Repeated literals go into constants and helpers (`adopt`, `check_plan`, `plan_of`, `add_submodule`, `commit_as_source_human`). The first test in the file uses them too. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry that holds one of the source's submodules, whatever rule matched it, so it no longer proposes the `root` that `check` refuses, and `check` now prints the submodule remediation after its findings (#166) The second review of aaa9099, by lane openRepoShape-2, found that `plan` proposes what `check` refuses. `walk` sees paths, not modes. So a submodule vendored at `.specify/vendored`, `.cursor` or `.agents` came out `leg: root` with no question (rules `root-spec-kit` and `root-assistant-instructions`). One at `docs/theme` came out `spec` (`spec-governance`), also unasked. `check` then answered `plan-submodule-root`, or a split once `.gitmodules` was answered. Reproduced at 09d7889: `.specify/` written `root`, then `plan-submodule-root: .specify/vendored (entry .specify/)`. - `ask_about_submodules` runs in `cmd_plan` right after `walk`. Every entry that `_covering` says covers a gitlink takes the verdict `.gitmodules` itself gets: `leg: null`, `review_required: true`, rule `ambiguous-gitmodules` and its reason. It also takes that question, plus "This entry holds the submodule(s) <paths>: it is asked this question, not classified by its rule `<rule>`." When the entry holds other paths too, the question adds that it can be split. `walk`'s folding is unchanged; only the covering entry is replaced. - `check` on the untouched plan now reports `plan-unresolved` there, never `plan-submodule-root`. - The 2026-09-02 ruling is unchanged for files. The `ambiguous-gitmodules` header comment says that a vendored SUBMODULE under a root-ruled path is asked rather than rooted, because the root keeps no submodule of the source's. - A source with no gitlink gets its entries back untouched. On the suite's SYNTHETIC_TREE source, these are byte-identical to main's: the plan file, `plan`'s output, and `check`'s output both unresolved and resolved. Not fixed here, and now said so: `execute` refuses every SUBMODULE problem `check` finds, but not yet every finding. An entry that is uncovered, covered twice or has a misspelled leg still reaches the split; that is #168, which merges right after this. The docstrings that said "the plan `check` passes is the plan `execute` runs" now say exactly this, in `submodule_plan_problems`, `_answered_legs` and `_refuse_an_unrunnable_plan`. The same review's smaller findings: - The question and the remediation offered no way to be rid of one submodule except `git rm` after adopting. The rule has always allowed dropping one on its own, and both texts now say so: its registration stays behind in the leg, inert. - `check` prints SUBMODULE_REMEDIATION once after its findings when any of them is a submodule problem, as `execute`'s refusal does. `_submodule_findings` returns it, so `cmd_check` gains no branch. - The module docstring's list of what `check` validates now includes the submodule rule. docs/cli.md is generated from the first line only and is unchanged. - #162's kept-root mount test and the `_mount_the_legs` comment are re-labelled as mount defence in depth for a plan that `check` and `execute` refuse (#166). In `_mount_the_legs` only the comment changes. Tests in tests/test_adopt_submodules.py, 52 -> 77 cases: - `plan` asks on the entry holding a gitlink at `.specify/vendored`, `.cursor`, `.agents`, `docs/theme` and `upstream/dependency`, and on one entry holding two gitlinks. Each row is the `.gitmodules` row apart from its path and size. Its question names the gitlinks and the rule, and `check` on the untouched plan reports only `plan-unresolved`. - The `tools/dep` test now answers `tools/` as its rule would have, since `plan` asks there. - Branches no test reached before: a `.gitmodules` with no gitlink beside it; two submodules split across legs; a gitlink that is its own entry. Also uncovered, doubly covered and bad-leg entries: `check` reports each by its own finding, and the helper skips it. - The `check` matrix asserts the remediation is printed once, after the findings, exactly when there is a submodule finding. - One `needs_filter_repo` end-to-end test runs `execute` on accepted plans: both code, both spec, and the submodule dropped on its own. `git clone --recurse-submodules` of the assembly and `bootstrap.py` in the clone then both exit 0. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… submodule reader reads it in the leg, so a 0xFF byte or an `[include]` no longer changes the answer and a registration git ignores is no registration, says what git said when it cannot read one, and an entry holding a submodule among other paths names the entries that replace it (#166) Round 3, from lane openRepoShape-2's re-verification of 09d7889 (N1-N3, P3.3). N1. `Source.registered_submodules` read the blob with `git config --blob`, which git's submodule code never does: it reads bytes through a `char` (signed on x86), so a 0xFF byte ends the file there, and it follows `[include]`. A Latin-1 comment before a registration was a false refusal; one before a `path` override hid it, and the adoption verified and then failed to clone (#166's symptom); a relative include was a false `plan-gitmodules-unreadable`; an absolute one read a file on the operator's machine. The blob is now piped into `git config -z --no-includes --file - --list`. `--no-includes` is not optional: probed on git 2.43, `--file -` FOLLOWS includes by default (stdin sets no file, so git turns `respect_includes` on), unlike `--file <path>`. The `plan-gitmodules-unreadable` detail now says what git said, on one line ("git cannot read the source's .gitmodules for its submodules (fatal: bad config line 2 in standard input), ..."), including when the `.gitmodules` in the tree is no blob at all (`git cat-file` fails). N2. `_registrations` mirrors git's `parse_config` for `path` and `url`: a name that is empty or has a `..` component at `/` or `\` is ignored (`check_submodule_name`); a value starting with `-` is ignored and the one before it stands; a path belongs to the last name given it, and a name moved to another path removes its old path's entry whoever holds it (git's `cache_remove_path`); a `path` or `url` with no value, which `--list` prints but git's reader dies on, makes the file unreadable, except under a name git already ignores. Every one probed against a recursive `git submodule update --init` on git 2.43. "Matched exactly as git matches them" is gone from the docstring; it now says which of git's rules are mirrored. N3. Adding `src/vendored: drop` beside `src/` is `plan-covered-twice`. An asked entry holding more than one path now names the entries that replace it, each submodule an entry of its own, descending only into a directory that holds one ("replace it with an entry for each of `src/app/`, `src/vendored`, each with its own `leg:`"), and SUBMODULE_REMEDIATION says to REPLACE a directory entry, not add beside it. An entry holding only submodules, two or more, is split too, so one can be dropped on its own. P3.3 (partial). The `ambiguous-gitmodules` question keeps #166's sentence, framed "When a repository is adopted, ..." so it is true where `shape-doctor.py --placement-plan` prints it for a leg already split. Docstrings and comments that said "until #168" are corrected now that #168 is merged here: `_refuse_what_check_finds` refuses the findings `submodule_plan_problems` leaves to `check`'s own. Tests: the registration test gains the 0xFF, name, `-`, last-name, moved-name and no-value shapes; new tests for `[include]` relative and absolute, for git's words in the unreadable finding, and for the split the question names (beside: covered twice; the named split: plan ok); the accepted-plan e2e gains upper-case keys, a 0xFF comment and an include. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `root by rule`, so an assembly adopted the way #166 requires, with a vendored submodule at `.cursor` or `.specify/vendored` in its code leg, is no longer MISPLACED (#176) The placement row classified every tracked path of a leg with `contracts/path-classification.yaml`'s globs, and a glob cannot tell a gitlink from a file. A submodule at `.cursor` read as the assistant instructions and one at `.specify/vendored` as the Spec-Kit tooling, both of which the 2026-09-02 ruling roots. But the assembly root keeps no submodule except its legs, so an adoption keeps such a submodule in a LEG (#166). The doctor answered `code/.cursor: root by rule root-assistant-instructions; code/.specify/: root by rule root-spec-kit` and exited 1 MISPLACED over a placement `adopt-project.py` had required. This was reproduced on b18cca9 with a scaffolded assembly whose code leg held both gitlinks, and with the r4 review's adopted CURSOR, I3split and I3whole assemblies. How it is fixed: - `leg_tracked_files` runs `git ls-files --stage -z` in place of `ls-files -z`. It is still one git call per leg. The path list is identical, checked on unmerged stages, intent-to-add entries and a tab in a name, and the call now also gives each path's mode. Modes come from the index, as the paths do, so a staged gitlink is seen as one. - `audit_leg` keeps every gitlink (mode 160000) out of `walk()`. A gitlink no longer votes in the fold of the files beside it and is not counted as a file under it. It is returned on its own and named in a `note`: "N submodule(s) the path policy does not judge, kept in a leg because the assembly root keeps no submodule but its legs: ...". - Files are judged exactly as before. A constitution beside `.specify/vendored` in the code leg is still MISPLACED by root-spec-kit (the I3whole case). The submodule clause is added to that row's sentence. - This holds for every gitlink, whatever rule its name matches, because #166's `plan` asks on every entry that holds one. `docs/theme` (spec by spec-governance) in the code leg is a note, not "spec in the code leg". `vendor/lib`, which no rule claims, was a `default` review note before and is the submodule note now. The row is `note` and the verdict is COMPLIANT, exit 0, both before and after. - A kept submodule alone keeps the row at `note` rather than `ok`, because a gitlink is a tracked path the policy did not classify. Its `next` is None: a note with nothing to resolve has no command. The `--placement-plan` decision: a kept submodule carries a NOTE, not the `ambiguous-gitmodules` question. The question is asked once, when the repository is adopted, and the leg the submodule is in is the answer somebody gave. Asking it again of a project already split would put an answered question back on every run. Today's contract would ask it in `default`'s words, which offer `root`, the one answer a submodule cannot take. `ambiguous-gitmodules` exists only on the unmerged #166 branch, and its text is written for adoption (the r4 review's P3.3). So a kept submodule is not an entry under `paths:`, which holds paths a human resolves. It is listed in a new `submodules:` block with `path`, `in_leg` and `note`, placed before `ignored:`. No submodule, no change: `detail.submodules` and `counts.submodules` appear only when a leg keeps one, and the plan block likewise. I checked the old and new doctor, run from one standard, on six projects with no gitlink (clean, misplaced plus a question, a question only, an unread leg, an unwritable name, 50 files folded). Text, `--json` and the plan were byte-identical on all six. On the two gitlink projects only the placement row changed, plus the status-column width in the text report. Not changed: a leg's own `.gitmodules` is still classified as a file, as a `default` review note on this contract (`ambiguous-gitmodules` once #166 lands). That is the r4 P3.3 residue and is outside #176. Tests (tests/test_shape_doctor.py, 164 -> 171): a code leg holding a submodule at `.cursor` and at `.specify/vendored` (parametrized), a root-ruled file beside a kept submodule, a submodule at a path no rule claims, one the policy sends to the other leg, a no-submodule project whose row keys, counts and reason are exactly the old ones, and the plan's `submodules:` block read by the adoption's own reader. Run against main's doctor, 6 of the 7 fail; the unchanged-output test passes on both, as it must. tests/test_shape_doctor.py 171 passed; tests/test_cli_reference.py and tests/test_repo_hygiene.py 63 passed; render-cli-reference.py --check ok. `--help` is unchanged. This touches no file the #166 branch changes (adopt-project.py, contracts/path-classification.yaml), so the two are independent. Closes #176 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Lane: openreposhape-2 (openRepoShape-2)
…x holds for it is a gitlink, and a submodule at an `everywhere` path such as `.github/actions` is named all the same (#176) (Codex review on #186) Codex review on #186 found two paths by which the gitlink rule dropped something it should have judged or reported. Both are P2 and both are valid. 1. A mixed-stage path was removed from the classification entirely (thread 4226514885). `git ls-files --stage` lists an unmerged path once per stage, so `.cursor` that is a file on one side (100644) and a submodule on the other (160000) gave two records. `leg_tracked_files` kept only the NAME of every path with a gitlink stage, so both records were taken out of `walk()`: the ordinary file never reached the policy and the doctor said "a submodule" and COMPLIANT where the 2026-09-02 ruling requires the root-placement finding. It now keeps each path's modes until the last record is read, and a path is a gitlink only when every stage it holds is one. A path with any regular stage is classified as the file it is, as it was before #176. A path whose every stage is a gitlink (two sides that moved a submodule to different commits) is still exempt, and is still named once and not once per stage. 2. A submodule at an `everywhere` path was never reported (thread 4226514890). `audit_leg` built the list of paths to judge by dropping the `everywhere` matches (`.github/**`, `LICENSE`, `COPYING`, ...) and then derived the submodules from what was left, so a submodule kept at `.github/actions` vanished from it. The row stayed `ok` and neither `--json` nor the placement plan named it, which contradicts #186's promise that every gitlink a leg keeps is reported as a note. The submodules are now collected from every tracked path BEFORE the `everywhere` patterns are applied; the files to classify are the ones that are neither a gitlink nor an `everywhere` match, which is the same list as before for every project without a gitlink. Nothing else moves. The path list `ls-files --stage -z` gives is unchanged, so `tracked` and `classified` are the numbers they were, a project with no gitlink gets the same row, `--json` and plan byte for byte, and an unmerged path is still listed once per stage as it is on main, which is why the first test does not pin how many entries the one file gets. Tests (tests/test_shape_doctor.py, 171 -> 176): - an unmerged `.cursor` with a file stage and a gitlink stage, in both orders (parametrized, built with `git update-index --index-info`): still a FINDING by root-assistant-instructions, and no submodule is named for it - an unmerged `.cursor` whose every stage is a gitlink: one submodule, named once, COMPLIANT (the guard on the other side of the rule) - a submodule at `.github/actions` and at `COPYING`, each first checked against the row's own `everywhere` list (parametrized): named in the text report, in `--json` and in the placement plan's `submodules:` block, and the row is a `note` Run against bff8122's doctor, 4 of the 5 fail; the all-gitlink guard passes on both, as it must. tests/test_shape_doctor.py, tests/test_cli_reference.py and tests/test_repo_hygiene.py: 239 passed; render-cli-reference.py --check ok. `--help` is unchanged, so docs/cli.md is too. The full suite was not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Lane: openreposhape-2 (openRepoShape-2)
…g is a placement the doctor cannot verify, named with the rule, in text, `--json` and the plan, and still a `note` (#176) (Codex review on #186, round 2) Codex review on #186, round 2 (thread 4228030879, P2) is right that the exemption is blind. A kept gitlink was never judged by its path, so `docs/theme` in the code leg, which `spec-governance` assigns to spec, got the same note as `.cursor` and the doctor said COMPLIANT. That is correct for a project adopted under #166, where a person answered the leg. But #166's `ask_about_submodules` is not on main: main's adopt discards tree modes (adopt-project.py:703-704) and never asks about a gitlink, and a submodule can also be added by hand. For those projects nothing in the assembly records that anyone chose the gitlink's leg, and the doctor said "COMPLIANT" with no evidence either way. The decision, taken by the coordinator: - A kept gitlink is still never `root by rule`, which is #176's defect and stays fixed. The row's status for it stays `note` and the verdict stays COMPLIANT, because MISPLACED would recreate #176's false positive for the case #166 requires: a human answered the leg and the doctor cannot tell. - But the note now says which of two things the doctor found, and names the rule: * `kept`: the name matches a root rule, no rule, or the rule of the leg it is in. "a submodule; the assembly root keeps no source submodule (#166), so a leg holds it", with the rule's id added where one matched. * `unverifiable`: the name matches a rule assigning the OTHER leg, as `docs/theme` under `spec-governance` in the code leg, or a code rule in the spec leg. "a submodule at a path rule `<id>` assigns to the <other> leg; a submodule's leg is the adoption plan's answer and the doctor cannot verify it here: confirm against the plan, or move it". How it is carried out: - `submodule_placement(policy, role, path)` asks the policy's own `rule_for` (first match; no extension majority and no `default`, which read what a FILE looks like) and returns `placement`, `rule` and `note`. A rule whose leg is `ambiguous` leaves the leg to a human, so it is `kept` with the rule named. - Each entry of `detail.submodules` in `--json` now carries `placement` (`kept` | `unverifiable`) and `rule` (the id, or null) beside `path`, `leg`, `path_in_leg` and `note`. - The row's sentence has one clause per class present, so both are said apart, each submodule once, with its rule. A submodule beside a misplaced path or a question still only adds its clause. - The plan's `submodules:` block carries `placement` and `rule` on every entry, and its comment says what `unverifiable` asks of a reader. It is still not an entry under `paths:`, and asks no question. - `counts.submodules`, `next` (None on its own) and every other row and verdict are as before. A project with no submodule in a leg gets the identical row, `--json` and plan: the no-submodule identity test is unchanged and passes. Recording the plan's answer in the assembly, so the doctor can verify a gitlink's leg instead of naming it, needs adopt to write it and is filed as a follow-up issue by lane openRepoShape-2. Tests (tests/test_shape_doctor.py, 176 -> 181): `docs/theme` in the code leg is a `note` with the unverifiable wording and the rule id, verdict unchanged, in `--json` and the text report; the same gitlink in the spec leg gets the kept wording with the rule named; a code rule makes a gitlink in the spec leg unverifiable; `.cursor` and `.specify/vendored` in code keep the root-rule wording, now asserted with `placement` and `rule`; a project with both classes says them apart in one row; an unverifiable submodule beside a misplaced file adds its clause and leaves the FINDING and its `next`; the plan carries the class and the rule of three submodules. The expected wording is spelt out in the tests, not read back from the doctor. Against e0069c6's doctor 10 of the tests that touch a submodule fail; the unmerged-stage, `everywhere` and no-submodule tests pass on both. tests/test_shape_doctor.py, tests/test_cli_reference.py and tests/test_repo_hygiene.py: 244 passed (181 + 63). `render-cli-reference.py --check` ok: `--help` is unchanged, so docs/cli.md is too. The full suite was not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Lane: openreposhape-2 (openRepoShape-2)
Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…three unmerged-index fixtures stage `.cursor` and not `.cursor` plus a CR on the Windows job, where `tests-windows` has been red since fix round 1 (#176) (PR #186) The `tests-windows` job on PR #186 has been red since e0069c6 (fix round 1; bff8122's run was green) and is still red at 2ead645 (run 37905437159, job 113737647734): `3 failed, 1113 passed, 201 skipped`. The three are fixture assertions, not the doctor: `[file-then-gitlink]` and `[gitlink-then-file]` of `test_an_unmerged_path_with_a_file_stage_is_still_judged_as_a_file` and `test_an_unmerged_path_whose_every_stage_is_a_gitlink_is_one_submodule`, `assert [] == ['100644', '160000']` and `assert [] == ['160000', '160000']`. `git ls-files --stage .cursor` listed nothing after `stage_unmerged` had run with `check=True` and no error. Linux and macOS pass. Cause: `stage_unmerged` piped `<mode> <object> <stage>\t<path>\n` records to `git update-index --index-info` with `text=True`. A text-mode pipe writes each `\n` as `os.linesep`, which is `\r\n` on Windows, so git read the path as `.cursor\r`. A CR is a legal byte in a path for `update-index`, so git accepted it, staged the entries at `.cursor\r` and exited 0, and the later `ls-files --stage .cursor` found no entry. The `hash-object -w --stdin` call a few lines above had the same text-mode pipe; its blob is a placeholder, so it was harmless, but it is the same mistake. The fix: both calls hand git bytes (`input=lines.encode("utf-8")`, `input=b"x\n"`, no `text=True`; `hash-object`'s stdout is decoded), which no operating system rewrites. The docstring says why. Test-only: no assertion is changed and shape-doctor.py is not touched. Not chosen: NUL-terminated records with `update-index -z`. `-z` does work with `--index-info` on this box's git 2.43 (probed), but encoding is what removes the translation, it is the required part and sufficient, and it leaves the records in the very lines `ls-files --stage` prints. Nor was the test marked WINDOWS_SKIP: the doctor's placement of an unmerged path is the same on every system, and the Windows job is the one that proves it. The same mistake, found by grep: `grep -n 'input=' tests/test_shape_doctor.py` has four more hits, lines 738, 762, 782 and 798. Three are `input=""` to `bash openRepoShape ...` and the fourth pipes the `openRepoShape` shim's own text to `bash -s`; none feeds `update-index`, `mktree`, `hash-object` or a path to git, and all four tests are WINDOWS_SKIP, so they are left alone. The Windows pipe, reproduced on Linux (scratchpad review186/windows_crlf_pipe.py, git 2.43.0; the OLD code with the translation spelt out as `lines.replace("\n", "\r\n")`): OLD, LF: ls-files --stage .cursor -> two stages at `.cursor` OLD, CRLF: ls-files --stage .cursor -> '' ls-files --stage -z -> b'100644 587be6b4... 2\t.cursor\r\x00160000 8df1922e... 3\t.cursor\r\x00' NEW (stage_unmerged imported from this tree), file-then-gitlink: ls-files --stage .cursor -> '100644 587be6b4... 2\t.cursor\n160000 8e312f43... 3\t.cursor\n' NEW, gitlink-then-file and two gitlinks: both stages listed at `.cursor` tests/test_shape_doctor.py: 181 passed on Linux (git 2.43.0), unchanged from 2ead645. I cannot run the Windows job from here; the `tests-windows` run on this commit is the proof. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Lane: openreposhape-2 (openRepoShape-2)
…or a source whose `src/` or `contracts/` holds a submodule, spells every path its question names so a non-UTF-8 or newline name no longer crashes it or empties `--out`, and caps the split it names; `check` reads a key with no value beside no submodule as git does, says where git fails on a `.gitmodules` it cannot read, and why a named entry registers nothing; `execute` refuses with the one list `check` prints (#166) Round 4, from this lane's re-verification of ca5fb3d (S1-S3, N1-N4) and lane openRepoShape-2's passes 4 and 5 (NEW-1, NEW-2, NEW-3, R5-1, R5-2). NEW-1 / S1. `cmd_plan` computed the collision prediction, the follow-ups and the seeding record after `ask_about_submodules` had set the entry holding a submodule to `leg: null`. A submodule under `src/` dropped main's follow-up for a harness there that reads `contracts/`, and called the code leg empty: a SEEDED warning, `seeded_from_template: true`, and an `--allow-empty-leg code` it never needed. One under `contracts/` lost the harness line and the `CONTRACTS_DIR` line. All three are now read from `walk`'s entries, before the question is asked; a source with no submodule plans byte for byte as before. NEW-2 / S2 / R5-1. Every path the question names, the submodules and the entries that replace a directory alike, is `_spelled`: between backticks, a byte that is not UTF-8 as `\xe9`, a newline as `\n`, any other character that is not printable by a backslash, and a backslash or a backtick escaped so the backticks still delimit it. A raw surrogate made `write_lf` raise mid-write and leave `--out` empty; a raw newline read back as a space. The spelled newline, between double quotes, is the YAML entry that covers the file. R5-2. Past SPLIT_ENTRIES_NAMED (8) entries the question no longer names them one by one: "replace it with an entry for each path directly in `src/`, 402 in all", or "in one of `docs/`, `docs/theme/`, other than those directories" when it is nested. That edit is the one `check` passes. S3. A `.gitmodules` whose one defect is a key with no value is fatal to git's submodule reader alone, which git runs only for a gitlink. Beside no submodule it is now read as registering nothing: main adopts and clones such a source, and this branch no longer refuses it. Measuring every shape end to end against main also found where git really fails on a file it cannot read, so `plan-gitmodules-unreadable` now says so: beside a submodule, `git filter-repo`'s `git fast-export` reads the checked-out `.gitmodules` for each gitlink and fails the extraction of either leg, wherever the plan sends the two. That includes both of them dropped, which this branch passed and main and this branch both died on after making both leg remotes; it is refused now. Only a plan that extracts no leg escapes it. Otherwise the finding names the assembly's recursive clone, which fails in a leg that keeps a file with a line git cannot parse (no submodule needed), or a `.gitmodules` that is itself a gitlink. SUBMODULE_REMEDIATION no longer offers dropping such a file beside a submodule as an exit. NEW-3 / N1. `check` and `_refuse_what_check_finds` read one list, `_entry_findings`: coverage, each entry's `leg:` (#169's leg paths among them) and the submodule problems. A plan `check` rejected as uncovered and split was refused by `execute` for the split alone; it is refused for both, in one refusal, with #168's remedy and the submodule one. `_refuse_an_unrunnable_plan` is main's again, so its literal `8` cap is gone with the code that had it. #168's tests pass unchanged. N2. `.gitmodules` is now read into `Registrations`, which also keeps why a path an entry names registers nothing: the entry holding it has no url git reads, its name is one git ignores as suspicious, its path starts with `-`, or another entry's move dropped it. The `plan-submodule-unregistered` detail says that, and "no .gitmodules entry ... registers its path with a url" only when no entry names it. N3. git calls the file read through `--file -` "standard input"; the finding says `.gitmodules`. N4. `_SplitTree` reads `GITLINK_MODE` (`_verify`'s literal is left to #173), and `cat-file` is `GIT_CAT_FILE`, as `config` is `GIT_CONFIG`. Tests: new ones for each of the above (follow-ups and seeding for `src/vendor/dep` and `contracts/vendor/x`; four names no plan line can hold, built through `update-index --index-info` and skipped where git refuses them; a newline entry typed from the question; the capped split, flat and nested; why an entry registers nothing, eight ways; where git fails, six shapes; one refusal for an uncovered and split plan through `cmd_execute`); the unreadable-file matrix now refuses both dropped, and says the extraction; the end-to-end refusal gains that case and the end-to-end acceptance a key with no value beside no submodule. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it, so a `.gitmodules` that is itself a gitlink is pinned to the clone of its leg (#166) `_where_git_fails` names the extraction only for a FILE git cannot read beside a submodule: a `.gitmodules` that is itself a gitlink leaves `git fast-export` nothing to read, and git fails in the clone of the leg that keeps it (measured on main: execute 0, recursive clone 128). No test held that condition; `test_a_gitmodules_git_cannot_read_says_what_git_said` now asserts the ending of each of its four findings. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into fix/adopt-record-submodule-placement #189 makes `execute` record the leg each kept source submodule was answered into and `shape-doctor.py` read that record, so the doctor's read side builds on PR #186's gitlink handling. Brought in by merge, not by rebase; the branch is lane openRepoShape-2's and is not pushed to from here. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rce git will not record a name in, so the 13 tests of test_adopt_submodules.py that failed only on `tests-windows` no longer fail there (#166) PR #194 was green on Linux and macOS and red on `tests-windows`: 13 tests, two causes, both in the test helpers. adopt-project.py is unchanged: the plan it writes is LF on every platform (`write_lf`). 1. `edited()` found no `paths:` and no ` - path: <dir>` line in the plan (10 tests). `conftest.resolve` wrote the answered plan back with `Path.write_text`, which on Windows turns every LF into CRLF, and `edited` reads the plan as BYTES, for the LF `plan` writes. It now writes with `open(..., newline="\n")`: `write_text` has no `newline` before 3.10 and this standard runs on 3.9. 2. `raw_source` builds a source holding a name no filesystem need hold with `git update-index -z --index-info`, which exits 0 and says `Ignoring path` for a name git will not record, so the Windows runner committed a source with no `src/a\nb.py` and no `src/ven`d\or`, and the question never named them (3 tests: KeyError 'question', and a list without the file). The helper now reads the committed tree back with `git ls-tree -r -z` and skips the case when a name it was given is not there: asked of git, not of the operating system. Where git keeps the name the case runs, with its assertion unchanged. Two tests keep both from coming back on a platform that does not show them. `windows_text_mode` opens text files for writing as Windows does (CRLF for LF unless a `newline=` is named), around the one `answered()` it is for: with `resolve` reverted the plan holds CRLF and `edited` raises the ValueError of the CI log. A `.git/...` name, which every git leaves out of the tree, makes `raw_source` skip. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… answered into, under `project.yaml`'s `submodules:`, and shape-doctor.py holds every submodule a leg keeps to it, so one in its recorded leg is placed and one in the other leg is MISPLACED, where both were a note the doctor could not verify (#189) Which leg holds a source submodule is a human's answer in the adoption plan (#166), and the plan is committed nowhere. So after #176 the doctor could only note a kept gitlink, as `kept` or as a placement it cannot verify, and a submodule added by hand looked exactly like one a person had placed. - adopt-project.py: `submodule_record` appends one row per gitlink the plan keeps in a leg (path in the leg, leg, the covering entry's `resolution:`, source commit) to the split's `project.yaml`, read with `_answered_legs`, the reading `check` holds the plan to. Paths and reasons are always double-quoted (`_quoted`), so `null`, `true` or `0001` read back as names, and a byte that is not UTF-8 is spelled so `write_lf` can write it. A source with no submodule kept in a leg gets "" and the manifest it always got. - validate-manifest.py: `submodules` is optional; present, each row needs a relative `path` without `..`, once, and a `leg` of spec or code, with an optional text `resolution` and an optional 40-hex `source_commit`. - shape-doctor.py: `recorded_submodules` reads the record by adopt's key; `leg_submodules` places each gitlink by its row. In its recorded leg it is `recorded`, and a row of nothing but recorded submodules is `ok`. In the other leg it is `misplaced`: an entry in `misplaced` (`confidence: recorded`, `rule: null`, the reason carries the plan's resolution), so the row is a FINDING, the verdict `MISPLACED (n paths)`, and the placement plan lists it under `paths:`. A gitlink no row names keeps #186's note, word for word, and a row the validator refuses decides nothing. A project with no submodule, and one whose submodules no record names, gets byte-identical doctor output (text, --json, --placement-plan) and a byte-identical split (same tree object) against 85dd832, in one committed standard with only the two tools swapped between runs. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pt-record-submodule-placement shape-doctor.py's base is now main's; 4ce9cb0 carries the same shape-doctor.py and tests/test_shape_doctor.py as 5922521, merged earlier as 85dd832, so they reconcile with no conflict. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # shape-doctor.py # tests/test_shape_doctor.py
…cord-submodule-placement This branch is no longer stacked on fix/adopt-split-submodule-plan: #166 landed on main as e4decfb, whose tree (569fb8a) is the tree of 96c8353, the #166 head merged here as 1038dbc. The one textual conflict, in tests/test_adopt_submodules.py, is git meeting the same #166 file from both sides with #189's tests appended on one of them. It is resolved as main's copy of the file with #189's own diff (96c8353..1038dbc) applied on top, and adopt-project.py auto-merged the same way. The merged tree is 1038dbc's (e69e77a): nothing of #166 or #189 is dropped or changed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cord-submodule-placement Brings in 20e13f0 (#172 via #198), 8df21e6 (#179 via #201), a5b09c3 (#174 via #200) and 54aa69a (#165 via #173) on top of e4decfb. No rebase. Conflicts, and how each was resolved: - adopt-project.py, the module docstring's `execute` paragraph (textual). #173 ended the VERIFIES sentence with "and that the root's only submodules are the two leg mounts"; #189 added the sentence saying the split's `project.yaml` records each kept submodule's leg under `submodules:`. Kept both: main's clause closes the sentence, #189's sentence follows it. The docstring is not part of docs/cli.md (`render-cli-reference.py --check`: ok). - tests/test_shape_doctor.py, `test_a_submodule_in_the_other_leg_from_its_record_is_misplaced` (semantic; the text merged cleanly). #198 made `_leg_findings(plan, tree)` take `tree` with no default, on purpose (tests/test_adopt_leg_path_collisions.py, `test_leg_findings_takes_no_default_tree`), and this branch's test called it with one argument, so it died with `TypeError: _leg_findings() missing 1 required positional argument: 'tree'`. It now passes `None`, as main's other placement-plan callers do: a placement plan has no source tree. This is P2-1 of lane openRepoShape-2's review of e167d8b. `_leg_findings(plan, tree)` is main's, unchanged. `_commit_the_split` auto-merged with the record's `append` intact. #189's 16 tests (36 items) pass on this merge. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t-project.py writes a recorded submodule's unprintable character as one, so a submodule named with a no-break space, U+3000, a zero-width space, NEL or U+2028 is held to its row and moving it to the other leg is no longer COMPLIANT (#189) P3-1 of lane openRepoShape-2's review of e167d8b. `_quoted` spelled every character `str.isprintable` refuses as `ascii()` does (`\xa0`, ` `) and a byte that is not UTF-8 as `\xe9`, and `repo_shape._unescape` resolved only `\n \t \" \\`. So the row for `vendor/nb\xa0sp` read back with a literal backslash, named no gitlink, and the doctor called the submodule `kept`; a real adoption of each of six such names, moved to the other leg, stayed COMPLIANT (scratchpad/fix189e/logs/probe-names-dbf8887.log). - scripts/repo_shape.py: `_unescape` also resolves `CODE_POINT_ESCAPES`, YAML's `\xNN`, `\uNNNN` and `\UNNNNNNNN`, with YAML's meaning: the code point, never a byte. One with too few digits, or past U+10FFFF, keeps its backslash, and `\\x41` is still a backslash and `x41`. - adopt-project.py: `_quoted` writes only the escapes those two tables resolve: the named ones for a backslash, a quote, a newline and a tab, and the shortest code-point escape for every other unprintable character. A byte that is not UTF-8 is now `\udce9`, which reads back as the surrogate the path holds, where `\xe9` read back as U+00E9, another name. The doctor reads `project.yaml` with the standard's own `repo_shape` (shape-doctor.py imports it from SHAPE_ROOT/scripts), and the project's own validator with its copy, which `execute` writes from the same standard. PINNED FILE: scripts/repo_shape.py. Every assembly root and every family holder reads `upstream-changed scripts/repo_shape.py` in `update-shape.py check` until its estate re-pins with `apply`, as for #156 and #193; this repository has no digest of it to update. `apply` copies every upstream-changed row together and rolls back if the project's validators go red, so the record's validator never runs beside an older reader. HOW THIS MEETS #199 (fix/adopt-plan-linebreak-names, not on main). #199 adds the same reader to repo_shape.py and more: it also resolves `\r`, and it adds the writer `double_quoted` from the same tables. Whichever lands second takes #199's side of repo_shape.py's escape section, deletes `_quoted`, `_escaped` and `_NAMED_ESCAPES` here, and spells both `submodule_record` fields with `double_quoted`. A record this branch writes reads back unchanged under #199's reader, because both spell by YAML's code points; the one difference is a carriage return, `\x0d` here and `\r` there. Tests: the reader's escape table gains the code points and the cases that must keep their backslash (tests/test_windows_paths.py); the record's round-trip names gain nine unprintable ones and a literal `\x41`, and the non-UTF-8 name now reads back as itself (tests/test_adopt_submodules.py); and the doctor holds six such names, written by `submodule_record`, to their rows: `recorded` in code, MISPLACED (6 paths) once staged in spec (tests/test_shape_doctor.py). Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne way Git records a path, by the segment rule repo_shape.py now holds for it and for a leg's mount path alike, so `./x`, `x/`, `a//b` and `a/./b`, which name no gitlink the doctor could hold to them, are `manifest-submodule-path` findings (#189) The validator half of P3-2 in lane openRepoShape-2's review of e167d8b. The validator accepted `./x`, `x/`, `a//b` and `a\b` as a row's `path`. - scripts/repo_shape.py: `path_spelling_fault` and its five `PATH_*` codes, the segment half of #179's leg-path rule: empty, absolute, a `..` segment, naming the top itself, any other empty or `.` segment. It lives here because a project carries this file and not shape_materialize.py. - scripts/shape_materialize.py: `_spelling_reason` asks it for the segments and keeps its own sentences, in its own order: the same answer, byte for byte, for 33 spellings compared against dbf8887, and tests/test_adopt_leg_paths.py and tests/test_scaffold_leg_paths.py pass unchanged. - templates/assembly-root/scripts/validate-manifest.py: a row's `path` is held to it, with a sentence about a submodule; the docstring documents the record's schema field by field. A BACKSLASH AND A DRIVE LETTER ARE ACCEPTED, against the brief, with evidence. On POSIX each is a character of a name: `git update-index` records `a\b`, `C:/x` and `vendor/a\b` and refuses `./x`, `x/`, `a//b`, `a/./b`, `a/../b`, `/x` and `.` as `Invalid path` (git 2.43). A real adoption of a source with a submodule at `vendor/a\b` plans, checks and executes, records `"vendor/a\\b"`, and the doctor holds the gitlink to it (`recorded`; MISPLACED once moved), so the review's "never matches" holds for the other three spellings and not for this one. Refusing it would make the manifest `execute` writes INVALID. The leg-path rule still refuses both, because a person TYPES a leg path for commands Windows also runs; that half stays in shape_materialize.py. `leg_path_problem` was never the rule to reuse: it also refuses non-ASCII names such as `café`. PINNED FILES: scripts/repo_shape.py (as in the previous commit) and templates/assembly-root/scripts/validate-manifest.py (as already under #189): `upstream-changed` in every assembly root, and repo_shape.py in every family holder, until each re-pins with `update-shape.py apply`. Tests: tests/test_manifest_validator.py refuses five more spellings, says why for one, accepts five names Git records (a backslash, a drive letter, non-ASCII, a code-point escape, a space), and holds the rule to `git update-index` itself on thirteen paths, two of them POSIX only; and tests/test_shape_doctor.py places a recorded `vendor/a\b` with the manifest `ok` (POSIX only). Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, as the project's validator reads the record, so a first row whose `leg:` it refuses now decides nothing and the duplicate it names no longer stands in for it, and first-row-wins is pinned by a test (#189) Nit 4 of lane openRepoShape-2's review of e167d8b: mutation MU12, the last duplicate row winning, survived every test, because no test wrote two rows for one path. THE CHOICE, WITH ITS EVIDENCE. The brief offered two: the validator refusing two rows that name one path, or a test pinning first-row-wins. The validator already refuses the duplicate: `_submodule_path_problem` records the first good path in `seen` and names the SECOND row, `submodules[1]: path '.cursor' is recorded twice` (MU11 was killed by `path-twice`). So the doctor's rule, "a row the validator refuses decides nothing", already means the first row decides, and this pins it. Two rows are only ever an edit: `execute` writes one row per gitlink. Whichever row decides, the `manifest` row is a FINDING and the doctor exits 1 (lane 2's probe: INVALID or MISPLACED, never COMPLIANT). One reading still differed from the validator's. `recorded_submodules` skipped a row with a refused `leg:` before it claimed the path, so with `leg: root` then `leg: code` the second row decided, though the validator refuses both. The first row naming a path now claims it, and decides only if its leg is `spec` or `code`. Tests: `test_where_two_rows_name_one_path_the_first_decides`, three orders (code then spec: recorded; spec then code: misplaced; root then code: kept), each with the validator naming `submodules[1]` and the manifest row a FINDING. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a leg's `.gitmodules` before that leg's questions are counted, so an adoption whose record places every submodule reads `ok` where it read a `note` naming its own `.gitmodules` (#189) P3-3 and nit 3 of lane openRepoShape-2's review of e167d8b. The placement row was never `ok` on a real adoption: the leg that keeps the source's submodules keeps the `.gitmodules` registering them too (#166 refuses a plan that splits them), rule `ambiguous-gitmodules` makes that file a question, and `placement_verdict` returns `note` on a question before it reaches `submodules_verdict`. Every real adoption in the probe showed `review=['code/.gitmodules']` (scratchpad/fix189e/logs/probe-names-dbf8887.log). The question is which leg owns the submodules that `.gitmodules` registers, and in a leg that keeps a submodule the record places there, the record is that answer and it is this leg. So `audit_leg` now asks `leg_submodules` first, and `gitmodules_the_record_answers` names the leg's `.gitmodules` as answered: it is placed, not a question, the leg's entry lists it under `answered` (only where there is one), and the row's held clauses say so (`answered_clause`). In a leg that keeps no submodule the record places there -- the record puts it in the other leg, or there is no record -- the file stays the question it was, word for word. `walked_row` and `walked_findings` are split out of `audit_leg`, which would otherwise have grown a nested branch; the dictionary each walked path becomes is the same, key for key. VERDICTS: only the row's status moves, `note` to `ok`, for a leg whose record places its submodules; the exit code and the verdict line are what they were in every case, and a project with no record gets the report it got. Tests: `test_the_record_answers_the_question_about_the_gitmodules_beside_it` (recorded in this leg: `ok`, no question, the clause; recorded in the other leg: FINDING, still asked; no record: `note`, still asked), and `test_an_adopted_submodule_is_recorded_and_placed` now asserts the row is `ok` with no question left (nit 3, the review's tests/test_shape_doctor.py:3714). Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s `.gitmodules` only where it places every submodule that leg keeps, so a submodule added by hand beside a recorded one no longer drops that question or has the row say the record answered it (#189) A finisher's correction to 90608b8 (P3-3 of lane openRepoShape-2's review of e167d8b). `gitmodules_the_record_answers` called a leg's `.gitmodules` answered when ANY submodule the leg keeps was recorded there. Beside the recorded `.cursor` a code leg may keep `vendor/lib`, added by hand after the adoption: `git submodule add` registers it in that same `.gitmodules`, and no row names it. At 90608b8 the row then dropped the file's question and said "project.yaml's `submodules:` answers the question the policy asks of code/.gitmodules: which leg owns the submodules it registers", which the record does not answer for `vendor/lib`; and the same beside a second submodule the record puts in the spec leg. Now the record answers it only where it places EVERY submodule the leg keeps in that leg. Otherwise the file stays the question it was, beside the note or the finding that names the other submodule. VERDICTS: none moves. A leg keeping an unrecorded or a misplaced submodule was already a `note` or a FINDING on that submodule's account, and still is; a leg whose record places every submodule it keeps still reads `ok`. Only `review_required`, the leg's `answered` and the row's clause change, and only in those mixed legs. With no record, nothing is recorded, so nothing is answered, as before. Tests: `test_the_question_stays_where_the_record_does_not_place_every_submodule`, two cases: a hand-added submodule the same `.gitmodules` registers (`note`, the file still asked) and one recorded in the other leg (FINDING, still asked). Both fail at 90608b8 with `review_required == []`. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…spelled by their code points, so a reviewer sees the no-break, ideographic and zero-width spaces, the soft hyphen and U+2028 the test is about, and an editor offering to strip an unusual line terminator can no longer rewrite one of them (#189) `ESCAPED_NAMES`, added in cd2a622, held five of its six characters raw: a no-break space, U+3000, a zero-width space, a soft hyphen and U+2028, which read as a space, a wider space, nothing, nothing and a line break, and only NEL as `\u0085`. tests/test_adopt_submodules.py, test_windows_paths.py, test_manifest_validator.py and test_commit_trailers.py spell the same characters as escapes. Now the tuple does too, and the file holds none of them raw. The values are unchanged: `ast.literal_eval` of the old and the new tuple compare equal, and the test passes as it did. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce submodule's leg under `project.yaml`'s `submodules:`, and "Checking" point 4 says a submodule is judged by that record, with `rule: null` and `confidence: recorded`, so an assistant no longer reads a MISPLACED submodule with no rule id as a broken row or edits the record to turn it green (#189) Nit 1 of lane openRepoShape-2's review of e167d8b: AGENTS.md was not updated for the record. - "Adopting an existing repository", after step 7: `execute` also records the plan's answers about the source's submodules in the split's `project.yaml`, under `submodules:` -- per submodule a leg keeps, its `path`, `leg`, `resolution:` and `source_commit`; it confers nothing, nothing rewrites it, and the doctor holds each submodule to it. - "Checking a repository's compliance", point 4, which said every MISPLACED path carries "the rule id that judged it": a submodule is judged by the record instead (`rule: null`, `confidence: recorded`), is a `note` where no row names it, and one moved on purpose stays MISPLACED, its row the human's to change. "Nothing rewrites it" was checked against every writer of `project.yaml`: only the scaffold and `adopt-project.py execute` write the file; `update-shape.py apply` edits the two lines inside `shape:` and nothing past that block (`rewrite_manifest`, `_shape_block_bounds`), and `bump-leg.py` writes the leg's pin and the workflows. The schema, field by field, stays where the validator documents it (validate-manifest.py's docstring, 379763c). tests/test_repo_hygiene.py: the cap moves 464 -> 472, with the paragraph saying what the eight lines buy, as every earlier move did. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…) into fix/adopt-record-submodule-placement Brings in e7d1720 (#208), 404760f (#213) and 4835fb4 (#199) on top of 54aa69a. No rebase. #199 landed first, so this branch is the one that lands second, and it takes #199's side of the double-quoted escape section, as cd2a622's message and lane openRepoShape-2's review of e167d8b both said it would. Conflicts, and how each was resolved: - scripts/repo_shape.py, two hunks (textual): the escape section and `_unescape`'s docstring. Both sides added `CODE_POINT_ESCAPES`, `_HEX_DIGITS` and an `_escape_at` whose body is the same line for line, and both reworded the docstring; #199 also added `r` to `DOUBLE_QUOTED_ESCAPES` and the writer, `double_quoted` with `_escaped` and `_ESCAPE_OF`. The auto-merge had also left this branch's own copies of `CODE_POINT_ESCAPES`, `_HEX_DIGITS` and `_escape_at` beside main's. Resolved as main's file, byte for byte, plus this branch's one change to it that is not about escapes: 379763c's `path_spelling_fault` section and its line in the module docstring, applied with `git apply`. `git diff origin/main -- scripts/repo_shape.py` is that 51-line addition alone. Merged without a conflict, and checked: - adopt-project.py: main's `y` now quotes with `double_quoted`, and this branch's `submodule_record` still quotes with `_quoted`, which reads main's table now, so it writes a carriage return as `\r`, where it wrote `\x0d`. The two writers give the same text for every character to U+2FFF, every surrogate and seven more (14,343 compared, 0 different). The next commit removes `_quoted`. - scripts/shape_materialize.py: main's `write_paths_listing` and this branch's `path_spelling_fault` import and `LEG_SEGMENT_REASONS`. Every merged module compiles. On this merge, tests/test_windows_paths.py, test_adopt_plan_escapes.py, test_yaml_flow_quotes.py, test_manifest_validator.py and the record's two name tests in test_adopt_submodules.py: 218 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…double_quoted`, the writer #199 gave the reader, so the record and the adoption plan share one escape table and `_quoted`, `_escaped` and `_NAMED_ESCAPES` are gone (#189) #199 landed first (4835fb4, merged here in 04c4e14), so this branch takes its writer, as cd2a622's message and lane openRepoShape-2's review of e167d8b said the second to land would. After that merge two writers spelled a double-quoted scalar from the same two tables: `repo_shape.double_quoted` for `y`, and this branch's `_quoted` for `submodule_record`. On the merge they give the same text for every character to U+2FFF, every surrogate and seven more (14,343 compared, 0 different). - adopt-project.py: `submodule_record` spells `path` and `resolution` with `double_quoted`; `_quoted`, `_escaped` and `_NAMED_ESCAPES` are deleted, and `CODE_POINT_ESCAPES` and `DOUBLE_QUOTED_ESCAPES` leave the import, which is main's again. Why the record always quotes (`null`, `true`, `0001` would read back as something that is not a path) and spells a character that is not printable by its escape (NEL and U+2028 end a line, a no-break or zero-width space is another name, a surrogate cannot be written to UTF-8) moves into `submodule_record`'s docstring. - scripts/repo_shape.py: `double_quoted`'s docstring names the record among the writes that quote with it, and why the record needs it read back. No byte of any record changes: after 04c4e14, `_quoted` already read #199's table and wrote a carriage return as `\r`, as `double_quoted` does, where cd2a622 wrote `\x0d`. tests/test_adopt_submodules.py's round-trip docstring now says so. Tests: tests/test_adopt_submodules.py, test_windows_paths.py, test_adopt_plan_escapes.py and the two doctor tests of an escaped or a backslashed recorded name: 264 passed. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 12, 2026
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
… conflict with #394 (#225) 7a8b981 (#394, issue #189) moved a leg path's segment rule into `repo_shape.path_spelling_fault` and added it, with `PATH_CLIMBS`, `PATH_ITSELF` and `PATH_NOT_CANONICAL`, to the `from repo_shape import (...)` list in scripts/shape_materialize.py, the list this branch had added `parse_yaml` to. GitHub could no longer update the branch. The one conflicting hunk is resolved as main's list. The branch's `parse_yaml` is left out because main has imported it at the top of the module since #380 (`from repo_shape import YamlError, _strip_comment, parse_yaml`), and `_read_back` uses that binding. Naming it in both imports was a redefinition that pyflakes reported. With this merge the pull request changes no import line. Everything else merged without a conflict and keeps both sides. From main: the `EXCEPT THE SEGMENTS` comment, `LEG_SEGMENT_REASONS`, and `_spelling_reason` asking `path_spelling_fault`. From this branch: the #225 paragraph, `_leg_path_lines`, `_misread_reason`, and the read-back step in `leg_path_problem` and `_canonical_spelling`. The read-back is still asked last, after the spelling, Git's and Windows' names and the alphabet. 7a8b981's `submodules:` record adds no line that holds a leg path. Each row's `path:` is a submodule's path inside its leg, and `leg:` is `spec` or `code`. `execute` appends the block in code instead of rendering it from a template, and it double-quotes every path. The rule still finds the four real template lines. An adoption at `--spec-path legs/v2 --code-path c0de` with a recorded submodule writes a leg path on those four lines only, and its bootstrap passes 7a8b981's validator. The merge also brings in ec4a7a0 (#456), 437a360 (#343) and d222a12 (#397). None of them touches a file of this pull request. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
…urce and both legs `execute` has just pushed into through git, with `clone_just_pushed(..., branch=...)`, and imports `FILE_PROTOCOL` again for its `submodule add`, so #394's three adopted-submodule tests no longer die in `NameError` and none of its clones walks a just-pushed remote's object files (#440) This composes #467 with #394. #467 removed `FILE_PROTOCOL` from this file's `from conftest import (...)`, because both of its own uses had become `clone_just_pushed(...)`. #394 then added two new uses in the `adopted_cursor` fixture, and merging `main` into the branch composed as text and not as meaning: the fixture died `NameError: name 'FILE_PROTOCOL' is not defined` at its `git submodule add`, and with it `test_an_adopted_submodule_is_recorded_and_placed`, `test_an_adopted_submodule_whose_record_is_edited_is_misplaced` and `test_a_submodule_added_by_hand_after_adoption_is_the_note` (3 errors on 0760a28). - The fixture's `git clone --recurse-submodules -b adopt/three-repo-shape` of the source reads three repositories `execute` pushed into a moment before: the source itself, which takes the split branch, and the two bare legs in `--local-remote-dir`. That is #440's race, so it is now `clone_just_pushed(source, clone, branch="adopt/three-repo-shape")`. The rewrite's prefix is the source's parent, which holds `remotes/` in this fixture, so both legs are read as `file://` URLs. - `clone_just_pushed` takes an optional `branch`, spelled `-b <branch>` ahead of `--recurse-submodules`. A caller that omits it gets the command it got before. - `FILE_PROTOCOL` is imported again for the fixture's `git submodule add` of `dependency`: a repository the test only committed in and never pushed into, so there is no background pack for that clone to lose a file to, and it stays a plain-path clone. - New: `test_the_adopted_clone_was_read_through_git_not_through_the_remotes` asserts the clone's root and both legs hold a pack and no loose object, as #467's tests of its other clone sites do. With the plain-path clone this fixture had, the root holds 37 loose objects and the test fails. Unchanged: every assertion of #394's three tests, and every clone site #467 converted. Left out: the other test files that clone a just-pushed remote by plain path with `*FILE_PROTOCOL` (the adopt and family suites among them); #467 is the session fixtures and the two entry points. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 12, 2026
…endent review of the v1.0.0 accuracy pass asked for, among them exit 3 and the piped setup one-liners (#483) The review of bfbd4ee found two false corrections and nine gaps. This merges main (7a8b981, #394) and fixes them, line-neutral (AGENTS.md 490, README.md 1422, the caps tests/test_repo_hygiene.py holds them to). - Exit 3 is "usage or environment": a flag argparse rejects is exit 2, but a `--placement-plan` the tree cannot take is exit 3 (shape-doctor.py `main`, `shape-doctor-no-placement-audit`). The round-one text said "environment alone". - AGENTS.md no longer says the doctor's next commands each have a section in that file; it says each command keeps its own refusals, and that a FINDING carries a next command where one can be named. - The `curl ... | bash -s --` one-liners (README.md, twice, and the worked example; AGENTS.md section 2) run from a file, `bash <(curl ...)`, so stdin stays the terminal and the run can ask. Under a pty each form printed `Type yes to continue:`; the piped form printed `REFUSED: ... no terminal to confirm on`. - AGENTS.md `check` lists `already-at-target`, `upstream-removed`, `unmapped` and `copy-missing` as README.md does; its machine-row item says `openRepoShape --preflight` offers only the `gh` set-up. - The preflight also offers `gh`'s credential helper (README.md, four handbook panels); `openRepoShape --preflight` exits 2 when `git` or a Python 3.9 or newer is missing; the park sweep skips a STANDALONE root with no overlay; the leg suffixes are lowercase against the neutral-product, domain-descendant and family forms. - Since #394 a submodule in the other leg than its `submodules:` record's is MISPLACED; README.md's placement paragraph and the handbook's say so (the table row sits against #468's INVALID row, so it stays for #468). docs/handbook.html mirrors the changes and its footer blob is recomputed. Four paragraphs of README.md and two of AGENTS.md that no correction touches are re-wrapped, no word changed, to give back the lines. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
…submodules:` record is appended to `project.yaml` by the one materializer call that also hands it this branch's `spell=_spelled` Brings in, by sha, main's two commits since 437a360: d222a12 (family.py `init`, #307 via #397) and 7a8b981 (#189 via #394). One conflict hunk, in `_commit_the_split` of adopt-project.py, resolved by keeping both sides: #189 builds `append` (the Makefile block, and `project.yaml`'s `submodules:` record when the split keeps a submodule in a leg) inside #188's late-failure reporting, and this branch passes the materializer `spell=_spelled` (#212) in the same call. The merged call is `materialize_assembly_root(..., append=append, spell=_spelled)`, with both comments kept. scripts/shape_materialize.py merged by git alone: #189 moves the leg path's segment rule into `repo_shape.path_spelling_fault` and imports it; this branch's hunks are elsewhere. Checked on the merged tree: each side's own change reappears whole, the one line both changed apart (sides_intact); no top-level name in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, scripts/family.py or shape-doctor.py is bound twice that was not already so on main (the materializer's `YamlError` and `parse_yaml`, shape-doctor.py's `sys`), and no name is loaded and bound nowhere. #189 changed no signature of a function this branch calls. This branch does not edit AGENTS.md, so #189's move of tests/test_repo_hygiene.py's cap is main's alone. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
…r-tail One conflict, adopt-project.py's module docstring `execute` paragraph: this branch's sentence (the topic on all three, then the split's pull request against the tracking branch, both skipped for local remotes), then main's sentence on the `submodules:` record in the split's `project.yaml`. Everything else merged without conflict; docs/cli.md re-rendered byte-identical. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
…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>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
…pin-file: #409's four files take main's blobs, which are #409's final head's, `scaffold-project.py` keeps the pin code this branch moved out and gains main's own changes, and #445's `try` in `_commit_the_split` carries this branch's `neutral_pins=` argument beside #394's `append` #409 squash-landed as 9736424 (its final head b70a07d). This branch held #409 at 6494714, and after it #409 only merged main five times, none with a hand edit (`git show --remerge-diff` empty for bac7dee, 480c348, efb7322, 5bdcae3 and b70a07d). `git diff b70a07d 9736424 -- <file>` 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. Conflicts, four files, one hunk each: - scaffold-project.py. #409's squash re-adds `PIN_COLLISION_REMEDIATION`, `PIN_VARIABLE_REMEDIATION`, `_refuse_colliding_pins`, `_colliding_pin_variables` and `_pin_collision_remediation`, which this branch moved to scripts/shape_materialize.py (#273). The hunk takes this branch's side, empty, and #409's auto-merged import of `PIN_SOURCE_ENV_PREFIX, pin_source_env_collisions` (and its two comment lines), which nothing in the file uses any more, is dropped. Not the whole file `--ours`: main changed scaffold-project.py beyond #409 since this branch's last merge (6c2a371's `--tracking-branch` check, a73b4e9's `RULESET_HINT`), and those lines stay. The result equals the file `git merge-tree` gives for this branch with main before the squash (518010b), blob fbe7215, and its diff from this branch's tested file has the patch-id of main's own 3ef059c..518010b change to the file. - templates/assembly-root/scripts/validate-pins.py. Main's side, which is #409's final blob (5f83eb7) with main's #359 workflow reader. This branch had no change of its own to the file beyond #409 at 6494714. - adopt-project.py, `_commit_the_split`. #445 wraps the function in its late-failure `try` with `step` names, and #394 builds an `append` with the plan's submodule record. This branch's only edits there were a comment and `neutral_pins=_recorded_pins(plan)` on the materializer call. Resolved with main's side, and the comment and the argument on main's call inside the `try`: `append=append, neutral_pins=_recorded_pins(plan)`. - docs/cli.md, generated: regenerated with scripts/render-cli-reference.py over the resolved tree (`--check` ok). It differs from main's only in `adopt-project.py plan`'s `--pin`, `--pin-owner` and `--pin-source` help. scripts/repo_shape.py and tests/test_pin_source_env_collisions.py auto-merged to main's blobs (82346be, 5220787). pyflakes reports the same four warnings on the merged adopt-project.py, scaffold-project.py and scripts/shape_materialize.py as before the merge. #445's tests/test_adopt_late_failure.py hands `_commit_the_split` a `SplitPlan`, a `dict` that adds `entries`, as its plan, which `_read_pins` cannot read yet; the next commit makes it read one. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
…mmit_the_split` takes the `SplitPlan` mapping #445's tests hand it as the plan and no longer stops at "committing the split" with an AttributeError, and a loaded plan reads exactly as before (#273) #445 (75052cb) tests `_commit_the_split` directly. In tests/test_adopt_late_failure.py, `split_arguments` passes `plan=SplitPlan()`, a `dict` subclass that adds `entries` (#394). Those are all the function read before this branch: `plan.get("follow_ups", [])` and `plan.entries`. Since #273 it also writes each declared pin, `neutral_pins=_recorded_pins(plan)`, and `_read_pins` read `plan.data.get("pins")`. On the merge commit before this one, three of #445's tests failed (3 failed, 83 passed in that file), each with "STOPPED committing the split by AttributeError: 'SplitPlan' object has no attribute 'data'": - test_a_split_that_fails_names_its_step_and_both_pushed_legs[commit] - test_a_split_that_fails_names_its_step_and_both_pushed_legs[push] - test_a_split_the_materializer_cannot_write_is_refused_with_the_notice `Plan.get(key, default=None)` returns `default` when the value is None, and the value otherwise. With no default it returns `plan.data.get("pins")` exactly, so a loaded plan's `pins:` reads as before, absent and `null` alike, and the `rows is None` branch below is unchanged. A mapping has the same `get`, so the test's plan reads too. This was the only `plan.data` in the file. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 13, 2026
…s say a FAMILY holder also gets **agent files** and a directory that is neither also gets **machine**, and the `MISPLACED (n paths)` row says a submodule in a leg other than the one its `submodules:` record names counts too (#424) Lane openRepoShape-5's accuracy read of #468. The registry has Check("agent-files", ..., (PROJECT, FAMILY), ...) and Check("machine", ..., (PROJECT, FAMILY, NOT_A_ROOT), ...), so the holder list and the plain-directory list were each one row short. `recorded_placement` and `leg_submodules` have counted a submodule held in the other leg from its record as misplaced since #394 (test_a_submodule_in_the_other_leg_from_its_record_is_misplaced), which the verdict row did not say. The footer's `README.md blob` follows the README; README.md stays at 1422 lines (the next paragraph was re-wrapped to absorb the one line the longer row list needs). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
…al` and its two leg clones are rewritten to `file://`, and the two session fixtures that clone a scaffold do the same, so a maintenance run packing the just-pushed objects mid-walk no longer dies `failed to copy file to ... No such file or directory` (#440) (#467) A clone of a plain path is git's local clone: it lists the source's `objects/` and links or copies each file in turn. The scaffold has just pushed into those bare repositories, background maintenance can pack the loose objects and delete them between the listing and the copy, and the clone dies. On the macOS runner that failed every doctor test at setup twice on 2026-10-12 (124 errors, then 118) and `test_setup_sh.py` once on 2026-10-11, and `setup.sh --local-remote-dir` refuses after all three repositories exist. A `file://` URL is read by upload-pack through git's object store, which a concurrent repack does not break. - The root clone of `--local-remote-dir` passes `--no-local`. It does not depend on how the remotes directory is spelled. - The two leg clones `--recurse-submodules` makes are git's own, from the plain path `.gitmodules` records, and no flag reaches them. Measured on a scaffold's remotes, `--no-local` alone leaves the root packed and both legs loose (8 loose objects, no pack). A `url.<file URI>.insteadOf` rewrite of the remotes directory, passed as `-c`, reaches the child clones: root and both legs are one pack each, and the clone's own `origin` and `submodule.<name>.url` still record the plain paths. The prefix is the directory RESOLVED, as `scaffold-project.py` writes it into `.gitmodules`, in both spellings of a Windows path. - `tests/conftest.py`'s `scaffolded` and `tests/test_shape_doctor.py`'s `scaffolded_here` clone through one `clone_just_pushed` helper: `--no-local` and the same rewrite, `protocol.file.allow=always` kept for the submodules, one scaffold per session as before. - A test per site asserts, with `git count-objects -v`, that the root and both legs hold a pack and no loose object: the session fixture (`test_scaffold_e2e.py`), the doctor fixture (`test_shape_doctor.py`) and the end-to-end run (`test_setup_sh.py`, `test_setup_project_py.py`). One helper in `tests/conftest.py`, `assert_read_through_git`, makes the check, and each condition (no loose object, at least one pack) is its own assertion; the two setup tests are one call to it each, under their own names. A fifth drives the rewrite alone on a plain path in a directory whose name a URL must encode (a space and a `%`), and a sixth runs the end-to-end through a symlinked remotes directory, where the rewrite's resolved prefix misses the root's URL and only `--no-local` reads the root through git (the macOS temporary root, `/var`, is such a symlink). All six fail on main and pass here. - With #394 on main: its `adopted_cursor` fixture in `tests/test_shape_doctor.py` keeps `protocol.file.allow=always` for the `git submodule add` of a repository it only committed in, and clones the adopted source's split branch through `clone_just_pushed`, which takes an optional `branch` (`-b`, emitted only when given); one more test asserts that clone was read through git. Unchanged: a clone from GitHub (the `https://` URLs, no `--local-remote-dir`) gets no new argument; `protocol.file.allow=always`; the `core.autocrlf=false` clone on Windows; every `--help` text (docs/cli.md is not regenerated). Left out: the macOS race itself is not reproduced here (git 2.43 on Linux; nothing runs maintenance in the remote). Injecting the deletion instead, a repack (or a prune-packed over a pre-built pack) racing the clone, kills a plain-path clone of the root 31 times in 60 and of a leg 26 times in 60 with the error above, and a `--no-local` root and a leg through the rewrite 0 times in 60 each. Per-test clones of a pushed remote in other test files still use a plain path; none has failed. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
… is `PRODUCT=PATH` only when its key names a chain link, and a link source that cannot be entered is a WARNING with the system's reason, not a traceback (#338) (#453) **What was wrong.** Two faults, one in the parser and one in the lookup. - **The parser.** `_link_sources_from_args` split every `--link-source` value at its first `=`. A link tree under a directory named `a=b`, passed bare, became a source keyed to a link `a` that no chain names. Nothing asked for it, so the link stayed `declared-unverified`, exit 0. The WARNING then offered `--link-source openXdox=<path>` to the person who had passed exactly that. A mistyped key failed the same silent way. - **The lookup.** `repo_shape.link_manifest_path` asked `Path.is_dir()` and then `is_file()`. Before Python 3.14 those swallow only ENOENT, ENOTDIR, EBADF and ELOOP. So each of these ended the naming gate in a traceback, exit 1: - a keyed source under a parent this user cannot enter; - `SHAPE_PIN_SOURCE_<LINK>` pointing there; - a checkout `../openXdox` at mode 000, which every `--project` run asks for; - a 300-character name. **What changed.** **How a value is read** (`LinkSources` in `scripts/validate-repository-naming.py`). This mirrors #312's `PinSources`, with a chain link where that file has a declared pin: - The text before the first `=` is a LINK only when it casefolds to a link of a chain this run reads. Those are the entries of `--referent-chain`, and of each leg's `naming.referent_chain:` under `--project`. Both are read before any leg is classified. - The keyed reading wins even over a directory spelled like the whole value. `./` reads that directory. - Any other value is a bare PATH, taken whole. **What a lookup says** (`scripts/repo_shape.py`): - `link_manifest_lookup` asks `stat()` inside a guard, as `_digest_from_source` does in #312. It returns the system's reason when a source cannot answer, or says which of four plain cases it is (`no such file or directory`, `it holds no project.yaml`, `its project.yaml is not a file`, `it is neither a directory nor a file`), and it never raises. It replaces `link_manifest_path`, which is gone: nothing calls it. `link_declarations` gives the reason a manifest could not be read, and `declared_neutral_pins`, which it left with no caller, is removed (fix round 1). - `resolve_link_source` and `link_pins_from_trees` take a keyword-only `tried`. It records each source asked, in order, with its reason. Nothing in `repo_shape` prints. - `link_pins_from_trees` also takes a keyword-only `with_reasons` (fix round 2). Asked, it returns a `LinkPins`, a dict equal to the plain mapping that also remembers why a tree it reached could not be read. Only the scaffold asks. **What the validator prints** (stderr, after each name's WARNINGs): - `WARNING link-source-unreadable: <path> (<how>) could not answer for <link>: <reason>`. This covers a `--link-source` asked for a link that holds a pin, a `SHAPE_PIN_SOURCE_<LINK>` likewise, and a checkout beside the project that exists and still cannot answer. A keyed value that also names a directory gets a second line saying how to ask for that directory with `./`. A bare value holding `=` says it was read as a PATH. - `NOTE --link-source <typed>: not used — <why>.` for a value no such link asked for. The why is one of five: a later value replaced it; no name reads that link's own declaration (a keyed value); no name records a chain at all; no name reads any chain link's own declaration (a bare value, where chains exist and none is read); or every link that is read had a more specific source. - Three kinds of source are never named: a variable or a checkout asked only for a chain's LAST link (no one ever reads that link's own declaration); a variable or a checkout for a chain no classification reads (fix round 1: a spec leg's chain, a name with no referent, a chain whose shape fails; a `--link-source` value for such a chain is the NOTE); and a checkout that is absent or holds no manifest. - `--quiet` drops both, with the WARNINGs. So `make bootstrap` and the doctor's `naming` row print nothing new. `--help`, the module docstring and `docs/cli.md` (regenerated) say all of this. **Fix round 1 (lane 5's review of 40762ca: 0 blockers, 2 should, 4 nits).** Second commit, 4b21263. Each review finding, in order: - **A link manifest whose `neutral_product_pins:` is not a list of names is a tree that cannot answer** (SHOULD-1). `link_declarations` iterated `data.get(key) or []` as it stood, against the `link_pins_from_trees` docstring this change wrote ("nothing a source does raises out of here"). `5` ended the naming gate, the scaffold and the project's `validate-manifest.py` in `TypeError: 'int' object is not iterable`. A scalar `openDox` was read as the characters `D`, `e`, `n`, `o`, `p`, `x`, so the chain broke, and under `--project` that is a FINDING. A mapping was read as its keys, so `openDox: yes` DECLARED `openDox`. Each is now `(None, why)`, as `not a mapping` is: declared-unverified, with the reason named ("`neutral_product_pins` is not a list", "holds an entry that is not text"). An absent or null value, an empty list, and a blank entry (`null`, `''`) in a list, declare nothing, as ever. The fix is in the shared function, so the scaffold and `validate-manifest.py` are reached too. **Three behaviours change on purpose here, and the tests of fix round 2 pin each:** - **A falsy value is no longer an empty list.** `neutral_product_pins: 0`, `false` or `''` was read as no pin at all (`or []`), so a chain through the tree was BROKEN, and under `--project` that is a FINDING. It is now a tree that cannot answer: the link is declared-unverified, a WARNING, exit 0. - **An entry that is not text is no longer read as text.** `[openInk, 0]` and `[openInk, false]` dropped the entry as a blank, and `[openInk, true]` read it as the text `True`, so a chain through the list VERIFIED. Each is now declared-unverified, with a WARNING saying the list "holds an entry that is not text". That is stricter than the link tree's own `validate-manifest.py`, which takes `str(pin)` of an entry (`_declared_pins`, `templates/assembly-root/scripts/validate-manifest.py:85`): a chain is better left unverified than verified through a name nobody wrote. - **The scaffold no longer refuses `chain-not-declared` over a pins value that is a scalar.** With `--pin-source` a tree whose `neutral_product_pins:` is the scalar `openInk`, the scaffold used to refuse (`REFUSED chain-not-declared: ... openXdox declares I, e, k, n, o, p, not openInk`, exit 2, from the characters). It is now the declared-unverified WARNING and the scaffold goes on, exit 0, which is its documented rule for a tree that cannot answer: "a warning, never a refusal, because the check is offline". The warning says the tree was reached and could not be read (fix round 2, below). - **The reach wording is corrected** (SHOULD-2): "Who else this reaches" below, and the docstring of the test that made the claim. What stops crashing is the naming and manifest steps. `make validate`, `validate.yml` and `make bootstrap` still end in a traceback on the sibling row before Python 3.14, at a file this change does not touch (below). On 3.14 the sibling is passed over without a word there instead, which is #430. - **The mutation gaps** (nit). Six of the review's ten mutations survived. Five now die, each by a new test: a `project.yaml` that is a directory; a device or a FIFO as the source; the order A, B, A; a value holding `=` with no chain at all; and every failure of `SHAPE_PIN_SOURCE_<LINK>` for a link that holds a pin (a path that is not there, a directory with no manifest, a file that is no manifest), not only a permission. The sixth, the `typed in self.used` test in `unread_lines`, could not be killed because it was dead: a value is read as one link's keyed source or is the one bare PATH, so every lookup asks it of the same path and it is used or unread, never both. That was asserted over 293 tests and the test is removed. - **A chain no classification reads** (nit). `classify` consults a link's own declaration only for a name that claims a referent, with a recorded chain whose shape holds. For any other chain (one recorded on a spec leg; one on a name with no `open<Product>` to reach; one that does not begin at a held pin or does not end at a referent of the name) a source was still asked, so a path that was not there was reported as `link-source-unreadable` and a good tree was counted used, each for a question nobody put. `NamingPolicy.reads_link_declarations` now says when `resolve_referent` reads a chain, and `LinkSources.link_pins` asks it first: such a chain asks nothing, and a value for it is the NOTE that no name reads that link's declaration. A test runs `resolve_referent` with a mapping that records whether it was read, over every way a chain is and is not, and holds the new method to it. - **Dead code and stale sentences** (nit). `declared_neutral_pins` is removed: `git grep` over the tree, every pushed branch and every sibling worktree finds no caller. `link_manifest_lookup`'s docstring no longer says "replaces" over a name that is gone. This description no longer says `tests/test_naming_policy.py` takes `SEARCH_DENIED` from #312: it defines a mark of its own, with the same condition and its own reason text. **Fix round 2 (lane 5's re-review of 4b21263: 0 blockers, 0 should, 3 nits).** Third commit, 2ac911c. Each finding, in order: - **The three behaviours the body did not state are stated and tested** (nit 1). Round 1's not-a-list fix changed three behaviours on purpose, each listed under SHOULD-1 above, and the re-review found two of them had no test. R1 (`if declared is None` read as `if not declared`, which makes `0`, `false` and `''` declare nothing again) and R2 (`if pin is None or pin == ""` read as `if not pin`, which drops a `0` or `false` entry as a blank) both passed all 210 cases of `tests/test_naming_policy.py` at 4b21263. Six rows join `NOT_A_LIST_OF_NAMES`, which runs at the CLI and in process: `0`, `false` and `''` as the value, and `0`, `false` and `true` among the names. R1 now fails 9 cases and R2 fails 6. - **The scaffold's warning is true for a tree it reached** (nit 1, the third behaviour). Over a `--pin-source` whose `neutral_product_pins:` is not a list, round 1 turned the `chain-not-declared` refusal into a declared-unverified WARNING that says the tree "is not reachable here (a sibling checkout, SHAPE_PIN_SOURCE_<LINK>, or --link-source <LINK>=<path> would let it be read)". `--pin-source` did reach the tree, and only its declaration could not be read, so that blamed a path that was right. Now `link_pins_from_trees` takes a keyword-only `with_reasons`; asked, it returns a `LinkPins`, a dict equal to the plain mapping that also keeps `unreadable`, `{link: why}` for each link whose source answered and whose manifest could not be read. `_chain_link_findings` words such a link "which was reached but could not be read as a declaration (<why>)", in a branch of its own, and only the scaffold asks. The new text is asserted for each of the eleven not-a-list rows, with `scaffold-project.py --dry-run` run as a subprocess. - **Every other path's text is the bytes it was.** The sentence for a link no source reached is the same code in the same lines, and four tests hold it: the generic warning for a plain mapping, for an empty `LinkPins` and for an absent tree; the scaffold's own old warning for a clone that holds no `project.yaml`; the naming validator's two lines (its warning, and the `link-source-unreadable` line that names the reason); and `validate-manifest.py`'s. The naming validator and `validate-manifest.py` do not ask for reasons, so they print what they printed. - **One more scaffold path changes with it, on purpose.** A tree the scaffold reached and could not read as a declaration for any other reason (a manifest that does not parse, or is not a mapping) used to say "not reachable here" too, and was no truer. It now says what it is, with the system's reason, and a test for each of the two (a manifest that is not a mapping, one that does not parse) holds it, at the scaffold's command line and in process: narrowing the wording to the not-a-list reasons fails both. - `_declaration` still returns its four values, so nothing else in `scaffold-project.py` moved. - **A qualified chain link in the spy test** (nit 2). `_chain_read_cases` had no case whose chain link is qualified, so `reads_link_declarations` could take the chain as typed and not through `_chain_links` (R3) and pass every test. With `--referent-chain testorg/openXdox,openDox` that turns a verified chain into a declared-unverified one. `("codexDox", {"openXdox"}, ("testorg/openXdox", "openDox"), True)` is a case now, and R3 fails it. - **The description** (nit 3): the ten corrections of the re-review are applied (the reason a lookup returns, the fifth NOTE reason, the third kind of silent source, the SHOULD-1 bullet, "before Python 3.14", the `validate-pins.py` bullet, the 3.14.7 rows, the merge-tree section, the filed numbers, and the project's own `validate-manifest.py`). The mutation proofs: all three survivors were reproduced first at 4b21263 (210 passed each) and each dies at 2ac911c. Six mutations of the new code each die too: the warning ignoring the reasons (12 cases fail), the lookup recording none (12), the scaffold not asking (11), `with_reasons` on by default (3), an absent tree called reached (3), and the reached sentence used for a tree nothing reached (17). **Where it departs from #312, and why.** - **An explicit source that cannot answer stays a WARNING, exit 0.** #312 made an unreadable `--pin-source` a finding. Three reasons for the difference: - Exit 1 here means "a name is wrong", and this module has always said an unread link tree is a warning. - `SHAPE_PIN_SOURCE_<PRODUCT>` also answers `validate-pins.py`'s digest question, from a checkout that need not hold a manifest. - The lookup falls through to the next source, which may still verify the link. - **The fall-through is kept**, as it always was. With `--pin-source`, a keyed value is final. - **The lines go to stderr, spelled `WARNING`/`NOTE` as this file spells them.** #312's notes are ` note` lines on stdout, where that file's report lives. **Who else this reaches.** The guard is in `repo_shape.py`, so the same `link_pins_from_trees` lookup stops crashing for these callers too: - **The project's own `validate-manifest.py`.** It reaches the lookup through `SHAPE_PIN_SOURCE_<LINK>` or a sibling checkout, and now gives its usual declared-unverified WARNING instead of the traceback. - **`scaffold-project.py --referent-chain`.** A `SHAPE_PIN_SOURCE_<LINK>` under a locked parent no longer kills the plan, and `--pin-source` verifies the link. Neither of these prints the reason a source could not be entered; the scaffold says it only for a tree it reached and could not read (fix round 2, above). A `--pin-source` the scaffold cannot enter is still a traceback: that is #339 (`pin_digest_from_source`), and this PR does not touch it. **What this does not reach, and what the doctor says.** - **The project's gates still end in a traceback on the sibling row.** The NAMING and MANIFEST steps of `make validate`, `validate.yml` and `make bootstrap` stop crashing for a checkout `../openXdox` at mode 000. Those targets also run `validate-pins.py`, whose `resolve_neutral_pin_source` asks `(sibling / ".git").exists()` with no guard (`templates/assembly-root/scripts/validate-pins.py:463` on main), so before 3.14 a sibling at mode 000 still ends them in a `PermissionError`. That file is #312's, and `fix/pin-source-env-collisions` (PR #409, since landed), `fix/adopt-pin-writes-pin-file` and open PRs #343 and #419 touched it when this was written, so this PR does not edit it. The same line is listed for Python 3.14 by #430 (lane 2), where the helper that would cure both halves is asked for; the pre-3.14 traceback reproduction is added there as a comment, not filed apart. A `SHAPE_PIN_SOURCE_<LINK>` under a locked parent is different: `validate-pins.py` makes that a `neutral-pin-source-unreadable` FINDING (#312), and that stays. - **The doctor's `naming` row** runs the STANDARD's copy of the validator, so it stops crashing at once. **Its `manifest` row** runs the project's OWN `validate-manifest.py` on the project's own `repo_shape.py`, so it changes only once the project has applied this change. Over an existing project the doctor says `INVALID (manifest, pins)` until `update-shape.py apply` lands the two `upstream-changed` rows below. - **The verdict.** For `SHAPE_PIN_SOURCE_<LINK>` under a locked parent, on a project that carries these copies, `INVALID (naming, manifest, pins)` becomes `INVALID (pins)`, and `pins` is #312's finding for that locked variable, which is right. For a sibling at mode 000 the `pins` that remains is the traceback above, not a finding. The two copied scripts this change edits (`scripts/repo_shape.py` and `scripts/validate-repository-naming.py`) are copied into every project and pinned there. A project takes the change through `update-shape.py` (two `upstream-changed` rows), and a family holder takes the `repo_shape.py` row. Neither file has a second copy in this repository. **Evidence.** - **Issue rows, main ab623dd** (`repro338.sh`, a `gh` stub first on PATH). Under 3.10.21 and 3.12.3, rows 4-7 (keyed under a mode-000 parent, `SHAPE_PIN_SOURCE_OPENXDOX` there, a sibling at mode 000, a 300-character name) are a traceback, exit 1. Rows 2-3 (`a=b`, the typo) are a silent declared-unverified, exit 0. Under 3.14.7 row 1 verifies, and rows 2-7 are the silent declared-unverified WARNING, exit 0. - **Issue rows, this PR.** Under 3.10.21, 3.11.16, 3.12.3, 3.12.14, 3.13.15, 3.14.7 and 3.15.0rc2 (first commit), all 7 rows exit 0 with no traceback, and the output is byte-identical across versions. `a=b` verifies. The other five rows each name their source with its reason. Re-run on the fix-round head, 4b21263, under 3.10.21, 3.12.3 and 3.14.7: byte-identical to the first commit's run. Re-run on the fix-round-2 head, 2ac911c, under the same three: all 7 rows exit 0 with no traceback, the output is byte-identical across the three versions, and it is identical to the re-review's runs of the previous head over main. - **Reach on main.** Main gives a `PermissionError` traceback, exit 1, for `scaffold --dry-run`, the project's `validate-manifest.py`, `validate-repository-naming.py --project` and its `--quiet` form, each with `SHAPE_PIN_SOURCE_OPENXDOX` under a locked parent. This PR gives exit 0 for all four. `validate-pins.py` on the sibling row is the exception said above. - **New tests.** 17 cases (11 functions) in the first commit; 16 fail on ab623dd. The fix round adds 31 cases in `tests/test_naming_policy.py`, and 17 of them fail on 40762ca: the five shapes of pins that are not a list (each at the CLI, in-process and, for the scalar, under `--project`), and the chains nobody reads. The other 14 guard what a mutation could break (below) or the side the fix must not move (an absent, null or blank pin list still answers). - **Targeted run on 4b21263**, files `test_naming_policy`, `test_manifest_validator`, `test_scaffold_referent_chain_links`, `test_cli_reference`, `test_repo_hygiene`, `test_neutral_pin_validator`, `test_scaffold_e2e`, `test_shape_doctor`, `test_git_output_decoding` and `test_pin_validator`: **765 passed, 3 skipped**. The 3 skips are the PowerShell-only rows. (40762ca: 734 passed, 3 skipped.) `render-cli-reference.py --check` is ok. - **Mutation probes, first commit.** Reverting the stat guard, the parse, keyed-over-directory, the NOTE lines, the chain's-end restriction, the sibling-absence filter, the manifest reason, the `--quiet` drop, the "read as a PATH" sentence, the `./` line, the project-chain read, the WARNING lines or the env reporting each fails at least one new test. - **Targeted runs on 2ac911c (fix round 2).** Under 3.12.3, `test_naming_policy` (238 cases: 210 and 28 new), `test_cli_reference`, `test_manifest_validator`, `test_scaffold_referent_chain_links`, `test_repo_hygiene` and `test_setup_project_py`: **468 passed**; and `test_scaffold_e2e`, `test_scaffold_pin_and_reuse`, `test_scaffold_leg_path_collisions`, `test_scaffold_pin_collisions`, `test_neutral_pin_validator`, `test_git_output_decoding` and `test_pin_validator`: **312 passed**. Under 3.10.21 and under 3.14.7, `test_naming_policy`, `test_manifest_validator` and `test_scaffold_referent_chain_links`: **321 passed** each. `render-cli-reference.py --check` is ok, and `ast.parse(feature_version=(3, 9))` passes on the three changed files. - **Mutation probes, fix round.** The review's ten, re-run on 4b21263: MW1, MW2, MW3, MW5 and MW11 now die (they survived), and MW4, MW7, MW12 and MW13 still die; MW6 is the dead test, removed. (Fix round 2 kills the three the re-review found, R1 to R3, and six more: see above.) Nine more for the new code (the list check, the entry check, the null and blank tolerances, `reads_link_declarations` always true, without the shape check, without the held pins, and the validator ignoring it): eight die. The ninth, `reads_link_declarations` ignoring whether the name has a referent, survives as an equivalent mutant: with no referent `_chain_shape_problem`'s last question always reports a problem, so the explicit early return is redundant, and is kept because `resolve_referent` has the same first line. The spy test holds the two together. - **Windows.** The mode-000 rows skip on Windows and as root, using a `SEARCH_DENIED` mark this file defines, with the same condition as #312's. The new FIFO and `/dev/null` rows skip where those do not exist. `a=b` and the 300-character name run everywhere. On Windows the long-name row asserts the line, not POSIX's reason. None of it had been run on Windows or macOS when this was written; the last paragraph gives the CI result. **Merge-tree.** Recomputed at 19:04Z with `git merge-tree --write-tree` of 2ac911c against `origin/main` as it was then, 53bee5b, and against every one of the other 37 pushed branches that touches a file this PR changes (a scan of each head against its merge-base with main): - **Clean:** origin/main 53bee5b (it carries #314 and #390); #394 `fix/adopt-record-submodule-placement` 563ea16 (`scripts/repo_shape.py` and `validate-manifest.py`) and #406 `fix/validate-family-unnamed-mounts` 369af2e (`scripts/repo_shape.py`); #397 abb27e6, #400 7062a47, #401 409f10b, #416 7cca975, #452 e2df9a8 and #395 1ef3310 (`docs/cli.md`); #355 6fdbe3d, #373 741cbf2 and #395 (`scaffold-project.py`). - **Conflict, four hunks each, all in `scripts/repo_shape.py`:** `fix/pin-source-env-collisions` (PR #409) 6494714, and `fix/adopt-pin-writes-pin-file` 421e4e5 (no PR). Both also touch `scaffold-project.py`, and that merges clean with this PR's one edit there. Fix round 2's new hunks merge with neither: #409's change to the "not reachable here" sentence (the `offered` text, when a variable is shared) and the reached branch sit in different lines of `_chain_link_findings`. The four hunks, each taking both sides: 1. the `resolve_link_source` signature: `default_source=None, env_shared: bool = False, *, tried: list | None = None`; 2. its docstring: both paragraphs; 3. the `link_pins_from_trees` docstring and the lines after it: both paragraphs, then `found`, this PR's `unreadable` and #409's `links` and `shared`; 4. the call: `resolve_link_source(name, root, keyed_sources, default_source, pin_product_key(name) in shared, tried=asked)` (the same spelling on both branches), followed by `if tried is not None: tried[name] = asked`. Resolved that way in a throwaway and committed there, the merge with 6494714 passed **496 tests** (`test_naming_policy`, `test_pin_source_env_collisions`, `test_neutral_pin_validator`, `test_scaffold_referent_chain_links`, `test_manifest_validator`, `test_cli_reference`), and the merge with 421e4e5 passed **559 tests** (the same files and `test_pin_source_unreadable`). `render-cli-reference.py --check` is ok on both, pyflakes is clean, and the grammar parses as 3.9's. Neither branch carried the resolution when this was measured. #409 has since landed, and the merge of main into this branch (the last paragraph) takes those four hunks both ways. The earlier counts (468 and 434 from the first fix round, 430 from the re-review) are those runs' own, at their heads. **Left out.** Each of these was found by lane 5's reviews of this change, and each is filed or folded: - **A bare `--link-source` answers for every link with no more specific source** (#441). In a chain of three or more links, one bare tree is read as two links' declarations, and a chain that holds is reported broken, a FINDING for a name with no direct pin on its referent. Pre-existing, the same on main; `validate-pins.py`'s `--pin-source` has the same documented rule and was not examined. - **An empty path after a key (`--link-source openXdox=`) is `Path("")`, the current directory** (#442). Run from a project root, the project's own `project.yaml` is read as the link's declaration. Pre-existing; `validate-pins.py`'s `--pin-source openXdox=` has the same shape (there a finding naming `.`). - **`validate-pins.py`'s `(sibling / ".git").exists()` is unguarded** (the sibling row above), folded into lane 2's #430, which already covers the Python 3.14 half: the pre-3.14 traceback and its reproduction are added there as a comment. #312's file. - **The project's OWN manifest** with `neutral_product_pins: 5` ends in a `TypeError` in two gates before either can say anything about it (#443): `_pins_from_project` in the naming gate (`scripts/validate-repository-naming.py:302` on main), and `_declared_pins` in the project's own `validate-manifest.py` (`templates/assembly-root/scripts/validate-manifest.py:85`), which ends it before its own `manifest-neutral-product-pins` finding (`_neutral_product_pins_type_finding`, `:524` on main) can print. Reproduced on 3ef059c. That is the project's own declaration, read by the project's own gates, and not a link source, so it is outside this change; "read by the project's own gates" covers two crashing gates. Also not done here: - **Python 3.9 (the floor) was not run** because it is not installed here. The grammar of the three changed files was checked with `ast.parse(feature_version=(3, 9))`. Windows and macOS were not run here; CI ran them (the last paragraph). Reviewed adversarially at 40762ca, the branch's first commit (CHANGES NEEDED: 0 blockers, 2 should-fixes, 4 nits). The head stopped every crash in the issue's table, and the should-fixes were a docstring claim the code did not keep for a `neutral_product_pins:` that is not a list, and reach claims that overstated what the project's gate does on the sibling row. Fix round 1 (4b21263) resolved every finding, and the re-review of 4b21263 read APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits: three behaviour changes this description had not stated, two of them untested; a spy-test case for a qualified chain link; and description corrections). Fix round 2 (2ac911c) resolved those three, and its re-verification of 2ac911c read APPROVE WITH NITS (0 blockers, 0 should-fixes, 2 nits: no test of the scaffold's wider wording for a manifest that is not a mapping or does not parse, now pinned by two tests that each fail when the wording is narrowed, and a cross-reference in this description that pointed the wrong way, now corrected). Main (9736424, #409's squash) was merged into the branch as ced8fac. `scripts/repo_shape.py` conflicted in four hunks; each takes both sides, as the review of 2ac911c resolved it: `resolve_link_source(..., default_source=None, env_shared: bool = False, *, tried=None)`, both docstrings kept, `link_pins_from_trees` opening with `found`, `unreadable`, `links` and `shared`, and the call passing `pin_product_key(name) in shared` with `tried=asked`. Nothing else conflicted. After the merge, `tests/test_pin_source_env_collisions.py` and the targeted set passed (667 and 312), and `render-cli-reference.py --check` printed `[ok]`. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 938a131 (main 48585e6 merged in). Its merge with main 994be39 passed the targeted set (the naming, manifest, scaffold, pin and setup test files): 983 passed. Closes #338 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
…rge` FINDING naming the paths, with `git -C <root> status` as its NEXT, and the verdict `INVALID (merge)`, exit 1, on a rung above every other but CANNOT ANSWER, so a merge `git commit` refuses no longer reads COMPLIANT, exit 0, once `checkout --ours` takes the markers out, or DRIFTED about a leg that never moved (#424) (#468) Nothing in the doctor asked git whether the root's index held an unmerged path. The validators read the working tree, and `recorded_gitlink` answers a gitlink a conflicted merge holds only at stages 1, 2 and 3 from HEAD. So the merge of two `bump-leg.py` branches read `DRIFTED (1 leg(s) not at the pin)` while the conflict markers sat in the pin, and COMPLIANT, exit 0, once `git checkout --ours` had taken them out, with `UU` still on the pin and the gitlink and `git commit` refusing. A conflicted file no row reads was COMPLIANT from the start, and so was a cherry-pick left conflicted. - `check_merge`, registered first in `CHECKS` for both kinds of root, asks `git --no-optional-locks ls-files -u -z` for the unmerged paths, read as bytes and kept once each. Any at all: a FINDING naming five, then a count, with every one in `--json` under `detail.unmerged`. A name holding a control character or a byte that is not UTF-8 is spelled by `repr`, as the placement row spells one, so the table stays one line per row. - Its NEXT is `git -C <root> status`, the bare command (#277). The words on finishing (`git add` each path, then `git commit`, or `git merge --abort`) are in the reason, and only where a `MERGE_HEAD` file exists. A cherry-pick or rebase conflict gets the same FINDING, and its reason points to `git status` for how to finish or abandon it. - A `MERGE_HEAD` with nothing unmerged is a `note` with no NEXT. Git commits it as it stands and nothing asserts a fault, so the verdict steps over it and COMPLIANT stays exit 0. `MERGE_HEAD` is read as the file `rev-parse --git-path` names, so a branch of that name is not a merge. - With neither, the check returns no row, the way the Triad row is silent where its rule says so. Where git gives no answer there is no row either. - `merge_verdict` is its own rung in `verdict_for`, after CANNOT ANSWER and before DRIFTED. The conflict is what turns the other rows red, as an edited copy is what turns `pins` red under DRIFTED. It needs a non-empty list, as `misplaced_verdict` does, so a merge check that raised falls through to the catch-all. - Not CANNOT ANSWER, because exit 3 is this checkout failing to ask and an unfinished merge is a fact about their repository. No other row is made to wait on it: they still say what is wrong with a file the merge left. Unchanged: the report of a root with no merge in progress. For the clean bump branch, the committed merge and the aborted merge, the text report is byte-identical to main's once the run directory and the standard's commit are normalized. A merge left unfinished inside a leg is not read, because the root records the leg's commit and never its index. AGENTS.md and docs/cli.md are unchanged; `--help` is the docstring's first line. README.md and docs/handbook.html do change: the row list names the `merge` row, the verdict table says `INVALID (<rows>)` also covers an unmerged path, and the paragraph on what outranks what says an unfinished merge outranks all three. The handbook footer's `README.md blob` follows README.md. Left out: the module docstring's EXIT CODES list, which #394 also edits, does not name the merge; the new paragraph above it does. Round two, from lane openRepoShape-5's accuracy read: the row lists in README.md, docs/handbook.html and the module docstring were each a row short. The registry has `Check("agent-files", ..., (PROJECT, FAMILY), ...)` and `Check("machine", ..., (PROJECT, FAMILY, NOT_A_ROOT), ...)`, so a FAMILY holder also gets `agent files`, and a directory that is neither also gets `machine`; the three places now say so (the docstring already named `machine` for the plain directory). And the `MISPLACED (n paths)` verdict row in README.md and docs/handbook.html now says a submodule in a leg other than the one its `submodules:` record names counts too, as `recorded_placement` and `leg_submodules` have counted it since #394. README.md stays at 1422 lines: the one line the longer list needs is absorbed inside the row-list paragraph, whose last seven lines are wrapped at up to 84 columns, no word changed. Lane: openreposhape-3 (openRepoShape-3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
… 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>
brettheap
added a commit
that referenced
this pull request
Oct 13, 2026
… false are corrected to what the tools do on main (#483) (#505) **Closes #483.** #483 is the v1.0.0 "README and AGENTS.md accurate" criterion. Three read-only audits (AGENTS.md; README.md lines 1-727; README.md lines 711-end) checked every statement that names a command, a flag, an exit code, a refusal, a file or a step against the code at `ec4a7a0e` and ran the tools offline (local bare remotes, a stub `gh`, a blank git identity). They found 38 false or incomplete statements. This pull request corrects 28 of them (27 whole, and D2-F1 in part), plus two the independent review found (the `MISPLACED` text after #394, and `openRepoShape --preflight` "offers to install" git-filter-repo): 29 rows in all, D1-F9 and D3-F7 sharing one. The other ten, and the rest of D2-F1, were settled by pull requests that landed while this one was open: #484 and #465 settle six (D1-F8, D2-F7, D3-F6; D2-F13, D2-F14, D3-F8), and #491 and #468, to which this branch had routed them, settle four (D1-F1, D2-F5, D1-F2; D3-F11) and the rest of D2-F1. The two routed to #490 (D1-F9, D3-F7) were still false once it landed, and are corrected here. A last read of every line main has added to README.md and AGENTS.md since `ec4a7a0e` found nothing else false, so nothing is left open under #483. Two independent reviews, six rounds. The first head (`bfbd4eeb`) came back CHANGES NEEDED: two corrections that were themselves untrue (exit 3; "each of those tools has its own section") and nine gaps. The second (`ccf520bd`, on a merge of `7a8b981d`, where #394 landed) fixed them and came back with no blocker and three landing changes, which the third commit (`f35e59fa`, on a merge of `5a253877`, where #484 landed) makes: D2-F5 and D1-F1's block go back to #491 (at `2d59d141` its `setup-project.py` reads the yes from the controlling terminal, so the piped one-liner asks once it lands, and its new test strips `<...>` from README.md, which a `bash <(` line breaks); D3-F5 is fixed here now that #484 has landed; and the routes are re-derived at the current heads. The same commit leaves AGENTS.md's family steps 3 and 5 as main has them, so #486-#488 (`10c93ded`) still merges clean. The fourth round merges `48585e62`, where #465 landed (README.md at 699-709, 1028 and 1095 and the handbook beside them, none of which this branch edits), and re-reads the three #465 rows on main. The fifth merges `57c85e92`, where #468 landed (the doctor's `merge` row), as `fb6a9ab4`: the doctor's verdict table, in README.md and in the handbook, takes this branch's `COMPLIANT`, `SHAPE BEHIND` and `DRIFTED` rows and main's `MISPLACED` and `INVALID` rows, and the handbook footer keeps both clauses (#424, then #483) with its blob recomputed. `aa4e1f59` (#508) then merged clean, as `83cfad54`. The sixth merges `70d0d1b3`, where #491 landed (setup asks at the person's own terminal), as `94793aeb`: README.md's "What Dana sees" paragraph takes #491's text whole with this branch's D2-F6 ("a hyphenated name"), and the handbook footer keeps #476's and #492's clauses ahead of #483's, with its blob recomputed. The same round re-reads every routed row on main, corrects D1-F9 and D3-F7 (`ea634043`), and reads what main changed in both files since `ec4a7a0e`. The first reviewer's round four came back APPROVE WITH NITS (0/0/3) and asked for one word, made at `9ac90d16`: "a hyphenated project name", since the leg names are hyphenated and pass. **What.** `README.md` and `AGENTS.md` only, plus `docs/handbook.html` where it repeats a corrected sentence, with its footer's `README.md blob` recomputed (`git hash-object README.md` = `1417b9ec0235675da8f73ca023756411bc3fbe8a`). No tool, test, template or `docs/cli.md` changes. `README.md` hashes identically at `ec4a7a0e`, `437a360a`, `d222a124` and `7a8b981d`, so its audit line numbers held; `AGENTS.md` changed only by #394 (it adds 5 lines after line 154 and 3 more after the doctor's item 4); #484, #465 and #491 since edited both files, and #468 README.md, without changing either total. Each fix is the smallest true sentence, keeps the surrounding voice, and states only what the code or a run showed. **The line caps.** `tests/test_repo_hygiene.py` holds `AGENTS.md` to 490 lines (#394 raised it from 482) and `README.md` to 1422, and both files stand exactly at their caps, so every correction is line-neutral. To give back the lines the longer sentences need, paragraphs that no correction touches are re-wrapped to 80 columns with no word changed: - README.md, eight paragraphs no correction touches: macOS (`and refuses anything below 3.9`), `2. Login.`, the InkRouter ruling quote, `Each leg is pinned TWICE`, the neutral-product election paragraph (`Ruled by Brett Heap on 2026-09-05`), `Its entries are adopt-project.py plan's entries`, `This repository implements neither`, `A FAMILY holder carries both verbs too`; and the tail of the `--link-source` paragraph that D2-F12 edits. None is in README.md's family section (903-925 on main), which #486-#488 edits. - AGENTS.md, four paragraphs no correction touches: the opening paragraph, the NO-code seeding paragraph, doctor item 1 and the park overlay item; and the first half of the doctor intro, whose second half D1-F7 edits. The family steps are main's exact bytes: an earlier head re-wrapped steps 3 and 5 (`make siblings`) and #486-#488 edits both. - Three sentences this pass corrects give up a few words to keep a line: D2-F1's "offers to run" (no "for you"), D2-F11's "asks the same of one name", and AGENTS.md's `check` verdict list and doctor next-command item (D3-F9, D1-F7). Word-level diffs of both files (`rvD-worddiff.py`) show only the edits in the tables below, `rvD-hunks.py` marks each re-wrapped hunk `WHITESPACE-ONLY`, and `rvD-mdstruct.py` finds the Markdown block structure, links and HTML identical (AGENTS.md 320/320 blocks, README.md 678/678). "Where" is the audited `ec4a7a0e` for README.md (it hashes the same up to #394, and #484 and #465 changed its text and not its line count, so those numbers still point at the same places on main); AGENTS.md lines after 154 read 5 higher on main, and 8 higher after the doctor's item 4, since #394. "Evidence" is code at `7a8b981d` (the line numbers below are that file's, re-derived at the merged head `48585e62` for every tool file that #394, #484, #465 and the later landings moved), and "run" is the audit's or this pass's offline run. | finding | where | was | now | evidence | |---|---|---|---|---| | D1-F3 | AGENTS.md:121 | "the preflight prints the install command" for git-filter-repo | "`execute` refuses without it, printing the install command" | adopt-project.py:4148-4158 (`_require_filter_repo`, `filter-repo-missing` + `FILTER_REPO_HINT` at 261-267), only call at 5652; `--preflight` checks git, Python, bootstrap, gh, helper, autocrlf only (setup-project.py:1110-1134); openRepoShape:212-216 | | D1-F4 | AGENTS.md:176-178 | the row reads `code: 0 of N source paths (seeded from template)` | the table's `code` row reads `0 of N source paths (seeded from template)` | adopt-project.py:6107-6108 prints `f" {leg:<6} {counts[leg]:>5} of {len(before)} source " f"paths{note}"`, no colon; run: ` code 0 of 2 source paths (seeded from template)` | | D1-F5 | AGENTS.md:182 | "`MedxGlass` descends from `openGlass` only if it PINS it (2026-09-02)" | "...or pins a layer that does and records the chain (`--referent-chain`; 2026-09-02, chains 2026-09-05)" | scaffold-project.py:379-391; run: `--pin openXglass@<sha> --referent-chain openXglass,openGlass --dry-run` printed "classifies as domain-descendant / assembly" | | D1-F6 | AGENTS.md:205-210, 258-259 | `openRepoShape --doctor` and `./shape-doctor.py` presented as one tool; "`--json` is the same report" | installed, it runs from a temporary clone deleted on exit and takes a path and nothing else; to act on a row (`--json`, `--placement-plan`, its next command) run `./shape-doctor.py --root <path>`; item 7 names `./shape-doctor.py --json` | openRepoShape:112-116 (EXIT trap), 329-352 (`shape_checkout` into `$WORKDIR/shape`), 379-392 (`run_doctor` clones when no checkout is beside it), 463-471 (`--doctor takes a path and nothing else`); run: the installed copy printed `NEXT python3 <tmp>/shape/adopt-project.py plan ...` and the directory was gone after exit; `--doctor <root> --json` printed `REFUSED: --doctor takes a path and nothing else`, exit 2 | | D1-F7 | AGENTS.md:226-232 | next commands are "`update-shape.py apply …`, `make bootstrap`, `bump-leg.py …`"; every one "carries the exact next command"; "each of those commands has its own section above" | "Every one carries the exact next command, where one can be named — `update-shape.py check …` and its `apply`, `git submodule update …` (in a holder, its `scripts/bootstrap.py`), the project's own validator"; "Each of those commands keeps its own refusals" | shape-doctor.py:2813 and 2828 (check NEXT), 3075-3089 (`legs` NEXT is `git submodule update`, `make bootstrap` only in the trailing comment), 5044 (holder `members` NEXT); no row's NEXT is a `bump-leg.py` command (`grep -c -i 'bump.leg\|bump_leg' shape-doctor.py` was 0 at `7a8b981d`; since #495 the `pins` row's reason names two `bump-leg.py --to` lines, as text, at shape-doctor.py:1847-1856 and 1904-1922 of `ea634043`); rows with no next command: 1393, 1526-1532, 5758 (`could not be run: {exc}`); only update-shape has a section in AGENTS.md | | D1-F9, D3-F7 | AGENTS.md:344-346, README.md:994-995, and the handbook beside it | after a bump the leg is left detached, and "`make bootstrap` in the root re-places it on its tracking branch AT the new pin; say so in your relay" (README.md: "`make bootstrap` re-places it on its tracking branch AT the new pin.") | AGENTS.md: "`make bootstrap` in the root puts it on its tracking branch AT the new pin, or says why not; relay which."; README.md and the handbook: "...re-places it on its tracking branch AT the new pin, or says why not." | templates/assembly-root/scripts/bootstrap.py at `ea634043`, `checkout_tracking_branch` (393-448): a branch behind the pin is fast-forwarded (275-340), and one ahead of it with no commit of its own is moved back (343-390, 177-187), both since #490 (`7348c57b`); a branch with commits of its own, or one that diverged, is not moved, and bootstrap prints `not moving an existing branch` (444-448); a commit made on the detached leg is left where it is, said as "which is neither" (313-322); a branch another worktree has checked out is not moved, and git's refusal is printed (226-228, 238-243) | | D2-F1 (part) | README.md:175-178, 217-220 | step 4's preflight "prints the command for THIS machine and asks once"; git commands are "the same commands that preflight offers to run for you" | "for `gh`, its login and its credential helper"; "the same commands `setup-project.py --preflight` offers to run" | setup.sh:85 (`command -v git ... \|\| die`, exit 2 by setup.sh:40) and :76; setup-project.py:1829-1831 (`bad(GIT_MISSING); die(PREREQ_MISSING)` ahead of `preflight(opts)`); the git offer (setup-project.py:926-932, rows 787-796) is reached only by `setup-project.py --preflight` (1820-1821); the `gh`, login and `gh auth setup-git` offers at 982-1078; run, no git on PATH: `openRepoShape --preflight` printed `REFUSED: git is not installed`, exit 2, no offer. The third place (README.md:434-436) is routed to #491 | | D2-F2 | README.md:181-183 | `openRepoShape --preflight` "exits 0 when everything is present and 1 when it is not" | "(`openRepoShape --preflight` exits 2 when `git` or a Python 3.9 or newer is missing)" | openRepoShape:446, 564 run setup.sh; setup.sh:76, 85 `die` (exit 2, setup.sh:40) before `preflight_only` (setup-project.py:1142-1166) | | D2-F3 | README.md:269-271 | "add `--visibility private` if you want that" | "`--visibility` defaults to `private`: add `--visibility public` (or `internal`) if you want that" | setup-project.py:428 (`self.visibility = "private"`), usage :138; scaffold-project.py:310-316; run: no `--visibility` printed `[ok] visibility private` | | D2-F4 | README.md:306-308 | the rehearsal leaves behind `./Atlas` and three bare repositories | "...and the scaffold's three working trees, in the temporary directory its `Working trees are at` line names" | scaffold-project.py:1570-1571 (`tempfile.mkdtemp(prefix="openreposhape-")`, never removed), 1611; run: `/tmp/openreposhape-q5eeici3` held `atlas atlas-code atlas-spec` | | D2-F6 | README.md:436-438 | "a hyphenated or lowercase name is refused here" | "a hyphenated project name is refused here" (the review's N1: `Atlas-spec` and `Atlas-code` are hyphenated and pass) | contracts/repository-naming.yaml:335-345 (`^[A-Za-z][A-Za-z0-9]*(?:-spec\|-code)?$`); run: `validate-repository-naming.py --explain atlas atlas-spec atlas-code` exit 0, `At-las` exit 1 `naming-unclassified`; the rehearsal for `atlas` ended `DONE. atlas is scaffolded and bootstrapped.` | | D2-F8 | README.md:573-574 | every other family "is CamelCase" | "from the neutral-product, domain-descendant and family forms, all of which are CamelCase words" | contracts/repository-naming.yaml:412 (`^[a-z0-9]+(?:-[a-z0-9]+)*-wip$`), 182 (`-Install` is hyphenated) | | D2-F9 | README.md:619-620 | the rewrite covers "both HTTPS and SSH leg URLs" | "both HTTPS and scp-form SSH (`git@github.com:`) leg URLs" | templates/assembly-root/.github/workflows/validate.yml:160-161 (two `insteadOf` rewrites) and :69-73 (`*) continue` skips any other spelling); run: `ssh://git@github.com/...` went to ssh unrewritten | | D2-F10 | README.md:625-628 | a leg under another owner "can only be reached via `SHAPE_LEGS_TOKEN`" | "`SHAPE_LEGS_TOKEN` is read only when the App is not configured, so that leg is reachable only with the App secrets removed" | validate.yml:146-154 (`if [ -n "$SHAPE_LEGS_APP_TOKEN" ] ... elif [ -n "$SHAPE_LEGS_TOKEN" ]`); run: both set printed `leg credential source: app` | | D2-F11 | README.md:680-682 | `validate-repository-naming.py --referent-chain openXdox,openDox` asks the same question for one name | `... --pins openXdox --referent-chain openXdox,openDox ...` asks "the same of one name" | scripts/validate-repository-naming.py:522-528 ("The first entry must be in `--pins`"); run: without `--pins`, `codexDox` classified `project-leg/assembly` (exit 0); with it, `domain-descendant` | | D2-F12 | README.md:687-689 | `--link-source [PRODUCT=]<path>`, env, or a checkout beside the root | `--link-source PRODUCT=<path>`, env, a checkout beside the root, then a bare `--link-source <path>` | scripts/repo_shape.py:2589-2621 (`resolve_link_source`, bare path 4th); templates/assembly-root/scripts/validate-pins.py:597-632 uses the same order | | D3-F1 | README.md:736 | "the only change to it is ONE split commit" | "the only change to its history is ONE split commit" | adopt-project.py:5355-5376 (`_set_the_topic` runs `gh repo edit <source> --add-topic` on all three), called at 5734; the topic is not history | | D3-F2 | README.md:764-765 | `plan` "writes it into `follow_ups:`" for every such file | "lists up to eight per directory in `follow_ups:`, counting the rest" | adopt-project.py:1329-1337 (`hits[:8]`, then `… and N more file(s) under the code leg naming ...`) | | D3-F3 | README.md:810-811 | the row reads `code: 0 of N source paths (seeded from template)` | the table's `code` row reads `0 of N source paths (seeded from template)` | as D1-F4; run: ` code 0 of 3 source paths (seeded from template)` | | D3-F4 | README.md:958-960 | `sorted-ls-tree-r-v1` records "joined with LF" | "each followed by LF, the last one included" | scripts/repo_shape.py:904-928 (`digest.update(record); digest.update(b"\n")`); run over `HEAD:templates/code-root`: LF-joined gave `f44766c9...`, LF-terminated and `tree_digest()` both `fca315e6...` | | D3-F5 | README.md:976 | `--dry-run` "prints the move and writes nothing", and (with #484) "`--dry-run` writes nothing, so it runs there" | "# --dry-run prints the move; it fetches into the leg and writes nothing else"; "`--dry-run` fetches into the leg and writes nothing else, so it runs there and prints that command" | scripts/bump-leg.py at `48585e62`: `fetch_leg` is called at :1453 (refspec `+refs/heads/*:refs/remotes/origin/*`, :854) and the `--dry-run` exit is at :1473, so the fetch comes first; the tool says so itself ("--dry-run: nothing was changed. The leg's object store was fetched into, which is how the digest above was computed", :1474-1477; docstring :447). Run (audit, offline): before the dry run the target object was absent from the leg and `origin/main` was f85673e; after it the object was present and `origin/main` was b9bce3e. #484 (landed) lets `--dry-run` run on the tracking branch (:1030 `and not dry_run`) and print `git switch -c`; it kept "writes nothing" at README.md:975 and in the refusal sentence, and the handbook (code comment and paragraph) says the same | | D3-F9 | README.md:1041-1044, AGENTS.md:285-288 | `check` prints `unchanged`, `upstream-changed`, `locally-modified` or `both` | "also `already-at-target` (the root has the target's bytes) and `upstream-removed`, `unmapped` and `copy-missing`; `apply` refuses the last three" (README; AGENTS.md appends the same four after `both`: `already-at-target`, `upstream-removed`, `unmapped` and `copy-missing`) | update-shape.py:239-256 (states, `CONFLICTS`, `ORDER`), 541-642 (`_classify_row`, `_row_verdict`) | | D3-F10 | README.md:1059-1063 | a `both` file "is refused outright" | "refused until a human merges it, commits the merge and re-runs with `--accept-local <path>`" | update-shape.py:2515-2535 (`_refuse_conflicts` excludes an accepted `both`), :1582 (`check` says "Merge it by hand, commit it, then `apply --accept-local <path>`"); run: `apply ... --accept-local Makefile` after the hand merge committed, exit 0 | | D3-F12 | README.md:1137-1139 | "every one that is not `ok` names the exact command that fixes it" | "a row that is not `ok` names the exact command that fixes it whenever there is one to run" | shape-doctor.py:1429, 3026, 5337 build `Row(..., None, ...)` with no fix; run: `--json` on a plain directory gave `contents n/a` with no next | | D3-F13 | README.md:1151-1153 | `COMPLIANT`: "every row ok"; one `SHAPE BEHIND` spelling; `DRIFTED`: "a copy was edited here, or a leg is off its pin" | "no row is a `FINDING`"; adds `(pin <a> -> <b>, no copied file differs)`; "edited, deleted or no longer maps onto the standard, or a leg (a member, in a holder) is off its pin" | shape-doctor.py:5942 (`COMPLIANT` is the end of the ladder, `n/a machine` is always there), 5875-5899 (`behind_verdict`), 5775-5795 (`drifted_file_reasons`), 5796-5824 (`drifted_reasons`) | | D3-F14 | README.md:1158 | "Exit 3 is usage or environment" | "Exit 3 is usage or environment (a flag argparse rejects is exit 2) — nothing about it is a statement about the tree you pointed at: no such path, a refused `--placement-plan`, a `shape-doctor.py` not sitting in a checkout of the standard, or a pin naming a commit **this** checkout does not carry (`CANNOT ANSWER`)" | shape-doctor.py:6029-6051 (plain argparse, exit 2), 6061-6082 (the exit-3 paths, among them `except Refusal: return 3` for the flag), 4547-4553 (`shape-doctor-no-placement-audit`), docstring :227 (`3 usage or environment`); tests/test_shape_doctor.py:5844-5858 asserts `returncode == 3`; runs: `--bogus` rc 2, `--root <missing>` rc 3, `--root <plain dir> --placement-plan pp.yaml` rc 3 | | D3-F15 | README.md:1273-1278 | bare `park` "PARKS EVERY ESTATE it finds" | "lists every estate ... and ASKS whether to park them all (with no terminal on stdin it refuses and names `--all`); `park --all`, or `-a`, parks them all unasked ... skipping a standalone root with no Speckit overlay (Brett Heap's RULINGs of 2026-09-10, the later superseding #91's unasked sweep)"; `resume` has no `--all` | openRepoTools `park` at GitHub's main `f87fd5a` (the review fetched it: nothing newer): header 17-41, usage 77-100, sweep skip at 802-808, `resume` refuses `--all` at 863-867; runs: bare `park </dev/null` outside an estate gave `REFUSED ... park --all parks every estate below without asking`, rc 2; `park --all --dry-run` gave `skipped Solo (no Speckit overlay)`, rc 0 | | MISPLACED (new, #394) | README.md:1176-1180 | `placement` "names the paths the policy would have put somewhere else" | "...and a submodule in a leg other than the one its `submodules:` record names" | since #394 (`7a8b981d`) a submodule in a leg other than the one `project.yaml`'s `submodules:` records for it is `misplaced`, counted in `MISPLACED`, exit 1: shape-doctor.py:3604-3624 (`recorded_placement`), 3754-3769 (`leg_submodules`); tests/test_shape_doctor.py:6770 (`test_a_submodule_in_the_other_leg_from_its_record_is_misplaced`), 7219. AGENTS.md already says it (#394's item 4). The verdict table's `MISPLACED` row sits directly above #468's `INVALID` row, so it is not edited here: #468 is asked for it | | N7 | AGENTS.md:259-262 (item 6) | "`openRepoShape --preflight` is what offers to install one" | "`openRepoShape --preflight` offers only the `gh` set-up" | the preflight never checks git-filter-repo (setup-project.py:1110-1135; openRepoShape:212-216) and setup.sh:85 exits 2 for a missing git before the preflight; the offers are `gh`, its login and its credential helper (982-1078) | `docs/handbook.html` repeats these and is corrected where it does: the preflight offers, `git` and the credential helper (four platform panels), the `openRepoShape --preflight` exit codes, the visibility default, the rehearsal leftovers, the CamelCase sentence, the credential rewrite and `SHAPE_LEGS_TOKEN`, the source repository's one change, the digest definition, `check`'s verdicts and the `both` refusal, the doctor's row sentence, verdict table and exit 3, the placement paragraph's submodule, and bare `park`. The handbook says more than the README in four places (the `git`-before-the-preflight sentences in the Linux panel, the Windows git offer "only under `py setup-project.py --preflight`", glosses for the three non-comparable `check` states, park "from anywhere"); all are true. Its footer's history prose gains "added (#483)". | finding | where | evidence at `71359744`, the merge of `48585e62` | |---|---|---| | D1-F8 | AGENTS.md:323-328, 337-339 | the block carries `git -C <path> switch -c <branch>` (AGENTS.md:335), item 3 says "A new project stands on its tracking branch; the dry run runs there and prints the `git switch -c`, but the real run is refused" (:348-349); scripts/bump-leg.py:1030 refuses on the tracking branch only `and not dry_run`; tests/test_bump_leg_dry_run_tracking.py | | D2-F7 | README.md:502-504 | README.md:502 `git -C ../Atlas switch -c <branch> # the tool will not commit on main` | | D3-F6 | README.md:974-977, 987-992 | README.md:988-991 "`git switch -c <branch>` first; `--dry-run` ... runs there and prints that command" | | D2-F13 | README.md:705-707 | #465 rewrote the sentence: README.md:705-707 now reads "Finding neither is a named SKIP, exit 0: no checkout and no network does not make a descent a lie. A checkout that cannot answer is the finding `neutral-pin-source-unreadable`", and the handbook says the same (1139-1141). True: a source that is found but cannot answer is a FINDING, templates/assembly-root/scripts/validate-pins.py:796; the SKIP is only "no source found and `gh api` fails" | | D2-F14 | README.md:708-709 | #465 deleted the whole sentence, so "a pin file the declaration names but that is not there, is reported as drift" is gone from README.md and the handbook (the missing file is the refusal `neutral-pin-missing`, validate-pins.py:699). It also dropped the true half, "A digest that recomputes differently is reported as drift" (the finding `neutral-pin-digest-mismatch`, validate-pins.py:853): not a false statement; see "Not holding #483 open" below | | D3-F8 | README.md:1028, 1095-1097 | #465 rewrote both: README.md:1027-1029 "a project runs its own gate, which never consults this standard (its validators' one possible network read is `validate-pins.py`'s `gh api` recheck of a neutral product's pin, above)" and :1095-1098 "That line adds no network call ... (Its pins gate may ask `gh api`; see above.)"; the handbook the same (1403, 1448). True: `validate-pins.py` falls back to `gh api` when no local checkout answers (scripts/repo_shape.py:931 `tree_digest_from_gh`) | Evidence is at `ea634043`, the merge of `70d0d1b3` with the D1-F9/D3-F7 correction; `9ac90d16` changes one README word in place and the footer blob, so the line numbers hold at this head. The runs are this round's, offline: local bare remotes, a stub `gh`, a blank identity. | finding | where | evidence | |---|---|---| | D1-F1 | AGENTS.md:22-24, 56-62, 78-80 | #491 reads the yes from the controlling terminal (setup-project.py:1322, `ask()` at 1325-1432; the confirmation at 1770-1779), and setup.sh hands the child the pipe (282-283). Run under a pty: the piped `cat setup.sh \| bash -s -- … --local-remote-dir …` asked `Type yes to continue:`, and `no` gave `REFUSED: not confirmed; nothing was created.`, exit 1. With no terminal at all (AGENTS.md:23, 83-86) it prints the plan and refuses (1770-1776): `REFUSED: this will create THREE repositories and there is no terminal to confirm on…`, exit 2 | | D2-F5 | README.md:315-336, 414-420 | the same read, for the README's three one-liner blocks | | D1-F2 | AGENTS.md:76-77 | #491 took the audit's text (AGENTS.md:73-75). `_choose_org` (setup-project.py:1244-1258) takes a fork's `origin` organisation (run: `[ok] organisation acmefork (from the \`origin\` remote…)`) and refuses opensoft's own without `--org` (run: `REFUSED: you are running from the upstream checkout (…); pass --org <your-org>.`, exit 2) | | D2-F1 (rest) | README.md:434-436 | #491's words, kept whole in the merge (README.md:432-445). The `gh` offers are the install (setup-project.py:992-1005, `INSTALL_OFFERS` at 795-798), the login (1009, 1015-1032) and git's credential helper (1051-1083, asked only once `gh` is ready, 1134-1137); git and Python are refused before any offer (setup.sh:76 and 85, exit 2 by :40). Run with no git on PATH: `openRepoShape --preflight` printed `REFUSED: git is not installed. Install it: https://git-scm.com/downloads`, exit 2, and offered nothing | | D3-F11 | README.md:1130-1133 | #468's sentence (README.md:1130-1134) names `agent files` for a holder and `machine` for a plain directory, as the doctor's `CHECKS` registry has them (shape-doctor.py:6115-6155, at 6135 and 6153). Run, `--json` on a plain directory: the rows naming, contents, way-in, machine | | `MISPLACED` table row (the review's) | README.md:1151, and the handbook's verdict table | #468's row reads "...or a submodule in a leg other than the one its `submodules:` record names" (README.md:1155, handbook:1509); shape-doctor.py:3843-3862, 3993-4008 | Every finding of the three audits is corrected above or settled by a pull request that has landed. A fourth read took what main changed in README.md and AGENTS.md since the audits' `ec4a7a0e` (#394, #484, #465, #468, #491: `git diff -U0 ec4a7a0 70d0d1b`), every line this branch does not itself rewrite, and asked the audits' question by their method. It found one false statement, D1-F9/D3-F7's own sentence as #484 kept it, corrected above, and checked the rest true: AGENTS.md 23, 63-67, 73-81, 83-86, 88-96, 155-158, 252-254, 335, 347-349 and 357-361; README.md 333-338, 343, 432-445, 503, 508, 699-709, 987-992, 1028-1033, 1095-1098, 1118-1136, 1155-1156 and 1163-1171 (line numbers at `ea634043`). Read in code only: the offers' real-GitHub paths and Windows (the real `gh` never ran). Not read: `park` and `resume`, which live in openRepoTools. **True but loose, left as they are.** AGENTS.md:83 and README.md:440 say a run with no terminal "stops after the plan"; a missing prerequisite stops it earlier, at the preflight (exit 2), which AGENTS.md:88 covers. README.md:1216-1218 says the machine check runs "with stdin closed"; the doctor also starts it in a session of its own (shape-doctor.py:1284-1285), which is what keeps the terminal out since #492. AGENTS.md's doctor item 3 does not say to ask the human which of #495's two `bump-leg.py --to` lines to run: an omission, not a false statement. **Outside these files.** `scripts/bump-leg.py:1513` still prints "re-places it on its tracking branch AT the new pin" after every bump. It is on #458's checklist with the rest of #490's follow-ups, and a docs-only pull request leaves it there. **Not holding #483 open: an omission.** #465 deleted "A digest that recomputes differently is reported as drift" together with its false clause. So README.md (698-709) and the handbook beside it no longer say what a recomputed neutral-pin digest that disagrees does. Nothing there is false, so it does not hold #483 open. Both files are at their caps, and the sentence needs about two more README lines that no untouched paragraph gives back, so it is not added here. Re-checked at `ea634043`: the finding is validate-pins.py:968-970 and 1140-1142 (exit 1), the refusal 814-816 and 1172-1175 (exit 2). A sentence for whoever next edits that paragraph: "A digest that recomputes differently is the finding `neutral-pin-digest-mismatch` (exit 1); a pin file the declaration names but that is not there is the refusal `neutral-pin-missing` (exit 2)." A robustness note on main's `test_the_documents_say_what_an_assistant_does_with_no_terminal` (#491): it strips `<...>` from README.md as well as from the HTML page, and a README line holding `<(` and no `>` swallows lines up to the next `>`; stripping tags from the `.html` page only would make that test indifferent to such a line. `git merge-tree --write-tree` of `ea634043` against the open pull requests that edit these files: clean with #503 (`6fe17be5`); #506 (`f184a51c`) and #486-#488's #507 (`708f3bbb`) each conflict in the handbook footer alone (its history prose and `README.md blob` line), and README.md and AGENTS.md merge clean with all three and stay at 1422 and 490 lines. Whichever lands later recomputes `git hash-object README.md` and keeps every "added (#n)" clause. #394, #484, #465, #468 (`57c85e92`), #490 (`7348c57b`), #508 (`aa4e1f59`) and #491 (`da197359`) are merged here. - At this head (`9ac90d16`): `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_setup_project_py.py tests/test_setup_sh.py`, 277 passed; `scripts/validate-repository-naming.py --explain Atlas Atlas-spec Atlas-code` exits 0 (`project-leg`: assembly, spec, code) and `--explain At-las At-las-spec At-las-code` exits 1 (`FINDING naming-unclassified`), so only the project name's hyphen is refused. - At `ea634043`: `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`, 122 passed (the same on the merge `94793aeb`); `tests/test_setup_project_py.py tests/test_setup_sh.py`, which read README.md and the handbook for #491's no-terminal route, 155 passed; `tests/test_free_plan_hint.py` with the AGENTS.md step 7 tests of `tests/test_adopt_late_failure.py` and `tests/test_adopt_github_exits.py`, 18 passed; and on the merge, `tests/test_bootstrap_after_bump.py`, 23 passed. A stub `gh` first on PATH (it logged no call), `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`, a blank identity. - `rvD-worddiff.py`, `rvD-hunks.py` and `rvD-mdstruct.py` over `ea634043` against its parent: four `WORDS CHANGED` hunks, all of them the D1-F9/D3-F7 sentence and the blob, and no re-wrap; block structure, links, HTML and code spans identical (README.md 678/678 blocks, AGENTS.md 322/322). The merge `94793aeb` against `70d0d1b3` changes the same lines as `83cfad54` against `aa4e1f59` (366 insertions, 336 deletions), apart from the two resolved hunks. - Review round four (the first reviewer, at `ea634043`): APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits). - It ran `git show --remerge-diff` on every merge since round three (`71359744`, `fb6a9ab4`, `83cfad54`, `94793aeb`). Each hand resolution is a named conflict hunk, and no row, clause or fact was lost. - It reproduced the D1-F9/D3-F7 sentence with a real bump and `make bootstrap` in seven cases, and each placed the leg or said why not: a branch behind the pin; ahead with no commit of its own; with commits of its own; diverged; checked out in another worktree; a commit on the detached leg; a move a local change blocks. - It re-checked every routed row and ten moved-evidence rows, and read the whole of main's change since `ec4a7a0e`, finding nothing else false. - It ran the hygiene, codes, CLI-reference, setup, bootstrap-after-bump and free-plan tests: 314 passed. - Its nits: N1, the hyphenated project name, made at `9ac90d16`; N2, a run with no terminal stops at the preflight rather than after the plan when something is missing, which AGENTS.md:88 covers; N3, this description. - Two `bootstrap.py` defects it met while reproducing are on #458's checklist (issuecomment-6142614866). - At `83cfad54`: `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py`, 119 passed. On `fb6a9ab4`, the merge of `57c85e92`, the same three gave 119 passed, and `tests/test_shape_doctor.py tests/test_doctor_unfinished_merge.py tests/test_free_plan_hint.py tests/test_openreposhape_command.py tests/test_shape_advisory.py` gave 465 passed, 3 skipped (the doctor's PowerShell line, which prints only on a Windows host). The table's "no row is a `FINDING`" holds with the `merge` row: `shape-doctor.py` returns `COMPLIANT` only at :6171, after :6168-6170 answer `INVALID (<ids>)` for any `FINDING` row, and `merge` is a `FINDING` only for an unmerged path (:5879; a merge with none left is a `note`, :5862). All runs with a stub `gh` first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`. - At `71359744`, the merge of `48585e62`: the three files, 119 passed (a stub `gh` first on PATH, `GH_TOKEN` bogus, an empty `GH_CONFIG_DIR`; the stub logged no call). - The same three files on a throwaway merge of `f35e59fa` with #491's head (merge-tree, commit-tree, a detached worktree, footer resolved, then removed): 122 passed, and `tests/test_setup_project_py.py tests/test_setup_sh.py -k no_terminal`: 8 passed. The failure the second review found (`test_the_documents_say_what_an_assistant_does_with_no_terminal[README.md]`, 1 failed, 122 passed) came from the `bash <(` lines this commit removes. - `tests/test_adopt_late_failure.py::test_agents_md_step_7_names_the_exit_the_tool_prints` and `tests/test_adopt_github_exits.py -k "step_7 or agents"` (round two, on `ccf520bd`): 2 passed; they read AGENTS.md step 7, which is untouched. - `rvD-worddiff.py`, `rvD-hunks.py` and `rvD-mdstruct.py` over the third commit against its merge parent: every changed region is one of the corrections above (the `bash <(` blocks back to main's text, D3-F5, the park parenthetical, the placement sentence and the three word trims), and the two `WHITESPACE-ONLY` hunks are the family steps going back to main's bytes; block structure 320/320 and 678/678, links, HTML and code spans identical. - No test greps a phrase this pull request changed: the old phrases appear under `tests/` only in history comments and the doctor's docstring. The third head (f35e59f) was re-verified: APPROVE WITH NITS (0/0/4), all four in this description and fixed there. Main 48585e6 (#465) was then merged as 7135974, and 57c85e9 (#468) as fb6a9ab; aa4e1f5 (#508) merged clean as 83cfad5. Main 70d0d1b (#491) was merged as 94793ae, and ea63404 corrects D1-F9/D3-F7. A fourth review round at ea63404 covered every merge since f35e59f (remerge-diff: no row, clause or fact lost), the bootstrap sentence in seven reproduced cases, and the whole docs delta since ec4a7a0: APPROVE WITH NITS (0/0/3), and #483 can close. Its N1, "a hyphenated project name", is 9ac90d1. CI is green on Linux, macOS and Windows, with SonarCloud passing, at 9ac90d1. Main cc8f2da (#376), which landed after its last merge, touches none of its files, and the combined tree passed the docs and setup tests (277 passed). Closes #483 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lane: openreposhape-1 (openRepoShape-1)
Claim: #189 (comment)
The answers this records come from #166:
planasks which leg holds each source submodule, andcheckandexecutehold the plan to that answer. Three things this builds on are onmain:.gitmodulesto one leg and a submodule it registers to another passescheckandexecute, saysadoption verified, and the assembly's recursive clone then fails:planshould ask,checkshould find,executeshould refuse #166, as e4decfb (adopt-project.py: a plan that sends a source's.gitmodulesto one leg and a submodule it registers to another, or keeps either in the root, is now acheckfinding and anexecuterefusal, andplanasks the one-leg question on.gitmodulesitself (#166) #194)..cursor,.specify/vendored) as MISPLACED, though #166 requires a leg for it #176's doctor read side, as 4ce9cb0 (shape-doctor.py: a submodule a leg keeps is a note naming it, neverroot by rule, so a #166 assembly with a submodule at .cursor or .specify/vendored is no longer MISPLACED (#176) #186).Icon\rfile), socheckrefuses the whole plan asyaml-unparsablewith no entry to edit #199's escape table, as 4835fb4 (adopt-project.py: the plan writer escapes every character that is not printable from the one tablerepo_shape's reader resolves, so a source holding the macOSIcon\rno longer writes a plancheckrefuses, and a non-UTF-8 name no longer empties the plan (#199) #247).This branch has merged
mainat 437a360, so its diff againstmainis #189 alone: 13 files, +1701 −158.Head: b88a36e,
gh pr update-branch's clean merge ofmain437a360 into 71d3ce2. 71d3ce2 isgh pr update-branch's clean merge ofmainec4a7a0 into 7adcc84. 7adcc84 isgh pr update-branch's clean merge ofmain8782333 into 7db009e. 7db009e isgh pr update-branch's clean merge ofmain6c2a371 into 120d6ed, and the re-verify found 120d6ed and 7db009e ready for their code and tests. 120d6ed isgh pr update-branch's clean merge ofmaind6df36b into 60a1a34. 60a1a34 is the merge ofmain75052cb, which landed #188, into 136a37f. 136a37f isgh pr update-branch's clean merge ofmaindd8d0ea into 78034e7, and 78034e7 is the merge ofmain53bee5b into 563ea16, which the re-verify found ready for its code and tests. 563ea16 and 3b7cb8f aregh pr update-branch's merges ofmain3ef059c into 3b7cb8f and of 45e01f4 into 4b00597. 4b00597 is the merge ofmain1f90a0f into 48bdbf9, and 48bdbf9 isgh pr update-branch's merge ofmain28bdc84 into 70f4d29, which the re-verify found ready before that. Lane openRepoShape-2 reviewed e167d8b, and a second review covered 3e86200. The fix round since e167d8b is thirty-one commits, twenty-two of them merges ofmain; the Reviews section maps each finding to its commit.Ruling
Brett Heap ruled that #189 goes ahead as a code change, with no OpenSpec change first: "merge #189 once its review passes and it's green". It had been held for that ruling, because it adds a
submodules:record to every adopted project'sproject.yaml.The record's schema is documented where this repository documents
project.yaml:validate-manifest.py, which checks it;executewrites and how the doctor judges by it.What
Under #166, a human answers in the adoption plan which leg holds each submodule of the source. The plan is committed nowhere, so the assembly kept no trace of that answer. After #176,
shape-doctor.pycould only note a kept gitlink, askeptor asunverifiable, and a submodule added by hand looked exactly like one a person had placed. Now the answer is written into the assembly, and the doctor holds each submodule to it.adopt-project.py:
executerecords the answer.submodule_recordbuilds asubmodules:block, and_commit_the_splitappends it to the split'sproject.yaml, in the split commit.REFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188's late-failure reporting, under thecommitting the splitstep. A failure while it is built, or after it is appended, is reported as any failure of that step is: oneREFUSEDline, what failed, and one notice of what the run left.path: its path in the leg, whichgit filter-repokeeps from the source;leg;resolution:, the covering entry's reason, when it has one;source_commit._answered_legs, the readingcheckalready holds the plan to. By the time this runs,_refuse_what_check_findshas refused any gitlink that is uncovered, covered twice, unanswered or answeredroot.dropis in no leg, so it gets no row.repo_shape.double_quoted, the writer adopt-project.py plan writes a plan its own loader cannot parse when a source name holds a line break (the macOSIcon\rfile), socheckrefuses the whole plan asyaml-unparsablewith no entry to edit #199 gave the reader. These all read back as themselves:null,trueor0001;"": theproject.yamland the split it always got.templates/assembly-root/scripts/validate-manifest.py: the record is checked, because something reads it.
submodulesis optional. When present it is a list of mappings, one per submodule a leg keeps.pathis named by one row only, and is spelled the one way Git records a path (repo_shape.path_spelling_fault): relative,/between segments, and no segment empty,.or..../x,x/,a//banda/./bare findings.git update-indexrefuses each asInvalid path, so no submodule is ever at one.vendor/a\b,C:/x), and an adoption records a submodule at one, so refusing them would makeexecute's own manifest INVALID.café, and names with a space such asmy lib, are names too.legisspecorcode.resolutionis optional text.source_commitis optional, and 40 hex when present.manifest-submodules,manifest-submodule-path,manifest-submodule-leg,manifest-submodule-resolution,manifest-submodule-commit.scripts/repo_shape.py and scripts/shape_materialize.py: one spelling rule for both.
path_spelling_faultis the segment half of scaffold-project.py accepts a non-canonical leg path such as--spec-path spec/, writes a straysubmodule.spec/.urlbeside a registration that points at the work directory, and the new repository's recursive clone fails once the work directory is gone: #169's bug on the scaffold side #179's leg-path rule. It moves intorepo_shape.pybecause a project carries that file and notshape_materialize.py._spelling_reasonasks it for the segments and keeps its own sentences, in its own order. At 4b00597 it givesmain1f90a0f's answer, byte for byte, for 87,482 strings, 33 named spellings among them, and tests/test_adopt_leg_paths.py and tests/test_scaffold_leg_paths.py pass unchanged.double_quoted's docstring now names the record among its writers.shape-doctor.py: the
placementrow holds every submodule a leg keeps to the record.recorded_submodulesreadsproject.yamlunder adopt's ownSUBMODULE_RECORDkey, so the writer and the reader spell it once.recorded twice. A row whoseleg:the validator refuses decides nothing, and themanifestrow names it instead.recorded, which counts as placed. The row isokwhen every leg was read, every name in each could be read, nothing else is misplaced or in question, and every submodule is recorded.misplaced. It is an entry inmisplacedwithrule: null,confidence: recorded, and a reason that names the record and carries the plan'sresolution:. So:MISPLACED (n path...), exit 1;paths:, whereadopt-project.py's own_leg_findingsaccepts it..gitmodules. Ruleambiguous-gitmodulesasks which leg owns the submodules a.gitmodulesregisters.answered, and the row's reason says the record answered it. So a real adoption readsok, where it read anotenaming its own.gitmodules.root by rule, so a #166 assembly with a submodule at .cursor or .specify/vendored is no longer MISPLACED (#176) #186'skeptorunverifiablenote, word for word. A plan in which no row names any submodule keeps shape-doctor.py: a submodule a leg keeps is a note naming it, neverroot by rule, so a #166 assembly with a submodule at .cursor or .specify/vendored is no longer MISPLACED (#176) #186's legend.--jsonand the plan'ssubmodules:block carryrecorded_leg:on any submodule a row names.AGENTS.md gains two passages, and tests/test_repo_hygiene.py moves its cap from
main's 482 to 490 lines, with the paragraph that says why:executewrites the record.rule: null,confidence: recorded), and that one moved on purpose stays MISPLACED, its row the human's to change.Pinned files.
scripts/repo_shape.pyandtemplates/assembly-root/scripts/validate-manifest.pyare copies a project pins.repo_shape.py, readsupstream-changedfor them inupdate-shape.py checkuntil its estate re-pins withapply, as for The two copies of "is this the same repository" become ONE definition in scripts/repo_shape.py, and #149's case rule lands with the move (#155) #156 and repo_shape.py:load_yamlrefuses a YAML file holding a non-UTF-8 byte asREFUSED yaml-unreadablenaming the byte, soadopt-project.py checkandexecuteand the validators no longer die with aUnicodeDecodeErrortraceback and exit 1 on a hand-edited plan or manifest (#192) #193. This repository holds no digest of either to update.applycopies everyupstream-changedrow together, and rolls back if the project's validators go red. So the new validator never lands beside an olderrepo_shape.py. If a localrepo_shape.pyis kept withapply --accept-local scripts/repo_shape.py, the new validator runs once, fails its import, andapplyrefuses asupdate-validators-redand rolls back.What did not change. The proof is under Tests.
.gitmodulesthe policy asks about. That covers every assembly adopted before this change, and every submodule added by hand.project.yamland adoption plan.Where the record lives. The issue offered two homes: a
submodules:block inproject.yaml, or a newcontracts/submodule-placement.yaml. This usesproject.yaml, for three reasons:AGENTS-shape.mdcalls the manifest "this project's own content". It is not pinned.visibility:(Accept internal visibility; resolve --pin under opensoft by default #6) was added the same way, as one optional field with a validator check. A new file would need a newkind:registered with the doctor, and a validator copied into every root, which every existing project would see asupstream-added.Why (repro on
main)Measured on
main75052cb and on this branch at 60a1a34. The output is the same as at each earlier pair this round measured, back tomain4835fb4 and 2a93da8.On
main75052cb the doctor has no way to consult an answer. Take a scaffolded project whose code leg keepsdocs/themeas a gitlink; rulespec-governanceputs that path in spec. A hand-writtensubmodules:record changes nothing:On this branch:
On
main75052cb, an adoption records nothing. The source has.cursoras a registered submodule, and the plan answers both it and.gitmodulescode.executesucceeds (adoption verified) and the split'sproject.yamlhas no record. The doctor on the recursive clone says:Both rows also ask about
code/.gitmodules. The answered submodule and the one nobody answered are indistinguishable.On this branch the split's
project.yamlends:and the doctor says:
submodules:answers the question the policy asks of code/.gitmodules: which leg owns the submodules it registers".vendor/lib, which no row names,code/.gitmodulesis a question again.Tests
New and changed tests against
main(26). Those marked ✱ are new or changed in the fix round.tests/test_shape_doctor.py:test_a_submodule_in_the_leg_its_record_names_is_placed:recorded, rowok.test_a_submodule_in_the_other_leg_from_its_record_is_misplaced✱: FINDING,MISPLACED (1 path), exit 1, and a plan entryadopt-project.py's_leg_findings(plan, None)accepts.test_a_submodule_no_record_names_is_still_the_note: shape-doctor.py: a submodule a leg keeps is a note naming it, neverroot by rule, so a #166 assembly with a submodule at .cursor or .specify/vendored is no longer MISPLACED (#176) #186's note.test_a_record_row_the_validator_refuses_decides_nothing: a row sayingleg: rootis amanifest-submodule-legFINDING and places nothing.test_where_two_rows_name_one_path_the_first_decides✱ (3 orders): the first row decides, the validator namessubmodules[1], and the verdict is red.test_the_record_answers_the_question_about_the_gitmodules_beside_it✱ (3 cases):ok, no question, the clause;note, still asked.test_the_question_stays_where_the_record_does_not_place_every_submodule✱ (3 cases): beside a hand-added or a misplaced submodule, or one whose name holds a control character (POSIX only), the.gitmodulesis still asked.test_a_name_the_record_spells_with_an_escape_still_names_its_submodule✱: six names spelled with an escape arerecorded, then MISPLACED (6 paths) once moved.test_a_recorded_name_with_a_backslash_is_valid_and_placed✱ (POSIX only).adopt-project.py executeand a recursive clone:test_an_adopted_submodule_is_recorded_and_placed✱: COMPLIANT, and the row isokwith no question left.test_an_adopted_submodule_whose_record_is_edited_is_misplaced.test_a_submodule_added_by_hand_after_adoption_is_the_note.tests/test_adopt_submodules.py:test_the_record_names_each_submodule_a_leg_keeps_with_its_answer.test_a_source_with_no_submodule_kept_gets_no_record(2 cases).test_a_recorded_name_reads_back_as_that_name✱ (21 names, 10 of them new). Every record is whole lines.test_a_name_that_is_not_utf8_is_spelled_and_still_written✱: the name is written as\udce9and reads back as itself.test_adopt_moves_existing_gitmodules_with_its_dependency(changed): it asserts the record.tests/test_adopt_e2e.py:test_a_source_with_no_submodule_gets_no_submodule_record.tests/test_manifest_validator.py:test_a_submodule_record_is_accepted.test_a_submodule_record_row_that_says_nothing_is_a_finding✱ (14 cases, 5 new spellings).test_a_submodule_record_that_is_not_a_list_is_a_finding.test_a_recorded_path_git_never_records_says_why✱.test_a_recorded_path_git_records_is_accepted✱ (5 cases: backslash, drive letter, non-ASCII, code-point escape, space).test_the_recorded_path_rule_refuses_what_git_will_not_record✱ (13 paths, 2 POSIX only): the rule is held togit update-indexitself.tests/test_windows_paths.py:test_a_double_quoted_scalar_resolves_one_escape_at_a_time✱, with 8 rows for the escapes the record relies on.tests/test_repo_hygiene.py:test_agents_md_is_short_enough_to_be_read✱, the cap at 490.tests/test_adopt_late_failure.py, adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints onlyREFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188's: itssplit_argumentshands_commit_the_splitaSplitPlan(), a dict withentries = (), where it handed{}, which has noentriesfor the record to be built from. Its sources keep no submodule, so no test of it changes, and its 86 tests pass with git-filter-repo on PATH.Revert table, fix round. Each mutation was applied alone, to a fresh clone of 60a1a34, and all 15 tests the round added or changed (83 cases) were run against it. Every case gives the same outcome at 4b00597, and did at 70f4d29 and a557f2a: 2,241 outcomes (27 runs of 83 cases) each time, none different. The 82 cases and 26 mutations it shares with the table at 27d039b give the same outcome there. They do so at 3e86200, f38d543, e334f50, a01ab02 and 2f5cda9 too, and at 2a93da8 before that. Each time 2,132 outcomes (26 runs of 82 cases) were compared, and none differed. F1's case and its revert, M29, are new.
_leg_findingswithouttree..._other_leg_from_its_record_is_misplaced_quoteddid (ascii(),\xe9)..._not_utf8_is_spelled_and_still_written\x4\xzz\U00110000\\x41and\U00110000; record name\x41..._says_why.segment..._says_why; git paths./x,x/,a//b,a/./babsolute-path; git path/xthe-leg-itself; git path...dotdot-path; git patha/../bbackslashanddrive-letter; git pathsa\bandC:/x; the doctor's backslash testnon-ascii,code-point-escapeandspace; git pathsmy libandcafé; the doctor's escaped namesxandvendor/xrefused-leg-then-coderecorded-in-this-leg; the end-to-end adopted test..._does_not_place_every_submodule.gitmodulesanswered whenever it keeps a submodulerecorded-in-the-other-legandno-record; all 3 cases of..._does_not_place_every_submodulerecorded-in-this-leg; the end-to-end adopted testnel\x85xandline\u2028sep; the doctor's escaped namesmain's 482ynullandtruecr\rx.gitmodulesbeside a name the doctor could not readone-whose-name-cannot-be-readRevert table, first round (at e167d8b, the 16 tests the record first came with). Each mutation was applied alone; with none, 36 passed.
held_clausesemits an empty clause;with nothing)..._kept_gets_no_record(2),..._gets_no_submodule_record(e2e)..._reads_back_as_that_name(2 of 11),..._not_utf8_...leg:resolution:..._record_is_acceptedokRound trip at 60a1a34, against
main's reader. Every name went throughsubmodule_recordand back throughrepo_shape.parse_yaml, path and reason both:str.splitlinestakes, and the 128 surrogates a byte that is not UTF-8 is kept as.? a: bin a flow mapping as the literal key? a, where the block reader refuses explicit keys, and refuses{a: }and{a:}asnot a flow collection: ''instead of reading{'a': None}#252 and scripts/repo_shape.py: the double-quoted reader that #247 adds (fix/adopt-plan-linebreak-names, not yet on main) resolves\x00inside a block-scalar KEY, and_parse_mappingsplits the NUL-joined sentinel record, so"a\x00b": |reads as the keyawith the bodyb<NUL>textwhere main reads the literal keya\x00b#248 changed in the reader since 39ac59c, five shapes each, so 240 records: 0 mismatches. They include? a,{? a: b},{a: },|,a: |, a NUL, and the old block-scalar sentinel.k: [it's, a]reads["it's, a"]where its block twin reads["it's", "a"],k: it's fine # notekeeps the comment in the value, and #244 turnsk: [a"b\", c]from right (by accident) into one item #249 and scripts/repo_shape.py_scalartakes any text that starts and ends with a quote for a quoted scalar and anything else for plain text, so an unterminated"abcreads as"abc,- "a\"reads asa\, and"a" b "c"reads asa" b "c, where PyYAML refuses each #250 now start and end a quoted scalar, five shapes each, so 195 records: 0 mismatches. They includeit's,a"b,"abc,a" b "c,a' b 'c,"a\",a\,[it's, a],note "[1]"and"Brett Heap.Byte identity, this branch against
main75052cb. One committed standard; onlyshape-doctor.pyandadopt-project.pyare swapped between runs.submodules,no-recordstages.cursor,docs/themeandvendor/libgitlinks in the code leg with no record, which are #186's cases.+gitmodules,no-recordadds the leg's.gitmodules.The last nineteen merges of
mainwere a01ab02, of 5751460; e334f50, of d94d155; f38d543, of c3a9645; 3e86200, of 6d8d88b; 27d039b, of 71b58a1; a557f2a, of a1b3a4b; 70f4d29, of f795cf7; 48bdbf9, of 28bdc84, made bygh pr update-branch; 4b00597, of 1f90a0f; 3b7cb8f, of 45e01f4, and 563ea16, of 3ef059c, both made bygh pr update-branch; 78034e7, of 53bee5b; 136a37f, of dd8d0ea, made bygh pr update-branch; 60a1a34, of 75052cb; and 120d6ed, of d6df36b, 7db009e, of 6c2a371, 7adcc84, of 8782333, 71d3ce2, of ec4a7a0, and b88a36e, of 437a360, all five made bygh pr update-branch. Among what they brought are:.gitmoduleswith a key with no value passescheckwhen the tip holds no submodule but the history does, andexecutedies ingit fast-exportafter both legs are pushed #195's.gitmodulesrefusal, and adopt-project.pycheckaccepts a.gitmodulesvalue git's submodule reader rejects (update = bogus,update =,update = !cmd,shallow = maybe,fetchRecurseSubmodules = bogus), soplan okis followed byexecutedying ingit filter-repoafter both leg remotes exist, whether the tip or only the history holds the gitlink #215's for a value git's submodule reader refuses;checkaccepts plan pathsexecuterefuses asunsafe-value(a space or a non-ASCII character), soplan okis followed by a refusal and #166's split instructions can name entriesexecutewill refuse #196'splan-unsafe-path, for an entry pathexecutewould not extract as it stands, and adopt-project.pycheckpasses a plan that keeps a plain file at a directory the shape writes into (.github,contracts,scripts), andexecutedies in aNotADirectoryErrororFileExistsErrortraceback after both legs are pushed #263'splan-root-blocks-shapeand adopt-project.pycheckpasses a plan that keeps the source's ownshape/in the root, andplace()then writes the shape's collision copy onto it after both legs are pushed: a realshape/README.mdis overwritten whileexecuteprints "nothing was overwritten" and endsadopt-lost, and ashape/Makefile/directory or a file namedshapeis a traceback #262'splan-root-blocks-shape-copy, for a path the plan keeps in the root where the shape writes;--elected-by,--referenceor the--sourcedirectory passesplanandcheck, andexecutepushes both legs before it dies withUnicodeEncodeErrorinwrite_lf#255'splan-unwritable-valueandadopt-unwritable-value, for a valueexecutewrites into a file that a file cannot hold;? a: bin a flow mapping as the literal key? a, where the block reader refuses explicit keys, and refuses{a: }and{a:}asnot a flow collection: ''instead of reading{'a': None}#252's, scripts/repo_shape.py: the double-quoted reader that #247 adds (fix/adopt-plan-linebreak-names, not yet on main) resolves\x00inside a block-scalar KEY, and_parse_mappingsplits the NUL-joined sentinel record, so"a\x00b": |reads as the keyawith the bodyb<NUL>textwhere main reads the literal keya\x00b#248's, scripts/repo_shape.py treats a quote character in the MIDDLE of a plain scalar as opening a quote, sok: [it's, a]reads["it's, a"]where its block twin reads["it's", "a"],k: it's fine # notekeeps the comment in the value, and #244 turnsk: [a"b\", c]from right (by accident) into one item #249's and scripts/repo_shape.py_scalartakes any text that starts and ends with a quote for a quoted scalar and anything else for plain text, so an unterminated"abcreads as"abc,- "a\"reads asa\, and"a" b "c"reads asa" b "c, where PyYAML refuses each #250's reader;project.yamlat a root that also carries afamily.yaml, so an assembly root whosefamily.yamldeclares two unmounted members is COMPLIANT, and itsmanifest kindsrow says that kind has a registered validator, which is never run #211'smanifest kindsfinding;planraisesUnicodeEncodeErrorin its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.mdunder cp1252, as on a redirected Windows stdout), soplanexits 1 beforeplan written to#214's stdout escaping;checkandexecuteprint a source path raw in a FINDING, so a\r,\n,\x85, U+2028 or ESC in a name overwrites, splits or escapes the line on a terminal, whereplan's questions spell the same path with_spelled#212's finding spelling;project.yamldoes not parse (a merge conflict) NOT A SHAPE ROOT, exit 2, and its NEXT line tells the reader to adopt that assembly root in place while the Triad row calls it a single repository #205's verdict for a manifest that does not load;validator_rowfalls back to the standard's TEMPLATE validator when a root carries no copy of its own, and prints that path as the NEXT command without thePYTHONPATHit runs it under, so pasting themanifest,pinsorfamilyrow's NEXT dies withModuleNotFoundError: No module named 'repo_shape'and exit 1, the code a validator uses for a finding #238's and shape-doctor.py: after #238 the doctor runs a root's OWN validator with the operator'sPYTHONPATHstill in the child's environment, so for an operator who exports one that reaches arepo_shape.pythemanifestrow of a root that lost its ownscripts/repo_shape.pysaysok ... passesagain, the false pass #238 removed, where the project's own gate in CI fails onModuleNotFoundError: No module named 'repo_shape'#306'sPYTHONPATHfor a validator, printed in its row's next step and given to its child, and bump-leg.py (a real bump) and update-shape.pyapplyrun the project's own validators as children that cannot print its display name on a cp1252 stdout, so a project named日本is refused and rolled back asbump-leg-validators-redorupdate-validators-redunderPYTHONIOENCODING=cp1252:surrogateescape, and the refusal blames the validators #254'sPYTHONIOENCODINGfor a project's validators;executeclones the source with the operator'sinit.templateDirapplied, so a template holdingmodules/specandmodules/codemakes the leg mounts'--force(#200) reactivate those planted stores: both gitlinks pushed on the split branch are a stranger's commit, the VERIFICATION still saysadoption verifiedbecause it checks a mount is present and not which commit, and the recursive clone of the result exits 128 #243's assembly cloned with no git template, and itsadopt-mount-commitverification;plan'scross_leg_referencespasses a spec directory's name togit grep -Fwith no-eand the code entries as glob pathspecs, so a spec directory named-notesor a code entry named:cachesilently drops the follow-up, a code entry named*names files that are not in the code leg, a name outside ASCII is printed in git's quoted spelling, and a spec directory named-O<cmd>is run as git grep's pager when a tracked file holds the planned commit's hash #216's literal paths inplan's follow-upgit grep, and adopt-project.py and scaffold-project.py substitute the template placeholders one after another, so a free-text value holding another placeholder's token is expanded (--elected-by '{{SPEC_PATH}}'is recorded aselected_by: "spec") or, after the legs are pushed, refused astemplate-unsubstitutedwith "a defect in the substitution table of the tool you ran" #260's one-passrender;scaffold-project.py --helpsay nothing of the scaffold's two path-collision refusals (scaffold-leg-path-collides,scaffold-pin-collides) or thatspec,codeandshapecannot be pinned, so a human or agent can learn of them only by hitting them #288's AGENTS.md lines on the scaffold's two path-collision refusals;url = ../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 check of a relative.gitmodulesurl a leg keeps, refused before any leg exists where the source's remote and the leg's resolve it differently;adopt-project.py executeexit is to fix the plan and re-run into fresh legs, but in GitHub modeexecuteexits 2 with the split pushed whengh pr createis refused, and exits 2 afteradoption verifiedwhen the topic will not set, so following step 7 abandons a pushed adoption whose own exit is a command to run by hand #304's verification beforeexecuteexits on a refusedgh pr create, and step 7 keyed to the exit it prints;legsrow's NEXT (and themembersrow's) runs<root>/scripts/bootstrap.pywithout asking whether the root has it, so a root that lost it pastespython3: can't open file, exit 2, and for an empty leg mount thelegsNEXT would not cure the row even with the file there (bootstrap.pyonly printsRun git submodule update --init spec) #276'slegsandmembersNEXT, a script-freegit submodule update;rewrite_shape_pinand bump-leg.pyrewrite_pinfindtree_sha256:by exactly two leading spaces and replace whole lines, so acontracts/<role>-pin.yamlwhosedigests:child is indented four is refused as hand-edited, and a trailing comment on acommit:ortree_sha256:line is dropped by every pin rewrite,rewrite_manifestincluded #323's pin rewrites, which keep each line's own layout;planwritesleg: specwith no question for a source path thatexecutewill refuse (-notes.md), so the answercheckorexecutethen rejects is oneplanchose for the person, androotordropis offered for that entry nowhere #319'splanquestion about every source pathexecutewill not extract as it is named;manifest kindscounts acontracts/*.yamlthat declares a registered kind as validated though that kind's validator never runs on this kind of root, so acontracts/family.yamlin an assembly root, or acontracts/project.yamlin a family holder, isokand COMPLIANT where the same bytes at the root are a FINDING #327'smanifest kinds, which counts a registered kindvalidatedonly on a kind of root that runs its check;apply --branch --prre-run:_awaits_its_pull_requestreads the pin from HEAD while the "nothing to do" test reads the working tree, so a re-run over a tree that an in-placeapply --at Calready wrote pushes HEAD and titles the pull request with C #333's--prre-run, family.pyadddoes not ask whether the member'sproject.yamlidis one the family already pins, so adding a repository renamed at the forge (sameid, new name) commits a holder thatvalidate-family.pythen refuses asmember-duplicate-id#330'smember-duplicate-idand family.py_checked_member'sMEMBER_REadmits an owner or a project that begins with.,_or-(testorg/_Zed,testorg/.Zed,testorg/-Zed,_org/Zed,.org/Zed), which the holder's ownvalidate-family.pyrefuses with itsQUALIFIED_RE, soaddexits 0 with a commit whose new row its gate then reports asmember-repository#366'smember-malformed;scaffold-leg-path-collidesorscaffold-pin-collidesin AGENTS.md, or either code renamed inscaffold-project.py, passes every test, and an assistant briefed from AGENTS.md relays a code no tool prints #348's tests/test_agents_md_codes.py, which holds every code-shaped name in AGENTS.md to a string a shipped tool prints;--idand--tracking-branchinto project.yaml unquoted, so--id nullor--id truescaffolds a project whose first bootstrap failsmanifest-id, and--tracking-branch true,007ornullputs the clone's legs onTrue,7ormainwith no upstream: #181's defect on the scaffold side, beside #225 #258's and 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'splan-manifest-invalid, which asks the project'svalidate-manifest.pyabout the manifest's values before anything exists;]or}is read, not refused, sok: {a"]:}reads{'a"]': None}since #284 (a quote toggle used to swallow the:}) andk: [a]]reads['a]'], where PyYAML refuses both #301's refusal of a bracket in a plain flow scalar, and validate-manifest.py accepts a display name holding a lone surrogate (name: "a\ud83db") and then raisesUnicodeEncodeErrorprinting it, somake validateand shape-doctor.py fail on a traceback instead of a finding while bump-leg.py and update-shape.pyapplypass the same project once #254 lands #280's finding for aname:holding a lone surrogate;validator_rowreads a validator's exit 1 as "reports a finding" whatever caused it, so a validator that CRASHED (a:in the standard's path splits thePYTHONPATHthe template fallback runs under, andvalidate-manifest.pydies onimport repo_shape) is a FINDING row quotingModuleNotFoundError, and a root whose manifest is valid goes fromCOMPLIANT, SHAPE BEHINDtoINVALID (manifest)#278, shape-doctor.py: thepinsrow says "ok ... the project's own scripts/validate-pins.py passes" for a project that pins a neutral product though that validator SKIPPED the product's digest after a realgh apicall, so the command documented to fetch nothing fetched from GitHub, and a digest nobody rechecked reads asokunder aCOMPLIANTverdict #297 and shape-doctor.py: for a root whoseproject.yamldoes not load AND whosescripts/repo_shape.pyis gone, themanifestrow's NEXT promises its validator "refuses it the same way until it loads", but pasted it dies onModuleNotFoundError: No module named 'repo_shape', exit 1, so the person is sent to a command that cannot say what the row quoted #298, which say when a validator crashed;--dry-rundies the same way #289's refusal of a leg pin or workflow file that is not UTF-8, and bump-leg.py: a workflow file, a.github/workflows/directory or a leg pin that this user cannot open ends the run in a PermissionError traceback, exit 1, under--dry-runas well, becausecmd_bumpandplan_workflowsread them with no handler andmain()catchesRefusalandCommandFailedonly, so no refusal names the file or says to make it readable #383's of one this user cannot open;force_stage's docstring says it has TWO CALLERS and that onlyupdate-shape.py apply --addcalls it alone, andstage_written's says the same, though since #242 family.py's_commit_one(everyadd,bumpandremovecommit) andcmd_bumpcall it alone too, so the next change to its contract is reasoned about with half of its alone-callers missing #369's contract forforce_stageandstage_written, family.pybump(andadd,remove) take no--dry-run, though the file's exit-code paragraph says a dry run prints and exits 0 andbump-leg.pyhas one, so the only way to see what a bump would pin is to run it: fetch, check the member out at--toand rewritefamily.yaml#326'sadd --dry-run, and adopt-project.py:_electorand_remote_repositoryreadgit config user.nameand the origin URL throughgit_out, which decodes strictly, so a name or URL that is not UTF-8 endsplan(and, for the URL,checkandexecute) in a UnicodeDecodeError traceback, exit 1, naming neither the git setting nor the flag that avoids it #292's_electorand_remote_repositoryfor auser.nameor an origin URL that is not UTF-8;check_shape_currencytakes every refusalupdate-shape.py prepareraises over the ROOT's owncontracts/shape-pin.yaml(a merge conflict, acommit:that is not 40 hex, a deleted file) for this checkout failing to ask, so the verdict is CANNOT ANSWER, exit 3, outranking INVALID, and its NEXTgit -C <standard> fetch --allblames the standard for a fault in the repository #237'sINVALIDfor a root's owncontracts/shape-pin.yamlthat does not read as a pin, and The assembly root'sscripts/validate-pins.pyends in aFileNotFoundErrortraceback, exit 1, a finding's code, when git is not on PATH, becauserecorded_gitlinkreachesrepo_shape.git_outand nothing catchesOSError, where the file's own exit codes give 1 to drift and 2 to a question that could not be asked: the sibling of #368, whose fix is ruled intovalidate-family.pyonly #412'sgit-not-runnable;REFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188's, adopt-project.pyexecuteexits 2 with nothing on stderr whengit filter-repofails, after both leg remotes exist:_build_the_legsswallowsCommandFailedon the premise thatrunalready reported it, andrunonly raises #171's and RULESET_HINT advisespush -u origin ...from an extracted leg whoseoriginfilter-repo removed, and says "listed above" of lines a combined log prints after it #177's late-failure reporting: once a leg repository exists, every failure flushes stdout, then printsREFUSED <step>., what failed and one notice of what the run left;mainprints the manifest'snamewith a plainprintbefore any step runs, soname: "a\ud83db"(a lone surrogate) endsmake bootstrapin aUnicodeEncodeErrortraceback, exit 1 (make: 2), and the validator that names the problem is never reached; a valid name the stdout cannot encode ends the same way #391's escaping in both roots'scripts/bootstrap.py, throughspelledandescape_unencodable_stdout, which it adds toscripts/repo_shape.py:make bootstrapno longer ends in a traceback when a manifest value holds a lone surrogate or a character its stdout cannot encode;read_pinand the pinned validate-pins.py and validate-family.py: iteratepin.get("files") or []without asking for a list, so a shape pin withfiles: 5orfiles: trueends shape-doctor.py,update-shape.py checkandapplyand the project's own validator inTypeError: ... is not iterable, exit 1, with no refusal naming the pin #290's refusal of a shape pin whosefiles:is not a list:update-shape.pyrefuses it asupdate-pin-row, both roots' pinned validators reportshape-pin-row, andshape-doctor.pyreads it as pinning nothing, wherefiles: 5endedcheck,apply, the doctor and a project's validator in aTypeError.plan-manifest-invalidvalidates the template manifest, rendered with the plan's values, before anything exists, andexecuteappends the record to the split'sproject.yamlonly later, in_commit_the_split, so that check never sees the record.There were eight conflicts, and each kept both sides:
--help.from repo_shape import, where scripts/repo_shape.py treats a quote character in the MIDDLE of a plain scalar as opening a quote, sok: [it's, a]reads["it's, a"]where its block twin reads["it's", "a"],k: it's fine # notekeeps the comment in the value, and #244 turnsk: [a"b\", c]from right (by accident) into one item #249 and scripts/repo_shape.py_scalartakes any text that starts and ends with a quote for a quoted scalar and anything else for plain text, so an unterminated"abcreads as"abc,- "a\"reads asa\, and"a" b "c"reads asa" b "c, where PyYAML refuses each #250 importdouble_quotedand this branch importspath_spelling_faultand its codes.executeclones the source with the operator'sinit.templateDirapplied, so a template holdingmodules/specandmodules/codemakes the leg mounts'--force(#200) reactivate those planted stores: both gitlinks pushed on the split branch are a stranger's commit, the VERIFICATION still saysadoption verifiedbecause it checks a mount is present and not which commit, and the recursive clone of the result exits 128 #243 ends theexecuteparagraph with "each at the commit pushed to its leg" and this branch adds its sentence about the record. Both stand, adopt-project.pyexecuteclones the source with the operator'sinit.templateDirapplied, so a template holdingmodules/specandmodules/codemakes the leg mounts'--force(#200) reactivate those planted stores: both gitlinks pushed on the split branch are a stranger's commit, the VERIFICATION still saysadoption verifiedbecause it checks a mount is present and not which commit, and the recursive clone of the result exits 128 #243's first. The docstring is not in--help.scaffold-project.py --helpsay nothing of the scaffold's two path-collision refusals (scaffold-leg-path-collides,scaffold-pin-collides) or thatspec,codeandshapecannot be pinned, so a human or agent can learn of them only by hitting them #288 moved it from 464 to 477 and this branch from 464 to 472, each with a paragraph saying why. AGENTS.md merged cleanly at 485 lines,main's 477 and this branch's eight. So the cap is 485: AGENTS.md, README andscaffold-project.py --helpsay nothing of the scaffold's two path-collision refusals (scaffold-leg-path-collides,scaffold-pin-collides) or thatspec,codeandshapecannot be pinned, so a human or agent can learn of them only by hitting them #288's paragraph is kept word for word, and this branch's follows it as "477 -> 485".adopt-project.py executeexit is to fix the plan and re-run into fresh legs, but in GitHub modeexecuteexits 2 with the split pushed whengh pr createis refused, and exits 2 afteradoption verifiedwhen the topic will not set, so following step 7 abandons a pushed adoption whose own exit is a command to run by hand #304 moved it from 477 to 481 for its four lines in step 7, and this branch's eight lines make the merged AGENTS.md 489. AGENTS.md step 7 tells an agent that a non-zeroadopt-project.py executeexit is to fix the plan and re-run into fresh legs, but in GitHub modeexecuteexits 2 with the split pushed whengh pr createis refused, and exits 2 afteradoption verifiedwhen the topic will not set, so following step 7 abandons a pushed adoption whose own exit is a command to run by hand #304's paragraph is kept word for word, this branch's follows it as "481 -> 489", and the cap is 489. Step 7 and this branch's paragraph after it are both true as they stand together.]or}is read, not refused, sok: {a"]:}reads{'a"]': None}since #284 (a quote toggle used to swallow the:}) andk: [a]]reads['a]'], where PyYAML refuses both #301's and validate-manifest.py accepts a display name holding a lone surrogate (name: "a\ud83db") and then raisesUnicodeEncodeErrorprinting it, somake validateand shape-doctor.py fail on a traceback instead of a finding while bump-leg.py and update-shape.pyapplypass the same project once #254 lands #280's tests addimport os,import subprocessandimport sys, and this branch had addedimport osandimport sys. The merge keeps all three, once each._commit_the_split, in 60a1a34. adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints onlyREFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188 puts every step after the legs exist inside onetry, with a step name and its late-failure report, and this branch appends the record there. The merge takes adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints onlyREFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188's function whole and builds the record inside itstry, under thecommitting the splitstep, where the materializer is handed what to append. Nothing else in the function differs frommain's. tests/test_adopt_late_failure.py'ssplit_argumentspasses aSplitPlan()where it passed{}, as An adopted assembly records nothing about which leg a human chose for a source submodule, so shape-doctor.py cannot verify a kept gitlink's placement and can only note it; adopt should write the plan's submodule answers into the assembly #189's first review planned for whichever of the two landed second.REFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188 moved it from 481 to 482 for its line in step 7, and AGENTS.md merged cleanly at 490 lines,main's 482 and this branch's eight. AGENTS.md step 7 tells an agent that a non-zeroadopt-project.py executeexit is to fix the plan and re-run into fresh legs, but in GitHub modeexecuteexits 2 with the split pushed whengh pr createis refused, and exits 2 afteradoption verifiedwhen the topic will not set, so following step 7 abandons a pushed adoption whose own exit is a command to run by hand #304's and adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints onlyREFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188's paragraphs are kept word for word, this branch's follows them as "482 -> 490", and the cap is 490. Step 7, as adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints onlyREFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188 words it, and this branch's paragraph after it read true together.Seven checks:
The doctor's judgement by the record is unchanged. The check uses one standard, and swaps only
shape-doctor.pyandadopt-project.pybetween the commits compared. It covers a code leg'sdocs/themewith no row, and with a row sayingcode,specorroot. It covers.cursorbeside the.gitmodulesthat registers it, recorded incode, then withvendor/libadded by hand, then recorded inspec. And it covers a real adoption: as adopted, withvendor/lib, and with its row edited tospec. The text,--jsonand--placement-planoutput are byte-identical in all 40 comparisons, for each merge that changes either file: 2f5cda9 to a01ab02, a01ab02 to e334f50, e334f50 to f38d543, 3e86200 to 27d039b, e0df56e to a557f2a, a557f2a to 70f4d29, 70f4d29 to 48bdbf9, 48bdbf9 to 4b00597, 4b00597 to 3b7cb8f, 3b7cb8f to 563ea16, 563ea16 to 78034e7, 78034e7 to 136a37f, 136a37f to 60a1a34, 60a1a34 to 120d6ed, 120d6ed to 7db009e, 7db009e to 7adcc84, and 71d3ce2 to b88a36e. The merge from f38d543 to 3e86200 changes neither file, but it does change the reader. So for it,scripts/repo_shape.pyandscripts/shape_materialize.pyare swapped as well, and the placement row and the plan file are compared: identical in all 20 comparisons. Every merge since then but 3b7cb8f, 60a1a34, 7adcc84 and b88a36e changesscripts/repo_shape.pyorscripts/shape_materialize.py, so those 20 were compared for each of the fifteen merges since then, those four included. Both files were swapped, withupdate-shape.py,scripts/bump-leg.pyandscripts/family.py, which importscripts/shape_materialize.py: identical in all 20 each time.adopt-project.py: a
.gitmoduleswith a key with no value passescheckwhen the tip holds no submodule but the history does, andexecutedies ingit fast-exportafter both legs are pushed #195's, adopt-project.pycheckaccepts a.gitmodulesvalue git's submodule reader rejects (update = bogus,update =,update = !cmd,shallow = maybe,fetchRecurseSubmodules = bogus), soplan okis followed byexecutedying ingit filter-repoafter both leg remotes exist, whether the tip or only the history holds the gitlink #215's, adopt-project.pycheckaccepts plan pathsexecuterefuses asunsafe-value(a space or a non-ASCII character), soplan okis followed by a refusal and #166's split instructions can name entriesexecutewill refuse #196's, adopt-project.pycheckpasses a plan that keeps a plain file at a directory the shape writes into (.github,contracts,scripts), andexecutedies in aNotADirectoryErrororFileExistsErrortraceback after both legs are pushed #263's and adopt-project.py: since #247 a byte that is not UTF-8 in--elected-by,--referenceor the--sourcedirectory passesplanandcheck, andexecutepushes both legs before it dies withUnicodeEncodeErrorinwrite_lf#255's refusals and the record agree. Eleven sources went throughplan,checkandexecute, undermain1f90a0f and under 4b00597, and again undermain75052cb and under 60a1a34.checkandexecutepass and refuse the same sources, with the same findings, in both:.gitmoduleswith a key with no value passescheckwhen the tip holds no submodule but the history does, andexecutedies ingit fast-exportafter both legs are pushed #195's own shape; a tip submodule besideignorewith no value; and adopt-project.pycheckaccepts a.gitmodulesvalue git's submodule reader rejects (update = bogus,update =,update = !cmd,shallow = maybe,fetchRecurseSubmodules = bogus), soplan okis followed byexecutedying ingit filter-repoafter both leg remotes exist, whether the tip or only the history holds the gitlink #215'supdate = bogusbeside a tip submodule andshallow = maybebeside one only the history holds: all refused, with no leg and no split;codebeside a filecontractsthe plan keeps in the root, where the shape writes a directory, the same submodule beside-notes.mdansweredcode, and the same with the plan'selected_byedited to hold a byte that is not UTF-8: refused asplan-root-blocks-shape,plan-unsafe-pathandplan-unwritable-value, with no leg and no split;.gitmodules: adopted, with no record;code, the same answeredspec, and one beside another that only the history holds: adopted, and only the branch writes the record. It names the tip's submodule alone. adopt-project.pyexecuteclones the source with the operator'sinit.templateDirapplied, so a template holdingmodules/specandmodules/codemakes the leg mounts'--force(#200) reactivate those planted stores: both gitlinks pushed on the split branch are a stranger's commit, the VERIFICATION still saysadoption verifiedbecause it checks a mount is present and not which commit, and the recursive clone of the result exits 128 #243's new check, that each mount is at the commit pushed to its leg, passes every adopted case on both.checkand the record both read the kept submodules as_kept_in_a_leg(_answered_legs(...))over the tip's gitlinks.executerefuses each of these findings before any leg exists and before the record is written. The doctor's placement plan carries thesubmodules:rows and has alegs:list. adopt-project.pycheckpasses a plan that keeps the source's ownshape/in the root, andplace()then writes the shape's collision copy onto it after both legs are pushed: a realshape/README.mdis overwritten whileexecuteprints "nothing was overwritten" and endsadopt-lost, and ashape/Makefile/directory or a file namedshapeis a traceback #262's and adopt-project.pycheckpasses a plan that keeps a plain file at a directory the shape writes into (.github,contracts,scripts), andexecutedies in aNotADirectoryErrororFileExistsErrortraceback after both legs are pushed #263's check is asked only beside alegs:mapping, so_leg_findings(plan, None)still accepts that plan, astest_a_submodule_in_the_other_leg_from_its_record_is_misplacedasserts.AGENTS.md step 7 tells an agent that a non-zero
adopt-project.py executeexit is to fix the plan and re-run into fresh legs, but in GitHub modeexecuteexits 2 with the split pushed whengh pr createis refused, and exits 2 afteradoption verifiedwhen the topic will not set, so following step 7 abandons a pushed adoption whose own exit is a command to run by hand #304's exits keep the record._commit_the_splitwrites the record into the split commit before the split is pushed, and it is the same text at 70f4d29, 48bdbf9 and 4b00597. AGENTS.md step 7 tells an agent that a non-zeroadopt-project.py executeexit is to fix the plan and re-run into fresh legs, but in GitHub modeexecuteexits 2 with the split pushed whengh pr createis refused, and exits 2 afteradoption verifiedwhen the topic will not set, so following step 7 abandons a pushed adoption whose own exit is a command to run by hand #304 changed only whatexecutedoes once the split is pushed. A source with a submodule atvendor/b, answeredcode, was adopted in GitHub mode with a stubgh, undermain1f90a0f and under 4b00597, and again undermain75052cb and under 60a1a34:gh pr createrefused, both exit 2 and printadoption verifiedbeforeOpen the pull request by hand;gh repo editrefused, both exit 2 and printSet the topic by hand;In all three, only the branch's pushed split carries the record.
adopt-project.py: a failure AFTER both legs are pushed (the split-branch push, the mount, the split commit) prints only
REFUSED <step>with no notice of what already exists and no stdout flush, so the combined log shows REFUSED before thebare …lines and the re-run meetsleg-remote-existsunexplained #188's late failures keep their one account. A source with.cursoransweredcodewas adopted with apre-receivehook in the source refusing the split branch, so the run got as far as committing the split with the record in it, and then failed to push it. Undermain75052cb and under 60a1a34 the merged stdout and stderr are the same, byte for byte with paths and hex masked: exit 2, the run's stdout first, then oneREFUSED pushing the split branch., the command and its output, and one notice. Neither prints a traceback oradoption verified. Only 60a1a34's split commit, left in the work directory, carries the record.shape-doctor.py calls an assembly root whose
project.yamldoes not parse (a merge conflict) NOT A SHAPE ROOT, exit 2, and its NEXT line tells the reader to adopt that assembly root in place while the Triad row calls it a single repository #205 waits for a record that does not load. A conflict marker, a tab or a byte that is not UTF-8 inside thesubmodules:block makesproject.yamlfail to load. The verdict is thenINVALID (manifest), exit 1. The placement row isn/aand waits on themanifestrow, so no submodule is judged.--placement-planrefuses, exit 3, and writes nothing. shape-doctor.py calls an assembly root whoseproject.yamldoes not parse (a merge conflict) NOT A SHAPE ROOT, exit 2, and its NEXT line tells the reader to adopt that assembly root in place while the Triad row calls it a single repository #205's gate runs incheck_placementbefore the record is read.No path the record work names can break a line. The record work adds no finding, refusal or print to
adopt-project.py, so adopt-project.pycheckandexecuteprint a source path raw in a FINDING, so a\r,\n,\x85, U+2028 or ESC in a name overwrites, splits or escapes the line on a terminal, whereplan's questions spell the same path with_spelled#212 has nothing of it to respell. The project'svalidate-manifest.pywrites every recorded value in its findings withrepr. The doctor writes a row's names throughascii_text, and reports a name holding a control character or a byte that is not UTF-8 byrepr, never judging it. A code leg kept submodules named with a carriage return, a line feed, an ESC, U+0085, U+2028, a no-break space, a backtick and a backslash, with a record putting each in the other leg. No carriage return, ESC, U+0085 or U+2028 reached the validator's output, the doctor's text, its--json, its placement-plan run or the plan file raw. The names holding a carriage return, a line feed or an ESC were listed byreprand not judged. The other five were judged MISPLACED, and the plan carries them exactly.The record keeps
double_quoted.submodule_recordis the same text at 2f5cda9, a01ab02, e334f50, f38d543, 3e86200, 27d039b, a557f2a, 70f4d29, 48bdbf9, 4b00597, 3b7cb8f, 563ea16, 78034e7, 136a37f and 60a1a34, and M24, the record spelled byy, is killed.Targeted runs at 60a1a34 (67 files).
With git-filter-repo on PATH:
4116 passed, 9 skipped.With it hidden:
3957 passed, 168 skipped. 159 of the skips aregit filter-repo is not installed.Each run is the same 4,125 tests. The other nine skips, in both runs, are:
tests/test_adopt_plan.py's MedxEHR-backed test (SHAPE_MEDXEHR_CLONEis unset);tests/test_stdout_encoding.pycase about Windows' own default;tests/test_shape_doctor.pycases about the PowerShell line;tests/test_update_shape_add_symlink.pycases about Windows junctions;tests/test_bump_leg.pycase that needs a disk that ignores case.The last eight run only on Windows, or, for the disk that ignores case, on macOS and Windows.
Four of the files reach
scripts/repo_shape.pyandscripts/shape_materialize.pyonly throughscripts/family.py,scripts/bump-leg.pyorupdate-shape.py: tests/test_family_add_excludes.py, tests/test_family_member_path.py, tests/test_bump_leg_rollback.py and tests/test_update_shape_add_symlink.py. tests/test_adopt_github_exits.py, tests/test_doctor_legs_members_next.py, tests/test_pin_rewrite_layout.py and tests/test_bump_leg.py aremain's new or changed tests from #304, #276 and #323, which changedadopt-project.py,shape-doctor.pyandscripts/shape_materialize.py. The seven after them aremain's tests, from the merges since 4b00597, of what reaches this branch's files: #258's and #261's manifest values, #301's flow brackets, the validator rows of #278, #297 and #298, #366's member names, #348's AGENTS.md codes and #289's unreadable pins. The last nine are #188's tests/test_adopt_late_failure.py and tests/test_adopt_filter_repo_failure.py, andmain's tests of #292's git reads, #326's dry run and refusals, #330's member ids, #333's landing, #383's unopenable files and #412'svalidate-pins.pywithout git. Each of these reachesadopt-project.py,scripts/repo_shape.pyorscripts/shape_materialize.pythrough the file it tests.tests/test_shape_doctor.pyis in the set because the doctor feeds placement plans through adopt'sPlanand_leg_findings.tests/test_cli_reference.py, also in the set, fails when docs/cli.md andscripts/render-cli-reference.pydisagree.CI passed at 48bdbf9 and at 563ea16 on Linux, macOS and Windows. b88a36e is a new head, so CI runs again on it. Besides the runs above, at 60a1a34:
The re-verify ran 52 files at 563ea16 with git-filter-repo:
3414 passed, 8 skipped. tests/test_agents_md_codes.py, new in 45e01f4, passes: #189's AGENTS.md passages backtick no code-shaped token.Reviews
Lane openRepoShape-2 reviewed e167d8b: CHANGES NEEDED on one test argument, and mergeable with nits once that was fixed (0 P1, 1 P2, 3 P3).
_leg_findings()missingtree(TypeError) after main a5b09c3None, as main's other placement-plan callers do. Its revert is M1.repo_shape.double_quoted, deleting_quoted../x,x/,a//b,a\bacceptedpath_spelling_fault, inrepo_shape.py, refuses./x,x/,a//banda/./b.a\bis accepted, against the review, with evidence. Git recordsvendor/a\bon POSIX, a real adoption records it and the doctor holds the gitlink to that row (recorded, then MISPLACED once moved), so refusing it would makeexecute's own manifest INVALID.leg_path_problemis not reused, because it refusescafé.okon a real adoption.gitmodules. 9b2c36f corrects it to answer only where the record places EVERY submodule the leg keeps. At 90608b8 a submodule added by hand beside a recorded one dropped the question, and the row said the record answered it. e0df56e also requires that the doctor read every name in the leg (F1 of the second review, below). No verdict moves._verifydoes not cross-check the rows against the legstest_shape_doctor.py:3714does not assert the row's statusok, no question, and the clause.recorded twice. A first row whoseleg:the validator refuses now decides nothing, and the second does not stand in for it. Its reverts are M16 and M17.adopt-project.pywas_commit_the_split: the merge takes #188's function whole and builds the record inside itstry, under thecommitting the splitstep, andtest_adopt_late_failure.py'ssplit_argumentsnow hands it aSplitPlan()withentries = ()where it handedplan={}.Also in the fix round: 400812a spells the test's escaped names by their code points (
\u00a0,\u3000,\u200b,\u00ad,\u2028), where five were raw invisible characters. The values are unchanged.The second review, of 3e86200, found one BLOCKER and eight nits. Every finding of the first review is fixed or deferred, as above.
.gitmodulesbeside a submodule whose name the doctor cannot readgitmodules_the_record_answersanswers nothing for a leg holding such a name. The doctor sets a name with a control character, or a byte that is not UTF-8, aside before it reads its mode, so a submodule kept under one was never held to a row. In the review's probe, such a submodule with no row, with a row sayingspec, and with a row sayingcodeeach made the row print the answered clause and drop the question. Each keeps the question now, asmaindoes. A leg holding such a name is anoteeither way, so no verdict moves. Its revert is M29.repo_shape.py, and what the row needs to beok.bytes, a test docstring, a question for the follow-up issue, and a Sonar count.Not in this PR
Keeping the record current (P3-2's maintenance half). Nothing writes a row after the adoption, so a stale row is silent, and a submodule moved on purpose stays MISPLACED until somebody edits the manifest. A follow-up issue should decide:
_verifycross-checking the rows (nit 2). On every pathexecutetakes, the rows and the split come from one_answered_legsreading of one plan, andgit filter-repokeeps paths. So the rows match the legs by construction. A check would need:_verify;The doctor run AGENTS.md prescribes after every procedure already reports a mismatch as MISPLACED.
Pre-existing on
main, found while settling nit 2. Take a source whose ownproject.yamlis answeredroot:shape/project.yaml;executestill printsadoption verified;NOT A SHAPE ROOT, exit 2.Measured on 54aa69a and on 4835fb4. The record goes with the shape's manifest, wherever that lands.
scripts/repo_shape.py: the double-quoted reader that #247 adds (fix/adopt-plan-linebreak-names, not yet on main) resolves
\x00inside a block-scalar KEY, and_parse_mappingsplits the NUL-joined sentinel record, so"a\x00b": |reads as the keyawith the bodyb<NUL>textwhere main reads the literal keya\x00b#248 (an escaped NUL in a block-scalar key split the reader's sentinel record) is fixed onmainby c3f4987 (scripts/repo_shape.py: a block scalar's record carries its key and its body as a pair instead of NUL-joined text, so"a\x00b": |no longer reads as the keyawith the bodyb<NUL>text, and a line that begins with a raw NUL andBLOCKno longer reads as a block scalar nobody wrote (#248) #265), which this branch has merged. The record writes no key from data, and it reads back through that reader (see the round trip under Tests).Closes #189
🤖 Generated with Claude Code