Repository navigation
repo_shape.py: a plain scalar opening with an indicator, an empty flow entry and a single pair in a flow sequence are refused, so k: ] is not the text ], pins: [,a] not ['a'] and k: [a: b] not the string 'a: b' (#392, #393) - #635
Merged
Merged
Conversation
…w entry and a single pair in a flow sequence are refused, so `k: ]` is not the text `]`, `pins: [,a]` not `['a']` and `k: [a: b]` not the string `'a: b'` (#392, #393) The YAML subset reader read three kinds of text where PyYAML refuses the document, or reads another thing. Each is now a `YamlError`, which `load_yaml` turns into `REFUSED yaml-unparsable` naming the file. That is the reader's existing refusal, in the words of #252 and #301. #393, a block value or item. `_scalar` reads every plain value, block and flow, and its one test of the first character was `&*!`. It now also refuses a plain scalar that opens with `]`, `}`, `,`, `@`, a backtick or `%`, or is a lone `?` or `:`. `_parse_mapping` refuses a value that is a lone `-`. That one is refused there and not in `_scalar`, because a flow collection reads `[-]` and `{a: -}` as the text `-`, as PyYAML does. #392, three classes in a flow collection: - An empty entry. `_split_flow` dropped every empty part, so `k: [,a]` read `['a']` and `k: {,}` read `{}`. It now drops the one empty part after a trailing comma (`[a,]`) and refuses any other. - An entry that opens with an indicator. The new `_refuse_entry_indicator` refuses an item, or a mapping entry whose key it opens, that opens with `:`, `#`, `@`, a backtick or `%`, or with `- ` or `? `, or is a lone `?`. It runs in `_flow_value` for a sequence's items and in `_split_key_flow`, where it replaces #252's `? ` test and keeps its words for `?`. - A single pair in a flow sequence. `_flow_value` refuses an item that `_split_key` splits, after #301's bracket check, so `k: [a: [b]]` keeps #301's words. The choice for `[a: b]`: refused, not read as YAML reads it (`[{'a': 'b'}]`). No file this reader serves can hold one: - Every flow sequence in them is a list of names: `neutral_product_pins`, a plan's `pins` and `allow_empty_legs`, `also_matches` and `referent_chain`. There a mapping reaches the tool as a name; `validate-manifest.py`'s `str(pin)` makes it the pin `"{'openGlass': 'x'}"`. - Every list of mappings (`legs`, `members`, `files`, `submodules`, a plan's entries, a policy's `rules` and `families`) has rows of two keys or more, which no single pair can spell. So `pins: [openGlass: x]` is a typo, and it is refused by name. The header comment lists it with explicit keys, as a construct refused by design. Kept as they read: the indicator later in a scalar (`k: a]`, `k: a, b`), every quoted scalar, `-a`, `?a` and `:a` in a block, and `[-]`, `[-a]`, `{-: 1}`, `{a: -}` and #252's `{?a: b}` in a flow. Not in either issue, and left as they read, PyYAML refusing: - `?`, `:` or `-` followed by a space and more text as a value, block or flow (`k: - a`, `{x: ? a}`). `tests/test_yaml_flow_mappings.py` pins `{x: ? a}` to read as its block twin `x: ? a`, "a different change, the same one for the two forms if it is made". - A flow value opening with `:` and a character (`{a: :b}`). - A `?` inside a flow scalar (`[e?]`, #252's `{a: b?}`). - `|` and `>`, which `_opens_block_scalar` reads as text by design. - A block key that opens with an indicator (`@a: 1`, `- :`). The reader's own copy is the only one in this tree, and nothing here records its digest. A project takes it through `update-shape.py`, as an `upstream-changed` row. Reach. Every string literal in `tests/` and every tracked YAML file was read by the reader at main 8f453d0 and by this one. No tracked YAML file changes (0 of 15). The 636 inputs whose answer changes are prose that happens to parse (messages and docstrings); none is a fixture. The writers that ask this reader how a bare value reads (`adopt-project.py`'s `y`, the scaffold's pin entries, `family.py`'s id) catch a `YamlError` and double-quote the value. `scaffold-project.py --org @x --dry-run` is refused `scaffold-manifest-invalid` before anything is created, as on main, which refused it as a validator finding. Tests: `tests/test_yaml_indicator_openers.py`. - Every row both issues list, and the same in the other places each can stand. - The controls, and every refusal's words in full. - Three files through `load_yaml`. - A scaffolded project's own `validate-manifest.py`, exiting 2. - Each row compared with PyYAML where it is installed. - A seeded fuzz of 355 small block and flow documents. On main it finds 215 read where PyYAML refuses and 15 misread; here it finds none. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR tightens the custom YAML subset reader so indicator-opening scalars, non-trailing empty flow entries, and single-pair flow-sequence mappings are refused instead of silently misread, while preserving supported legacy cases. It adds detailed regression, PyYAML comparison, fuzz, refusal-message, file-loading, and validator integration tests. Flow diagram for YAML subset refusal rulesflowchart TD
Input[YAML input] --> Reader[scripts/repo_shape.py]
Reader --> Block[Block scalar checks]
Reader --> Flow[Flow collection checks]
Block --> Indicator{Indicator-opening scalar?}
Indicator -->|Yes| Refuse[YamlError]
Indicator -->|No| Parse[Parse supported value]
Flow --> Empty{Non-trailing empty entry?}
Empty -->|Yes| Refuse
Empty -->|No| Pair{Single-pair mapping in flow sequence?}
Pair -->|Yes| Refuse
Pair -->|No| Parse
Refuse --> Load[load_yaml]
Load --> Result[REFUSED yaml-unparsable]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
This was referenced Oct 16, 2026
…lock key and runs on three seeds, a lone `?` flow entry is pinned, and the header and the `[a: b]` docstring say only what they cover (#392, #393) R392's review of #635 (APPROVE WITH NITS) found four things to correct, all in comments and tests; no line of the reader's code changes. N1, tests/test_yaml_indicator_openers.py: `fuzz_documents` could draw `- :` (the context `- {}` with the opener `:`), which this reader reads as `[{'': None}]` and PyYAML refuses. It is the empty block key, outside both issues, and on seeds 1 and 3 the fuzz went red on it. It is now dropped after the draw (`EMPTY_BLOCK_KEY`), so every other document of a seed is the one it was, and the module docstring names it among what is not in these issues. The comment that said "nothing outside these issues is drawn" is corrected, and the test runs on seeds 392393, 1 and 3 (`FUZZ_SEEDS`). N4, the same file: `k: {?}` is added to `FLOW_ENTRY_ROWS` (`lone-question-mark-entry`), so dropping `or text == "?"` in `_refuse_entry_indicator`, which turns the explicit-key words for `{?}` into `flow mapping entry without a key`, now fails. PyYAML reads `{?}` as an explicit key, so the row joins the two that the PyYAML comparison skips. N3, scripts/repo_shape.py, `_flow_value`'s docstring: a plan's `pins` is rows of `product`, `repository`, `commit`, `tree_sha256` and `digest_source` since aa4e1f5, not a list of names, so it moves to the lists of mappings whose rows hold two keys or more. N5, scripts/repo_shape.py, the header comment: it said that an indicator opens no plain scalar, which `k: - a`, `k: : a`, `@a: 1` and `k: |a` still contradict. It now names the indicators this change covers (`]`, `}`, `,`, `@`, a backtick, `%`, and a lone `?`, `-` or `:`) and what still reads. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
brettheap
marked this pull request as ready for review
October 16, 2026 11:30
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 10 hours and 24 minutes by commenting |
14 of 17 tasks
brettheap
pushed a commit
that referenced
this pull request
Oct 16, 2026
…ies keyed by pin_product_key, the key the chain readers use (#444 x #610) #638 keys every reader of a chain by `pin_product_key` (`lower()`), and `_chain_link_findings` now finds a holder's remedy by it too, as `_link_remedies` keys it. Keyed by `casefold()`, the chain `openß, openss, openInk` wrote one entry for its two links, so the first link, `openß`, was offered the later link's words (that the scaffold reads no source for it) instead of its own `--pin-source`. New test: test_two_pin_files_one_casefold_are_each_offered_their_own. The one conflict, in `scripts/repo_shape.py`, keeps both `_chain_link_findings` docstring paragraphs and #638's key with this branch's `LinkPins` return. R444's N2: the naming gate's `LinkSources` docstring cited `validate-pins.py` by line numbers that no longer held the lines, and now names `PinSources.__init__` and `resolve_neutral_pin_source`. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 16, 2026
…h, the `switch -c` remedy says to give it another name, or go back only if it holds work not merged yet, and the docs say `family/add-<Project>` (#607) (#637) ### Fix round 1 (rv607) Review rv607 of 6212c31 said CHANGES NEEDED: 0 blockers, 2 should, 7 nits. This round is one new commit, 66b1043, on top of 6212c31. No amend, and main is not merged in. Each point: - **S1, the sentence.** It sent a person back to the old branch first. That is right only where the branch holds work not merged yet. `BRANCH_LEFT` now puts another name first and going back second (the text is under "L2-46" below). It is word for word the same in `scripts/family.py`, in the pinned `validate-family.py` and in the pinned test constant, and the comments that described the old order are rewritten. Two new tests type the printed line in the states the review probed: `main` has moved since the earlier act, and a commit was made on a detached HEAD. - **S2, a guard for every `_switch_line` remedy.** A new test reads the validator's source with `ast` and fails any remedy that names `_switch_line` whose prose does not end with `BRANCH_LEFT`. On a merge of this head with #634 it fails at `_lockstep`, which is the point: whichever of #637 and #634 lands second adds the sentence there. #634's lines are not on this branch and are not touched. - **N1.** Under "Left out". - **N2.** The commit body of 6212c31 said neither tool can print the one line that works by asking first. That holds for the validator only. `family.py` could ask; it says the validator's words so the two tools keep one rule. Decision 1 says so, and the new commit's body corrects the old one. - **N3.** The merge table is refreshed: current origin/main, #620, #622, #626, #630 to #636, #638, #639 (lane 5's #611) and #634 (S2). - **N4.** Decision 1 no longer says the typed line works "in every shell". It says what a person types is "the printed line with one word changed (the branch name, or `-c` taken out)", so it keeps the printed quoting, and that the tests type both changes as printed. - **N5.** Decision 3 now says the case of the branch name "does not matter to `add`", and adds the note about case-insensitive disks. - **N6.** The `init` NEXT STEPS test read the lines with a regex whose `--family-root \S+` breaks on a root with a space in it (`C:\Users\Jane Doe`, or any `tmp_path` with a space). It now reads the words with `host_words`, as the NEXT STEPS test beside it does. Run with a space in the base temp directory, the old test fails with `AttributeError: 'NoneType' object has no attribute 'group'` and the new one passes. - **N7.** "Three existing tests take the new text" was wrong. It is two test functions plus `GITLINK_REMEDY`, a constant four cases share. Fixed under "Tests". The one new test that passes on BASE is disclosed there too. ### Fix round 2 (rv607b) The re-verify rv607b of 66b1043 said APPROVE WITH NITS: 0 blockers, 0 should, 5 nits. This round is one new commit, a277695, on top of 66b1043. No amend, and main is not merged in. It changes comments and docstrings only, so no behaviour moves and no mutation row changes. - **Nit 1.** Two comments called another name "right in every state". It is not: where the old branch holds work not merged yet, another name leaves that work behind. The comment above `BRANCH_LEFT` in `scripts/family.py` and the one in the pinned `validate-family.py` now say a new branch starts where the holder stands now, "which works in every state". The three `BRANCH_LEFT` texts are unchanged and still equal. - **Nit 3.** The guard's docstring said it finds each `_switch_line` call "however it is passed on". A switch line moved into a variable would defeat that. It now says "wherever the call stands inside the string expression that is passed on". - **Nit 4.** The N4 line above now quotes decision 1 as written. - **Nit 5, the docstring half.** The docstring of `test_another_name_works_where_main_has_moved_since` said going back to the old branch conflicts, but the test does not assert that. It now says the test shows the other name works, and that the conflict on the old branch is shown by rv607's probe and decision 2's table. No assertion is added. - **Nit 2, left as written.** The guard's control counts stay: each case fails loudly with a clear message. Checks on a277695: the diff to 66b1043 adds ten lines and removes seven, in three files, all comments and docstrings, and the `ast` of each file is equal once docstrings are blanked. The four files `test_family_switch_branch_exists.py`, `test_validate_family_branch_step.py`, `test_validate_family_unnamed_mounts.py` and `test_family_tracking_branch.py` give `96 passed in 51.34s`, and `test_repo_hygiene.py` gives `65 passed in 1.39s`. ### L2-46, the fix `family.py add`, `bump` and `remove` refuse the holder's tracking branch with `git -C <holder> switch -c family/<act>-<member>` (#487). The holder's `validate-family.py` names the same line before each `add` and `remove` it prints (#539, landed as 54041cc). If an earlier act left that branch, the line fails with `fatal: a branch named 'family/add-atlas' already exists` (exit 128), and neither tool said what to do next. Both tools now add one sentence, in the same words (`BRANCH_LEFT` in each file): > If `git switch -c` says that branch already exists, an earlier act left it: give the branch another name, which starts it where the holder stands, or, only if that branch holds work not merged yet, the same command with `switch` in place of `switch -c` goes back to it and keeps every commit on it. - `scripts/family.py`: the sentence comes on its own line, right under the `switch -c` line, in the `family-on-tracking-branch` and `family-holder-detached` refusals (`_checked_branch`), and in the dry run's `branch` lines (`_branch_said`). - `templates/family-root/scripts/validate-family.py`: the sentence ends the remedy text of `member-gitlink-absent`, `member-duplicate-id` and `member-gitlink-unnamed`, the three remedies that print `_switch_line`. The command lines under them are unchanged. ### L2-45, rides along AGENTS.md, the holder's README, its AGENTS-shape.md and `init`'s NEXT STEPS spelled the branch `family/add-<project>`, right beside `--member <org>/<Project>`. All four now say `family/add-<Project>`, the one placeholder the validator already uses for both (`PROJECT_PLACEHOLDER`, #539). **1. What the remedy prints when the branch exists: one sentence that names the way out, not a second command line.** - *One line that works in both states* (`git switch X 2>/dev/null || git switch -c X`): rejected. On Windows, `family.py` quotes its lines for PowerShell (`shell_line` uses `os.name`; README.md:155-158 says so). Windows PowerShell 5.1 has no `||`; `&&` and `||` arrived in PowerShell 7, and `family.py`'s own `_print_next_steps` docstring says 5.1 rejects `&&`. PowerShell also has no `/dev/null`. And `2>/dev/null` would hide the real reason a `switch` fails, such as a changed file it would overwrite. - *The tool checks and prints the one line that works*: rejected for the validator. Its report is read in CI: the holder's `validate.yml` runs `python3 scripts/validate-family.py` (and `--no-members`) in a runner's checkout, which holds none of the person's branches. `family.py` could check, but then the two tools would follow two rules, and one rule in the same words was the aim. So `family.py` says the validator's words; it is not that it cannot ask. - *Two printed lines, one per state*: rejected. In the validator, each indented line is a step to run in order, and going back to the branch replaces step one; it is not another step. In `family.py`, a new `git switch` line would also be quoted for PowerShell on Windows. README.md:155-158 lists exactly which `family.py` lines are quoted that way (`NEXT`, `switch -c`, `add`, `git rm -r --cached`). That sentence would become false, and fixing it would mean editing README § "Starting a project in a new organisation". Seven handbook parts record that section's hash (`grep -c` gives 7). - *Chosen*: a sentence that names both ways out. It prints no new command, so nothing new needs quoting, and the text is the same in both tools. What a person types is the printed line with one word changed (the branch name, or `-c` taken out), so it keeps the printed quoting. The tests type both changes as printed (tests below); none of them runs a shell, and Windows and macOS are not checked (see "Left out"). **2. Another name first, going back second (S1).** The reviewer's probes, and mine, in real local holders (a holder `Orbit`, members `Atlas` and `Beacon`, local bare remotes, the `gh` stub first on PATH): | State of the old branch `family/add-atlas` | `git switch family/add-atlas` (going back) | `git switch -c family/add-atlas-2` (another name) | |---|---|---| | No commit on it, `main` has not moved (rv607) | works | works | | Holds a commit of its own, not merged yet (mine: `probe-atlas.sh`, `probe-b.sh`) | works; `add` then commits on top of it, and the earlier commit is the parent | works, but the earlier commit stays only on the old branch (`git branch --contains` names `family/add-atlas` alone) | | `main` has moved: a later member (`Beacon`) was squash-merged (mine: `probe-d.sh`; test `test_another_name_works_where_main_has_moved_since`) | `add` exits 0, but `git merge-tree --write-tree main family/add-atlas` exits 1 (`CONFLICT (add/add)` in `.gitmodules`, `CONFLICT (content)` in `family.yaml`), and an untracked `members/Beacon` is left (`warning: unable to rmdir 'members/Beacon'`) | `add` exits 0, `git merge-tree --write-tree main family/add-atlas-2` exits 0, and the validator says `2 member(s)` | | Merged, then the member removed and that merged too; the old branch is still local (rv607's probe, and mine: `probe-c.sh`) | `add` says `member-already-present`, whose remedy points at `bump` and `remove`: a dead end | `add` exits 0, once the object store `remove` leaves behind is deleted, as its NOTE says (either choice needs that) | | Detached HEAD with a commit made on it (mine: `probe-detached.sh`; test `test_another_name_keeps_a_commit_made_on_a_detached_head`) | `Warning: you are leaving 1 commit behind, not connected to any of your branches`, and `git branch --contains` names no branch | the commit stays: `git branch --contains` names the new branch | So going back is right only where the branch holds work not merged yet, and another name is right in every state but that one. The sentence says so, in that order. **3. The person's earlier work on that branch is never discarded.** `git switch <branch>` keeps the branch's commits. It refuses to switch when that would overwrite a changed file. The act then commits on top of what the branch holds; the tests check that the earlier commit is the parent of the act's commit. "Give the branch another name" leaves the old branch alone. No printed line names `switch -C` (which resets a branch), `branch -D`, `reset` or `--discard-changes`; a test checks that too. **4. The placeholder: `<Project>` everywhere.** - The validator already said `family/add-<Project>` beside `--member <org>/<Project>` (#539's fix round, pinned by `tests/test_validate_family_unnamed_mounts.py`). It is the only spelling that is one placeholder for both values. - The other option, a sentence saying the branch word is the name in lowercase, does not fit: AGENTS.md is at its cap, 492 of 492 lines (`tests/test_repo_hygiene.py:633`). This change keeps it at 492. - The case of the branch name does not matter to `add`. `_checked_branch` takes any branch but the tracking one, so `family/add-IRQS`, the doc line filled in as written, is a branch `add` commits on (test). Where the tools know the name, they still print it lowercased (`_suggested_branch`). On a case-insensitive disk (macOS, Windows) `family/add-Atlas` and `family/add-atlas` can be one loose ref there. That is untested: CI is Linux only. **5. Pinned copies.** This changes two pinned holder files: `templates/family-root/scripts/validate-family.py` and `templates/family-root/AGENTS-shape.md` (`scripts/shape_materialize.py` lists the pinned set). Each holder takes them as two `upstream-changed` rows on its next `update-shape.py` run. The holder's README is rendered once and is not pinned. No test pins those files' bytes; the tests that compare them read the template at run time. Fix round 1 changes only `validate-family.py` of the two. New file `tests/test_family_switch_branch_exists.py`, 25 tests (21 before the fix round). Four are new in the fix round: - `test_another_name_works_where_main_has_moved_since` (S1, state (d)): `family/add-irqs` is left at the `main` of its day; a later member (`IRXS`, scaffolded in the test) is added on a branch and squash-merged onto `main`, so `main` has one new commit; the refusal's `switch -c`, typed as printed, fails with `already exists` (exit 128); the same line with another name works; `add` exits 0, `git merge-tree --write-tree main <branch>` exits 0, and the validator passes. - `test_another_name_keeps_a_commit_made_on_a_detached_head` (S1): the refusal is `family-holder-detached`; after the printed line with another name, `git branch --contains <the commit>` names the new branch and no other. - `test_every_remedy_that_names_a_switch_line_ends_with_the_sentence` (S2, the guard) and `test_the_guard_sees_a_remedy_left_without_the_sentence` (its control: the source with the sentence taken out of any one remedy fails it, and so does a fourth remedy built by a helper as `_lockstep` builds its, until the sentence ends it). The other 21: - `test_the_refusal_typed_as_printed_puts_the_act_on_its_branch` [add, bump, remove] x [no-branch, branch-left]: the refusal, then its line typed as printed. With no branch, the `switch -c` makes one. With a branch an earlier act left, holding a commit of its own, the `switch -c` fails with `fatal: a branch named '<branch>' already exists` (exit 128) and changes nothing, and the same command with `switch` in place of `switch -c` goes back to that branch. The act then commits on top of the earlier commit, and NEXT pushes that branch. - `test_a_dry_run_on_the_tracking_branch_says_it_too` [add, bump, remove]. - `test_a_missing_gitlinks_remedy_typed_as_printed`, `test_a_duplicate_ids_remedy_typed_as_printed` (with the `remove` run as printed) and `test_an_unnamed_gitlinks_remedy_typed_as_printed`, each in both states. - `test_the_two_tools_say_the_same_words`. - `test_each_doc_spells_the_branch_as_its_member` [AGENTS.md, AGENTS-shape.md, holder README], `test_init_next_steps_spell_the_branch_as_its_member` (read with `host_words` now, N6), and `test_the_branch_filled_in_as_written_is_one_add_commits_on`. Two existing test functions take the new text: `test_init_next_steps_make_the_branch_before_the_add` (`<Project>`) and `test_a_missing_gitlink_remedy_makes_the_branch_and_names_the_holder` (the remedy line). `GITLINK_REMEDY` in `test_validate_family_unnamed_mounts.py`, a constant four cases share (`test_a_member_mounted_by_hand_is_two_findings` x 3 and `test_a_removed_row_is_named_by_its_gitlink_and_its_section`), ends with the sentence. The sentence is pinned once, as text, in `tests/test_validate_family_branch_step.py`. Before and after, with the four changed test files copied in (`run9.sh`, the `gh` stub first on PATH): - On BASE 396c0ff: `30 failed, 66 passed` in the four files. 24 of the 25 new-module tests fail. The one that passes, `test_the_branch_filled_in_as_written_is_one_add_commits_on`, is the evidence for decision 4: it shows behaviour this PR keeps. - On 6212c31: `23 failed, 56 passed` in the three files that pin the sentence. Both new S1 tests fail there, where they assert the new wording. The two guard tests pass there: they check where the sentence stands, not its words. - On the head 66b1043: the new module alone, `25 passed`. ### Targeted runs on the head 66b1043, BASE 396c0ff - Set A, 29 files: every `tests/test_family*.py` and `tests/test_validate_family*.py`, plus `test_bump_leg.py`, `test_bump_leg_dry_run_tracking.py`, `test_bump_leg_dry_run_truth.py`, `test_bump_leg_paste_lines.py` and `test_update_shape_branch_any_directory.py`. Result: `1016 passed, 20 skipped in 511.01s`. - From set B: `test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_readme_sections.py` and `test_cli_reference.py`. Result: `210 passed in 15.73s`. The fix round changes no template other than `validate-family.py`'s text, so the rest of set B (the 27 files of the first round) is not re-run. - Every skip is a platform skip that was there before this change (a case-ignoring disk, a Windows default, powershell.exe), or a template-quoting skip in `test_family_init_values.py`. A real holder `Orbit`, made with `family.py init`, and a member `Atlas`, scaffolded with `scaffold-project.py --local-remote-dir`. - **On BASE 396c0ff** (first round): `add` refuses with `switch -c family/add-atlas`. Typed as printed, that gives `fatal: a branch named 'family/add-atlas' already exists`, exit 128, and the remedy says nothing more. The validator's `member-gitlink-absent` remedy prints the same line, with the same result. - **On the head, an unmerged act** (`probe-atlas.sh`, a branch holding a commit of its own): the refusal prints the new sentence. The same command with `switch` for `switch -c` gives `Switched to branch 'family/add-atlas'`, exit 0, at the earlier commit. `add` then exits 0, its NEXT pushes `family/add-atlas`, and its commit's parent is the earlier commit. The validator carries the same sentence. - **On the head, `main` moved** (`probe-d.sh`): the printed line fails (128). With another name: `add` exits 0, `git merge-tree --write-tree main family/add-atlas-2` exits 0, `family ok: Orbit (orbit), 2 member(s)`. Going back instead: `add` exits 0, and `git merge-tree --write-tree main family/add-atlas` exits 1 with the two conflicts in decision 2. - **On the head, a detached HEAD with a commit** (`probe-detached.sh`): the printed line fails (128). With another name, `git branch --contains` names `family/add-atlas-2`. Going back with `switch`: git warns it is leaving 1 commit behind, and no branch contains it. No row needs real GitHub. Each change below was made alone in a scratch clone of the head, then the new module was run (and, for the validator changes, the two other files that pin the sentence), and the file was put back. Every change that alters behaviour makes at least one new test fail: | Change | Result | |---|---| | `family.py` `BRANCH_LEFT` back to the first round's words | 12 failed: the six refusal cases, the three dry-run cases, both new S1 tests, `test_the_two_tools_say_the_same_words` | | `family.py` `BRANCH_LEFT` one clause dropped (`only if`) | 12 failed, the same tests | | `_checked_branch` remedy with no sentence | 8 failed: the six refusal cases and both new S1 tests | | sentence printed above the `switch -c` line, not under it | 8 failed: the same eight | | `_branch_said` (dry run) with no sentence | 3 failed: `test_a_dry_run_on_the_tracking_branch_says_it_too` [add, bump, remove] | | `_switch_command` prints `switch -C` | 11 failed, both new S1 tests among them: the printed line no longer fails `already exists` | | validator `BRANCH_LEFT` back to the first round's words | 12 failed in the three files, `test_the_two_tools_say_the_same_words` among them | | validator `member-gitlink-absent` with no sentence | 5 failed: its two typed-as-printed cases, the remedy test, the guard and its control | | validator `member-duplicate-id` with no sentence | 4 failed: its two typed-as-printed cases, the guard and its control | | validator `member-gitlink-unnamed` with no sentence | 8 failed: its two typed-as-printed cases, the four `GITLINK_REMEDY` cases, the guard and its control | | **S2:** a fourth remedy added that names `_switch_line` and does not end with the sentence (built by a helper, as #634's `_lockstep` does) | 2 failed: the guard and its control, and nothing else in the three files | | **S2:** the same fourth remedy ending with `{BRANCH_LEFT}` | 79 passed | | `init` NEXT STEPS branch back to `<project>`; its `--member` lowercased | 1 failed each: `test_init_next_steps_spell_the_branch_as_its_member` | | AGENTS.md, AGENTS-shape.md, holder README: branch back to `<project>` (and AGENTS.md's `--member`) | 1 failed each: `test_each_doc_spells_the_branch_as_its_member` for that doc | The other hunks change only docstrings and comments (`_checked_branch`, `_branch_said`, `_print_next_steps`, `_switch_line`, and the comments above the two constants). They change no behaviour, so no test can catch reverting them. | Ref | Result | |---|---| | origin/main f6376d7 (#614 landed; main has moved since BASE through #625, #583, #596) | clean, tree eb91b600. A committed merge of the head with it, e26dca8c, ran set A and #625's `test_family_bump_to_local_only.py`: `1024 passed, 20 skipped in 500.52s` | | #620 a2b4f89 | clean (it conflicted in `adopt-project.py` in the first round; it no longer does) | | #622 227bc3b (lane 2, #609, `scripts/family.py`) | clean | | #626 207d4e1 (lane 4, `scripts/family.py`) | clean | | #630 82f9403, #631 9733547, #632 86c6b9c, #633 f7f5de5, #635 2c0b157, #636 4aade9f | clean | | #638 64e6621 | clean | | #634 40a85e1 (lane 2, #610: `_lockstep` prints a fourth `switch -c`) | clean, and S2 fires on it. A committed merge, 4d1a7161: the new module, `test_validate_family_branch_step.py` and #634's `test_pinned_copy_text.py` give `2 failed, 86 passed`. The two failures are the guard, `assert [124] == []` (line 124 of the merged file is `_lockstep`'s return), and its control. With one line added to `_lockstep`, `f"moving one alone is what this refuses. {BRANCH_LEFT}"` in place of the plain string, the same merge gives `306 passed` over the nine files that read the validator or the lockstep rule (`test_validate_family*.py`, `test_family.py`, `test_family_tracking_branch.py`, `test_pinned_copy_text.py` and the new module). So the second PR to land needs that one line and nothing else. | | #639 6f938ed (lane 5, #611) | conflicts in the same one sentence, in the same two places: the end of the `member-gitlink-unnamed` remedy text in `validate-family.py` (`_check_gitlinks_named`), and the `GITLINK_REMEDY` constant in `tests/test_validate_family_unnamed_mounts.py`. #611 adds "and commit that removal, since `family.py add` refuses ..." and this PR appends `BRANCH_LEFT`. Resolved as in the first round, keeping #611's clause and appending ` {BRANCH_LEFT}` in the validator and `+ BRANCH_LEFT` in the test constant, in a committed merge (92602308): `scripts/family.py` merged cleanly, and the six test files that either PR changes or pins gave `157 passed`. Neither branch was resolved. | - No compound `||` line and no state check: see decision 1. - No README.md edit: no README sentence becomes false, because no new kind of line is printed. - The docs do not mention the exists case. AGENTS.md is at its cap, and the tools print the sentence where the failure happens. - The `switch -c` in `init`'s NEXT STEPS gets no sentence: a holder `init` just made has no branch an earlier act could have left. - **A branch checked out in another worktree (N1).** Where `family/add-atlas` is checked out in another worktree of the holder, `git switch family/add-atlas` gives `fatal: 'family/add-atlas' is already used by worktree at '<path>'` (exit 128), so the going-back half of the sentence does not work there. Git names the worktree, and another name works (`probe-n1.sh`: `switch -c family/add-atlas-2` exits 0). The sentence is not made longer for it. - I could not run Windows PowerShell or macOS here. The tests read `family.py`'s lines with `host_words`, which parses PowerShell's quoting on Windows. They make the earlier branch with `commit-tree` and `update-ref`, move `main` with the same two, write no file with `write_text` and compare nothing CRLF-sensitive. `test_another_name_works_where_main_has_moved_since` runs `git merge-tree --write-tree`, which needs git 2.38 or newer. No CI job checks Windows or macOS. - **bump-leg.py: the `git switch -c bump/<role>-<sha12>` remedy fails `already exists` when an earlier bump left that branch.** `_switch_command` (scripts/bump-leg.py:1124) prints the line for the two refusals and the dry run's closing hint. Like #607, it says nothing about a branch that is already there. - **setup-project.py: the family `git -C <holder> switch -c family/add-<project>` line has the same gap.** The NEXT lines a scaffold into a family folder ends with (setup-project.py:2358) print it with no word on an existing branch. It matters only when an earlier attempt left that branch. - L2-46 ([#459 comment](#459 (comment))) - L2-45 ([#459 comment](#459 (comment))) Review: rv607 found 0 blockers, 2 should-fix and 7 nits on 6212c31 (CHANGES NEEDED), fixed in 66b1043; the re-verify rv607b found 5 nits on 66b1043 (APPROVE WITH NITS), of which nits 1, 3 and 4 and the docstring half of nit 5 are fixed in a277695, and nit 2 is left. CI (Brett Heap's ruling 27, with his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 a277695 passed tests (Linux), tests-git-bash and SonarCloud; the head 62263d2 is a clean update-branch merge of main a524542 (over e041867, a clean merge of 5b29ee1), with SonarCloud, tests-git-bash and package green on it, and the local gate passed on the head itself: the family and validator set, 1016 passed (20 skipped), and the hygiene, AGENTS.md-code, README-section and CLI-reference sets, 222 passed. Landed on Brett Heap's word of 2026-10-16T11:21:49Z, "Merge #637 when its turn comes". Closes #607 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 16, 2026
main gained #563's own #590 (fe237ec) and #599 (32c22b0), whose trees this branch already held through #599's 750366b (#599's final head, eaa0cda, is 750366b plus a merge of main), and #626, #633, #642, #641, #643, #644, #632, #636, #631, #638, #635, #640, #623, #637 and #622. No conflict: the merged tree is 9be0d0d, the tree `git merge-tree --write-tree` predicted. AGENTS.md, docs/cli.md and tests/test_repo_hygiene.py merged as text; README.md and docs/handbook.html are unchanged by the merge. README.md is 1461 lines, its cap (1449 + 12, tests/readme-cap.d/563.txt), and AGENTS.md 494, its cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 16, 2026
… tab shows the one-line install for Git Bash, held to the other copies by a test, and the notes are current to main d4a7fab with the fifteen pull requests since 4cf5fb0, the environments, the tag ruleset and the rehearsal (#563, #461) The handbook (#461's line "The handbook's step 3 has no Git Bash path", which lane 3 told lane 5 on #473 that #630 settles): step 3's install block was `data-platform="linux mac wsl"`, so a reader on the Windows tab never saw the one line the Windows panel says Git Bash runs. The Windows tab now has a lead, "In Git Bash, the bash that Git for Windows installs, the one-line install works as on Linux:", the Linux tab's block copied byte for byte as `data-lang="sh"`, and the panel's caveat: the preflight it ends with cannot take a typed `yes` in Git Bash. README.md is unchanged: no test asked for it, and it is at its cap. tests/test_repo_hygiene.py: the test above it finds one copy of the one line per file; `test_every_handbook_copy_of_the_one_line_install_is_the_same` holds every handbook copy (the masthead's and step 3's two) to ONE_LINE_INSTALL, so a copy edited on one tab alone fails. A drifted Windows copy fails it; the page as committed passes. The release notes: - "What changed since v1.0.1" gains one entry per pull request that landed after 4cf5fb0 and is not #563's own, from `git log --first-parent 4cf5fb0..origin/main` at d4a7fab: under update-shape.py and bump-leg.py, #644, #633 and #631; under scaffold, family and the materializer, #622, #643, #640, #641, #623, #638, #635, #626, #637 and #642; under shape-doctor.py, #636 and #632. Each from its squash commit and the issues GitHub says it closes. - The list's placeholder says it is current to main d4a7fab and stays a placeholder for what lands after it; the counts are refreshed inside their placeholder (66 pull requests, 75 of 97 milestone issues closed). - #590 and #599 are cited beside #563, and "Before tagging" says they landed (fe237ec, 32c22b0) and lists the 22 open milestone issues. - "How this release is cut": Brett Heap's word at 2026-10-16T12:06:40Z, the environments testpypi (12:07:31Z) and pypi (12:07:39Z), the tag ruleset 25237399 (12:08:14Z), and the rehearsal, run 38898735926 (12:10:53Z-12:12:00Z, success, both uploads skipped), each read back from the API. The trusted publishers' date stays a placeholder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 16, 2026
…-text: remedies keyed by pin_product_key, the key the chain readers use (#444 x #610) The remedies keying fix is in the merge below, b7472df: `_link_remedies` keys a link's remedy by `pin_product_key`, and `_chain_link_findings` finds it by that key, as #638's chain readers do. Keyed by `casefold()`, the chain `openß, openss, openInk` wrote one entry for its two links, so the first link was offered the later link's words. This merge adds #623 and #637, and git merged them clean. One more change rides with it (#607 x #610): `_lockstep`'s remedy now ends with #637's BRANCH_LEFT sentence, as each remedy that names `_switch_line` does. Without it, #637's guard, test_every_remedy_that_names_a_switch_line_ends_with_the_sentence, and its partner test fail on the merge. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 16, 2026
brettheap
added a commit
that referenced
this pull request
Oct 17, 2026
… in command scope and in a global config that includes the person's own, so a `copytree` of a fixture no longer meets a detached repack deleting its loose objects (#645) (#713) **What changed.** One session fixture in `tests/conftest.py`, `no_auto_maintenance` (autouse), and one new test file, `tests/test_no_auto_maintenance.py`. No shipped file and no other test file changes. Four commits: the fix and its tests (e2110f8), a comment (accc7d2), and the fix round below (8cfc8eb, and 979bb39, which only shares a test helper). The fixture sets `gc.auto=0` and `maintenance.auto=false` for every git the session starts, in two scopes: - **Command scope** (`GIT_CONFIG_COUNT`), appended after the entries the environment already holds (the Windows job holds two). Every git the suite starts, every git a tool under test starts, and every git a `git submodule` starts inherits it. It outranks every config file. - **A global config file** (`GIT_CONFIG_GLOBAL`, in the basetemp). It includes the global config git would read without it (the file `GIT_CONFIG_GLOBAL` already named, or `$XDG_CONFIG_HOME/git/config` and then `~/.gitconfig`) and sets the two keys after it. This one is for the `receive-pack` a push starts in a local remote: git starts it with `GIT_CONFIG_COUNT` and `GIT_CONFIG_PARAMETERS` removed, so command scope never reaches it. Where `HOME` is unset the `~/` paths are left out, and an empty `GIT_CONFIG_GLOBAL` includes nothing (fix round, below). A comment beside #440's note in `clone_just_pushed` says why, names both sites, and says what is left. **Why.** `shutil.copytree` reads a repository's `objects/` one file at a time. The suite copies a repository a fixture has just committed in, fetched into or pushed to in 139 places. Each `git commit`, `merge` and `fetch`, and the `receive-pack` in a remote after a push, runs `git maintenance run --auto` as it finishes. From git's source: - git 2.46: the auto run stays in the foreground. - git 2.47: it is started with `--detach`, takes `objects/maintenance.lock` and then detaches, so the command returns while it runs. - git 2.52 and 2.53: the default strategy is still `gc`, which does nothing below 6700 loose objects. - git 2.54: the default strategy is `geometric`. Its repack runs when packs are out of geometric order, or once its estimate, taken from `objects/17` alone, is above 256 loose objects, so two loose objects there are enough (`maintenance.geometric-repack.auto`, 100, rounded up). It deletes the loose objects it packed. So on a current git, a copy taken just after a commit can find a loose object gone. It happened twice on the Linux runner: - `holder` (`tests/test_family.py:129`), in `test_family_member_name.py` (lane 1's report on #473); - `the_standard_moved` (`tests/test_doctor_pin_restore_merges.py:99`), a copy of the session's `standard` with its `.git`, in `test_copies_neither_side_edited_are_taken_whole_with_the_pin` on #623's run 38895011713: `shutil.Error ... standard5/openRepoShape/.git/objects/1e ... [Errno 2]` (lane 2's #473 comment 6215422231). The same function copies `moved` at :102 just after a commit in it. Both are covered by the same session setting: the commits before each copy run under `os.environ` (`commit_all` passes `{**os.environ, ...}`). A listing of `.git/objects` taken before and after such a repack also differs: lane 3's `objects changed` in `tests/test_family_bump_remove_rollback.py` (#736's Linux run 38981191418), 37 loose objects packed into two. The same setting covers it (i645 on #645). **Decisions and evidence.** - *Option A (the suite's environment), not B or C.* - B (config in each copied repository) would have to reach repositories the tools under test create themselves, such as the bare remotes `scaffold-project.py --local-remote-dir` makes and pushes into before a test sees them. A config written after that first push is too late. - C (a copy that survives a repack) cannot be made safe. A copy can list `objects/pack/` before the new pack lands, and then a loose-object directory after it was emptied. That copy succeeds and is missing objects. - A covers all 139 places from one place. - *Two scopes, not one.* Probe on git 2.43, pushing into a local bare remote with `receive.denyDeletes=true` given each way: - with `GIT_CONFIG_COUNT`, the branch was deleted; - with `-c`, deleted; - through a `file://` URL with `GIT_CONFIG_COUNT`, deleted; - with `GIT_CONFIG_GLOBAL`, `remote: error: denying ref deletion` (refused). So command scope alone misses the remotes, and ten copies are of bare remotes. A global file alone would be dropped wherever a test gives git a global file of its own (14 test files set `GIT_CONFIG_GLOBAL`, five more through `excluding()`). No test file replaces both scopes and also copies a repository. rv645, the reviewer, repeated the `receive.denyDeletes` probe and got the same answers, and instrumented the git calls of the batches it finished before the checkpoint: the only receive-packs that ran without both settings were in `test_family_add_excludes.py` and `test_adopt_clone_template.py`, neither of which copies a remote. - *Which verbs start auto maintenance* (git 2.43 trace2): `commit`, `fetch`, `pull` and `submodule update --remote` (its fetch in the submodule) start `git maintenance run --auto`. A push's `receive-pack` starts `git gc --auto --quiet`. `clone`, `submodule add` and `submodule update --init` start none. - *Behaviour no test checks is unchanged.* The person's own global config is still read: it is included, and it comes first. A test of that is part of the new file. The tests that read or set a global config (`test_git_helper_identity.py`, `test_adopt_git_reads.py`, `test_tracking_branch_ref.py` and others) set their own, which replaces the session's. **Fix round (8cfc8eb), on rv645's review** (no MUST; two SHOULD, five NIT). - **SHOULD 1: with `HOME` unset every git was fatal.** The file's include of `~/.config/git/config` and `~/.gitconfig` made git exit 128, `error: could not expand include path '~/.config/git/config'`, in every command the session started. The estate runs with `HOME` set, so nothing failed here; an `env -i` or hermetic runner would have lost the whole suite. - `session_global_config` now writes a `~/` path only where `home_is_known()` (`HOME` is set, or `os.name == "nt"`, where Git for Windows finds a home itself), and writes no `[include]` header when nothing is included. An `XDG_CONFIG_HOME` that is set is still included, because it needs no `HOME`. - The two `test_a_persons_own_global_config...` cases skip where `HOME` was unset. They skip on the environment and not on the file's bytes, so a fixture that dropped its includes still fails them (probe M5 below). - A new test, `test_where_home_is_unset_the_file_is_one_git_can_read` (with and without `XDG_CONFIG_HOME`), reads the file back through `git config --global --includes`. It is skipped on Windows. It and the empty-value test share one helper, `probe_read_from_the_session_file`, so the new tests do not repeat a body for SonarCloud's duplication gate. - Measured under `env -u HOME -u XDG_CONFIG_HOME`, `run9.sh`, git 2.43.0: | file | before (accc7d2) | after (8cfc8eb) | |---|---|---| | `tests/test_clone_just_pushed.py` | 1 passed, 6 errors | 7 passed | | `tests/test_no_auto_maintenance.py` | 3 failed, 2 passed, 2 errors (7 tests) | 9 passed, 2 skipped (11 tests) | | `tests/test_family_member_name.py` | not run | 13 passed | The 2 skips are the two `test_a_persons_own_global_config...` cases. - Where `HOME` is set and the file is written as before, nothing changes: across 27 combinations of `HOME` (set, unset, empty), `XDG_CONFIG_HOME` (set, unset, empty) and `GIT_CONFIG_GLOBAL` (unset, a file, empty), the file's bytes are the same as accc7d2's in the 12 where `GIT_CONFIG_GLOBAL` names a file or `HOME` is set with `GIT_CONFIG_GLOBAL` unset, and differ in the 15 where `HOME` is not set or `GIT_CONFIG_GLOBAL` is empty. - **SHOULD 2: nothing guarded the fixture's scope.** At function scope all seven tests passed (rv645's probe), and the session fixtures `scaffolded` and `family` would have been built before the setting existed. A new test, `test_the_settings_are_in_place_before_a_session_fixture_builds_a_repo`, takes a session fixture as its FIRST argument, which reads `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_COUNT` as that fixture sees them, and compares them with what the test sees and with the fixture's file. Pytest builds the higher scope first, and then an autouse fixture, so the fixture is only in place early enough if it is session scope and autouse. - **NIT 1: the receive-pack test failed under a person's global `core.hooksPath`**, which hid the remote's own `post-receive` hook (`FileNotFoundError` on `read-by-receive-pack`). The test now sets `core.hooksPath` to the remote's own `hooks` directory in the remote's local config, which outranks a global one. Measured with `GIT_CONFIG_GLOBAL` naming a file that holds `core.hooksPath`: before, `1 failed, 4 passed, 2 skipped`; after, `9 passed, 2 skipped` (11 tests). - **NIT 2: an empty `GIT_CONFIG_GLOBAL=` was read as unset.** Git 2.43 reads it as "no global config" (checked here: with `HOME` holding a `.gitconfig` probe, `GIT_CONFIG_GLOBAL=` does not read it). The fixture now includes nothing for an empty value: `if named is not None: included = [...] if named else []`. A new test, `test_an_empty_GIT_CONFIG_GLOBAL_is_no_global_config_and_includes_nothing`, fails on the old `if named:`. - **NITs 3 to 5: no code change.** - NIT 3: `test_family_dry_run.py`'s own `NO_MAINTENANCE` is redundant now. It lives in another test file that this PR leaves alone, and it is harmless. - NIT 4: a `git config --global` read without `--includes` sees only the session's two keys. The only reader is `setup-project.py:1105`, behind `os.name == "nt"`, and no test reads it. `_credential_helper_configured` uses `--get-all` with no scope, so it still sees the person's config. The conftest comment already says so. - NIT 5: the version facts in the conftest comment ("2.47 detaches", "2.54 default geometric") are from git's source tags and are not run here: only git 2.43.0 is installed. The comment is unchanged. **Tests** (`run9.sh`: gh stub first on PATH, identity blanked; git 2.43.0, Python 3.12, pytest 9.1.1). - On the fix round (8cfc8eb; 979bb39 differs from it in the new test file alone, and the batches that read that file were run on it): - `tests/test_no_auto_maintenance.py`: 11 passed (7 from the first commit, 4 new: the `HOME` test twice, the empty value, the scope). - `tests/test_no_auto_maintenance.py`, `tests/test_clone_just_pushed.py`, `tests/test_repo_hygiene.py` and `tests/test_git_helper_identity.py` together, on 979bb39: 105 passed. With `tests/test_family_dry_run.py` added, on 8cfc8eb: 166 passed. - `tests/test_adopt_git_reads.py`, `tests/test_tracking_branch_ref.py`, `tests/test_adopt_late_failure.py`, `tests/test_adopt_github_exits.py` and `tests/test_family_add_excludes.py` (the tests that read or set a global config or a hooks path): 314 passed. - On the first two commits (e2110f8 for the first four, accc7d2 for the rest; the second commit changes comment lines only). The fix round changes nothing where `HOME` is set and `GIT_CONFIG_GLOBAL` is not empty (the byte comparison above), so these were not run again: - Family, `tests/test_family*.py` (20 files): 823 passed, 19 skipped. - Adopt, `tests/test_adopt*.py` (46 files): 3463 passed, 2 skipped. - Doctor, `tests/test_shape_doctor.py` and `tests/test_doctor*.py` (12 files): 741 passed, 3 skipped. - Scaffold, `tests/test_scaffold*.py`: 634 passed. - Update-shape and bump-leg, `tests/test_update_shape*.py` and `tests/test_bump_leg*.py` (24 files): 857 passed, 4 skipped. - The rest of the files that copy a repository (`test_bootstrap_after_bump.py`, `test_non_list_project_fields.py`, `test_pin_source_env_collisions.py`, `test_pin_source_unreadable.py`, `test_shape_pin_files_not_a_list.py`, `test_validator_child_encoding.py`, `test_siblings_family_name.py`, `test_bootstrap_spelling.py`, `test_non_utf8_git_reads.py`), with `test_repo_hygiene.py`, `test_git_helper_identity.py`, `test_clone_just_pushed.py` and the new file: 924 passed. - On a merge of accc7d2 with main c6122e8: `test_no_auto_maintenance.py`, `test_git_helper_identity.py`, `test_repo_hygiene.py`, `test_clone_just_pushed.py` and `test_family_member_name.py`, 114 passed. **Reproductions.** - *The mechanism, on git 2.43.* A repository of 300 files plus two loose objects named `17…`, committed under `gc.auto=1`, then copied at once with `shutil.copytree`. This is what git 2.54 does to such a repository by default. With the base's settings, 10 of 10 copies failed, then 9 of 10 (`[Errno 2]` on a loose object directory or file). With the fix's two entries appended after it, 0 of 10 failed. - *The emulation of the race* (rv645's recipe, run on 8cfc8eb: a global `gc.auto=1` that includes the person's `~/.gitconfig`, plus a `core.hooksPath` pre-commit hook that leaves two `17…` objects and drops `gc.log`, so git 2.43 repacks in the background after each commit; `GIT_TRACE2_EVENT` over the whole run). Base figures are rv645's, on a524542; head figures are the fix round's own, on 8cfc8eb: | file | git | auto-maintenance children | repack runs | result | |---|---|---|---|---| | `test_doctor_pin_restore_merges.py` | base | 78 `maintenance run --auto`, 77 `gc --auto` | 65 | 1 failed, 14 passed, 1 error | | | head | 4 `gc --auto` (from `receive-pack`, inert under `gc.auto=0`) | 0 | 16 passed | | `test_family_member_name.py` | base | 19 `maintenance run --auto`, 29 `gc --auto` | 15 | 13 passed | | | head | 14 `gc --auto` (inert) | 0 | 13 passed | On the base, rv645 reproduced the symptom itself: `shutil.Error ... moved/openRepoShape/.git/objects/pack/tmp_rev_...`, at `the_standard_moved`'s copy of `moved` (`tests/test_doctor_pin_restore_merges.py:102`). On head, 0 maintenance runs and 0 repacks in both files. - *The first writer's runs on the issue's file* (`test_family_member_name.py`, base then head, 19 maintenance and 15 repacks against 0 and 0) agree. **Mutation probes.** Each reverts one hunk of the fix in a throwaway clone; the new file then fails. First round (against the first seven tests): | probe | fails | |---|---| | fixture sets nothing | 6 of 7 (all but the append test) | | no command scope | `test_a_commit_a_fetch_and_a_merge_start_no_maintenance[a-tests-own-global]` | | no global file | the receive-pack test, both person's-config cases | | count overwritten, not appended | the append test | | global file without the includes | both person's-config cases | | the two keys before the includes | both person's-config cases | | no `maintenance.auto` | 5 tests | | no `gc.auto` | the receive-pack test, the append test | Fix round (against the eleven tests): | probe | result | what fails | |---|---|---| | S1, `no_auto_maintenance` at function scope | 1 failed, 10 passed | the scope test, alone (the first seven passed in rv645's run of the same mutant) | | S2, not autouse | 4 failed, 7 passed | the two commit/fetch/merge cases, the tool test, the receive-pack test | | H1, `home_is_known()` always true | 2 failed | `test_where_home_is_unset_the_file_is_one_git_can_read`, both variants | | H2, the XDG include needs `HOME` | 1 failed | the same test, the `xdg` variant | | E1, an empty value is read as unset (`if named:`) | 1 failed | the empty-value test | | M5, the file includes nothing (the first round's probe, again) | 3 failed, 8 passed | both `test_a_persons_own_global_config...` cases, and the `xdg` variant of the `HOME` test | | K1, the receive-pack test without its own `core.hooksPath`, under a global `core.hooksPath` | 1 failed, 8 passed, 2 skipped | the receive-pack test | S2 is caught by tests that ask for the setting by itself; the scope test does not catch it, because earlier tests in the file build the fixture on request. K1 only fails where a global `core.hooksPath` is set, as NIT 1 says. On the base itself the new file does not import (`cannot import name 'NO_AUTO_MAINTENANCE'`). **Merge checks** (`git merge-tree --write-tree` on 979bb39): clean against origin/main 2fac979 and against lane 5's `fix/662-insteadof-helper` (4149cbb; #662 is stacked on accc7d2 and also edits `tests/conftest.py`: the fix round's hunks are in `session_global_config`, its helper `home_is_known`, and the comment above them). Earlier, on e2110f8 and accc7d2: clean against origin/main d4a7fab and c6122e8, #621, #622, #630, #631, #634, #635, #636, #637, #638 and #639, and the sibling branches for #646, #647, #648 and #649. **What it leaves.** - A git whose environment drops both scopes (a `GIT_CONFIG_COUNT` overwritten, not appended to, together with a test's own `GIT_CONFIG_GLOBAL`, or an environment built from nothing), and a push made under a test's own global file. No repository the suite copies is made or pushed to that way today. - A git older than 2.32 ignores `GIT_CONFIG_GLOBAL`. Its `receive-pack` runs `gc --auto`, which does not detach below 6700 loose objects. - `clone_just_pushed` keeps reading through git. A test with its own global file still needs it. - `test_family_dry_run.py`'s own `NO_MAINTENANCE` is now redundant, and is left alone (NIT 3). - Not run: a real Windows job (none runs the full suite since #597, and the `HOME` test is skipped on Windows), and a git newer than 2.43. **Copies found** (`git grep -n 'shutil.copytree(' -- tests`, 152 calls): - 127 copy a repository the suite built (non-bare, many holding `.git/modules`): `conftest.py` 1, `test_family.py` 18, `test_shape_doctor.py` 32, `test_doctor_legs_members_next.py` 8, the `test_update_shape*` files 30, and 38 more in 28 files. Four of them copy a subtree that holds no store: `standard / name` twice, and a member's work tree twice. - 10 copy a bare remote, or a folder of them: `test_bump_leg_paste_lines.py:95`, `test_doctor_unfinished_merge.py:166`, `test_family_add_member_id.py:239`, `test_family_add_refusals.py:487`, `:788` and `:1983`, `test_family_dry_run.py:1446`, `test_family_tracking_branch.py:279`, `test_update_shape_landing.py:156` and `test_update_shape_next_quoting.py:116`. - 2 copy a `.git` directory: `test_adopt_clone_template.py:190` and `test_scaffold_mount_ignored.py:381`. - 13 hold no repository: a template, or this checkout with `.git` ignored. - `test_adopt_plan.py:449` copies a clone named by `SHAPE_MEDXEHR_CLONE`. It is outside the suite and skipped without the variable. No shipped script copies a repository. **Checklist candidates** (rv645's; the coordinator places them): - The import-time git probe at `tests/test_family_refuse_up_front.py:75-96` runs in a throwaway directory before any session setting. It is harmless today. - Tests that overwrite `GIT_CONFIG_COUNT` (14 files) lose command scope and rely on the global file alone. That holds today, and nothing guards it. Review: rv645, lane 5's independent review, found no MUST, two SHOULDs and five NITs on accc7d2; the fix rounds above take them, and its re-verification approved 979bb39. CI (Brett Heap's ruling 27): H0 979bb39 passed tests, tests-git-bash and SonarCloud; the head a9f36ce is a clean update-branch merge of main bf1f099, with tests, tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 760892f over main 2000488: every test file that #706, #703 and lane 5's six round-1 PRs change, run under #713's conftest, with the two copytree-race files and the hygiene trio, 1332 passed, 3 skipped, 1 xfailed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts". Closes #645 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 17, 2026
… YAML reader refuses for opening with an indicator is named by its line and says to double-quote it or reword it, `plan --org @x` names `--org` again under the reader's own remedy, and `check` names one bad `tracking_branch` once (#708) (#817) Three refusals named the value to change on main or in v1.0.1, and a v1.1.0 change took that away (lane 4's L4-151, L4-152 and L4-127). L4-151, from #635 (#392, #393). The reader refuses a plain value that opens with an indicator, so an adoption plan whose hand-written `resolution:` opens with a backtick, which AGENTS.md's step 2 makes likely (`` `adopt-project.py` reads it ``), ended `check` in `REFUSED yaml-unparsable: <plan>: <words>`, exit 2, with no line among the hundreds a plan holds and no remedy, where it checked clean before. - `YamlError` carries a `line` and a `remediation`. `_scalar` and `_flow` read text and know no line, so the block reader records it: the new `_block_value` reads a block mapping's value and a block sequence's scalar item (`_parse_mapping`'s lone `-` refusal moves with it), and sets the line on a refusal raised there that carries a remedy. - `load_yaml` names it, `REFUSED yaml-unparsable: <path>: line N: <words>`, and passes the refusal's remedy on. The words of `parse_yaml` are unchanged: the tools that ask the reader about ONE rendered line quote them in a sentence of their own, where `line 1:` would be noise. - `_indicator_refusal` carries the remedy: write the value in double quotes, spelled by `double_quoted` (so the remedy's spelling reads back as the value), or reword it. That is #393's block-value openers, and #392's flow-sequence items and flow-mapping values. The other refusals carry none (#370): the fix for an alias or an unclosed quote is no one edit. - A flow-mapping entry whose key opens with an indicator (`{@A: b}`, ``{`a`: b}``, `{: a}`, `{? a: b}`, `{#a: 1}`) carries no remedy, and so no line: `_split_key_flow` asks `_refuse_entry_indicator(text, remedy=False)`, because the whole entry double-quoted (`{"@A: b"}`) reads as an entry without a key and is refused again (lane 1's review of #817, finding 1). - Only a refusal with that remedy names its line. A line on every value's refusal (an alias, an unclosed flow, an empty flow entry) was measured: it changed words that the naming gate's `declared-unverified` reason and the root's `validate-pins.py` print whole, pinned by tests/test_pinned_copy_text.py, tests/test_naming_policy.py and tests/test_refusal_default_remediation.py, which is beyond this issue. - The project's own gates read their files with this copy: an indicator in a pin file is now the quoting remedy under `validate-pins.py`, where it printed the lockstep one, which is not a syntax error's remedy. L4-152, from #635. The reader refuses `repository: @x/Northwind`, so `plan --org @x` (a typo of `@opensoft`: GitHub owners never begin with `@`) stopped reaching the validator, whose `manifest-leg-repository` named `--org` (`FINDING_PLACEHOLDER`). It said "the manifest this run would write is a file the project's own YAML reader refuses", named no value, and its remedy said "the words after each value are the validator's own". - `_unread_sentences` renders each template line that carries a value of the caller's, by its own placeholder or by one `CARRIED_BY` names (each leg's `repository:` carries `ORG`, as the validator's finding about it does), reads it alone as `_misread_rows` reads one, and names the value where the reader refuses its line: `--org is '@x', which the project's own YAML reader refuses: <the reader's words for each line>`, main's sentence with the reader in the validator's place. The validator's own words cannot be had: it never reads a file the reader refuses. Where no such line is refused, the sentence is the reader's refusal of the file, as before. - `MANIFEST_UNREADABLE_REMEDIATION` is that case's remedy: choose another value for each one named, and where none is named, the text the reader's words quote holds it; the words are the reader's. `manifest_value_problems` gives the case the kind `KIND_UNREADABLE` (the kinds are named once: `KIND_MISREAD`, `KIND_INVALID`, `KIND_UNREADABLE`), and `refuse_manifest_values` (`plan`, the scaffold), which takes each kind's remedy from `KIND_REMEDIATIONS`, and `check`'s `_manifest_findings` (and so `execute`) give it that remedy, under the code they gave it before, `-manifest-invalid`. `MANIFEST_INVALID_REMEDIATION` is not reworded, so `scripts/family.py`'s copy of it does not move, and `family.py init --org @x` stays refused. L4-127, from #644 (#455). The validator finds a NUL or a lone surrogate in `tracking_branch`, and `check` asks it, so `tracking_branch: "a\x00b"` was `unsafe-value`, then `plan-manifest-invalid` in the validator's words, which name no plan key. No such value is in `checked_value`'s alphabet, so `unsafe-value` names every one. `_validator_ignores` leaves the validator's `manifest-tracking-branch` out where `checked_value` refuses the branch, as `LEG_PATH_CODES` leaves the leg paths to `_leg_findings`, and `_manifest_findings` and `_validator_judged` ask it alike. It asks `checked_value` alone, which starts no process, as the validator's question must not (test_the_question_runs_no_process_at_all). The issue's other option, a `FINDING_PLACEHOLDER` row, was measured: it names the key and still prints two findings. Pinned copy. `scripts/repo_shape.py` is the one pinned shape copy this changes. No digest of it is recorded in this tree; each root takes it through `update-shape.py` as an `upstream-changed` row, with v1.1.0's other bumps. Tests: tests/test_reader_refusals_named.py (new), with lane 1's test_a_flow_mapping_key_is_not_told_to_quote_the_whole_entry. In tests/test_yaml_indicator_openers.py, two assertions that pinned an opener's refusal without its line expect `line N:` on its row; in tests/test_adopt_plan_yaml_quoting.py, one that pinned the validator's remedy under the reader's refusal of a whole manifest expects the reader's. Closes #708 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
added a commit
that referenced
this pull request
Oct 18, 2026
…space opens reads as PyYAML reads it, and validate-manifest, bootstrap, bump-leg and update-shape refuse a branch holding a character that does not print or is white space (#715) (#823) What #635 left in the reader, and three texts about it: lane 4's lines L4-148 to L4-155 on #459's checklist, from R392's review of #635. Fix round 1 answers lane 3's review RV823. - L4-150. Every strip of a line in the reader (`_strip_comment`, `_split_key`, `_scalar`, `_flow`, `_split_flow`, `_significant_text` and `_block_scalar_body`) was `str.strip()` with no argument, and `_DASH_RE` used `\s`. Both also take U+001F and sixteen Unicode spaces (U+00A0, U+1680, U+2000 to U+200A, U+202F, U+205F and U+3000), which YAML keeps as text. So `k: \u3000]`, `k: \u2003@a` and `k: \xa0%a` were refused quoting `']'`, `'@A'` and `'%a'`, texts the file does not hold, and `k: [a,\u3000,b]` was refused as an empty entry; `k: \xa0` read as no value, `- \u3000` as `[None]` and `k: [a, \xa0]` as `['a']`. PyYAML reads each whole. One constant, `_YAML_WHITE = " \t"`, now names YAML's white space at every strip, and `_DASH_RE` is `^-([ \t]+|$)`. Measured with PyYAML 6.0.3 over 123 probe documents: 77 readings changed, 31 of them now as PyYAML reads them and 37 now refusals. The other 9 (7 inputs) now read a document PyYAML refuses, each in a class this reader already reads: U+001F as text, as a raw ESC is; a tab after a Unicode space inside a plain scalar, as in `k: a\tb`; and a `:` opening a value inside a block value that a Unicode space opens, as in `k: a: b` and `k: a:`. - RV823 MUST-1. So `tracking_branch: main\xa0`, which read `main`, reads `'main\xa0'`, a name no person can tell from `main`. Four tools now refuse a branch holding a character for which `str.isprintable()` is false or `str.isspace()` is true, and bump-leg and update-shape refuse no other: `validate-manifest.py`'s `_branch_finding` reports `manifest-tracking-branch` and `manifest-leg-branch` with the value as `{value!r}`; `bootstrap.py`'s `_argument_fault` refuses it (`UNSEEN`), so no ref is made and the leg is left as it is, while the byte-carrying surrogates U+DC80 to U+DCFF still reach git (#455); and `bump-leg.py`'s `Project.tracking` and `update-shape.py`'s `tracking_branch_of` read through `readable_branch`, in `scripts/shape_materialize.py`, which no root carries, and which refuses it as `unseen-branch` in the validator's words, a NUL and a lone surrogate in theirs too, exit 2, before any fetch or write, and refuses a leading `-` as `unsafe-value`, since update-shape hands the name to `gh pr create --base`. Read raw, `update-shape.py apply --branch main` on `main` committed onto `main` and printed `gh pr create --base 'main\xa0' --head main`. On Brett Heap's ruling (option A, 2026-10-17T20:23:58Z) the two tools do not read it through `checked_value`, which refused `feature/` and CJK, `développement` and `a&b`, names git creates and the root's gate passes; #209's quoted `--base a&b` stands. #391's spelling tests move onto the byte name `tb\udcffx` (POSIX only, skipped with a reason where the disk refuses the byte), one row keeps `tb\u2028x` as a refusal that spells the name, and the zero-width joiner, `a b` and `a\x1bb` move to the fault tables. `test_the_branch_and_its_upstream_are_spelled_in_the_move_back`, which waited on #710, is a plain test again, with HEAD read as bytes. - The merge with #824 (#710), main `0dee22b3`. `_argument_fault` asks a NUL and a lone surrogate with no byte, then `UNSEEN`, then #824's `UNWRITABLE` last: its line calls the name good, and a name holding a character no person can see is not good in any encoding. `_say_not_tried` keeps #824's `refused=` lines, with `UNSEEN`'s first. In #824's `tests/test_bootstrap_non_utf8_branch.py`, the row whose branch ended in U+00A0 has a printable branch `aé` and keeps the upstream ending in U+00A0 it was written for, and the `日本 x` row of the names git refuses is `日本~x`; three rows hold that a space is named before `UNWRITABLE`. - L4-148. `- :` read `[{'': None}]` and `: a` read `{'': 'a'}`, where PyYAML refuses both. `_refuse_unsupported` now refuses a line or an item that opens with a `:` opening a value, beside the explicit and merge keys and in their words: `line N: empty keys are not supported`. A quoted `"": a` still reads `{'': 'a'}`. The fuzz in `tests/test_yaml_indicator_openers.py` no longer drops `- :` (`EMPTY_BLOCK_KEY` is gone), and seeds 1 and 3 draw it again. - L4-149. `k: {?: 1}` read the key `'?'`, where PyYAML reads `{None: 1}`. Of the issue's two options it is refused, by the one test `_refuse_entry_indicator` already makes of `[?]`, `{?}` and `{a: ?}`: a key that is a lone `?`, a `?` whose `:` opens a value, is `explicit keys are not supported: '?: 1'`. `[?: 1]` gives that refusal too, where it gave the single-pair one. `{?a: 1}` and `[?:a]` stay the text #252 keeps, and a block `?: 1` is the key `'?'` to PyYAML as well. - L4-155. `k: [a: b: c]` was refused with the single-pair words. A flow sequence item whose pair's value splits again at a `:` opening a value is now `a flow sequence entry with a second `:` opening a value is not valid YAML: 'a: b: c'`. `[a:b: c]` and `[a: "b: c"]` are real single pairs to PyYAML and keep the single-pair words. A quoted item's value is asked the bracket test first, so `["a": b[: c]` keeps #301's words, unless it opens a flow collection: `["a": [b]]` keeps the single-pair words, as on main. - L4-153. `_read_back` called `parse_yaml` unguarded. `checked_value` admits a leg path opening with `@`, which opens no plain scalar since #393, so a bare leg-path template line holding `--spec-path @x` raised `YamlError` out of `leg_path_problem`. The refusal is now the reading, and `_read_as` returns `_reading_in_words(value)`, "nothing at all, because it refuses the line (...)", so the scaffold refuses the value as `scaffold-bad-leg-path` with `LEG_PATH_REMEDIATION`. `_misread_reason`'s docstring now says the alphabet holds `@`. The four shipped leg-path lines are quoted (#259), so `--spec-path @x` still plans. - L4-154. `_neutral_pin_entry`'s docstring gave `id: -` as "the text", which the reader refuses since #393. Its example is now ` - - a`, a list of its own, where `id: - a` is the text. Pinned copies: `scripts/repo_shape.py` (assembly and family roots), `scripts/validate-manifest.py` and `scripts/bootstrap.py` (assembly roots) are digest-pinned in each root's `contracts/shape-pin.yaml`. No digest is recorded in this repository, so nothing here is re-pinned. `update-shape.py check` shows each as `upstream-changed` and `apply` re-copies and re-pins it; after that a root's gate refuses a quoted scalar followed by a Unicode space, `- :`, `: a`, `{?: 1}`, `[?: 1]` and a branch name holding a character that does not print or is white space. Until a root re-pins, its own validators and bootstrap read `main\xa0` as `main`. `with_scalar` on main wrote `commit: "NEW"\xa0 # c`, which this reader refuses; it now writes `commit: "NEW" # c`. Tests: `tests/test_yaml_reader_spaces.py` (new, 339 tests) holds a row for each case, with each of U+3000, U+2003 and U+00A0, compares each with PyYAML where it is installed, finds the seventeen characters rather than listing them, and runs a project's own `validate-manifest.py`, bootstrap, bump-leg, update-shape and one scaffold `--dry-run`. Thirty-two mutants, each reverting one part, are each killed: two put `checked_value` back and fail the rows that take the readable names, and one asks `UNSEEN` after #824's `UNWRITABLE` and fails the rows that hold the order. The targeted run at the head, which is the merge with main `ca47b71d`, covered 73 files: the changed test files, #824's bootstrap gate set, every file that imports the reader, every bump-leg and update-shape file, the doctor, and the hygiene, CLI-reference and AGENTS.md checks. It gave 5718 passed and 34 skipped. Closes #715 Lane: openreposhape-4 (openRepoShape-4) 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-4 (openRepoShape-4)
Claim: #392 #392 (comment) ; #393 #393 (comment)
What changed and why
scripts/repo_shape.py's YAML subset reader read three kinds of text where PyYAML refuses the document, or reads another thing. Each is now aYamlError, soload_yamlturns it intoREFUSED yaml-unparsablenaming the file. That is the reader's existing refusal, in #252's and #301's voice. Re-measured on main 8f453d0 before the change: every row both issues list reproduced as the issue says.#393, a block value or item.
_scalarreads every plain value, block and flow, and its only test of the first character was&*!. It now also refuses a plain scalar that opens with],},,,@, a backtick or%, or is a lone?or:._parse_mappingrefuses a value that is a lone-. That one is refused there, not in_scalar, because a flow collection reads[-]and{a: -}as the text-, as PyYAML does and as #392 asks to keep.k: ]{'k': ']'}a plain scalar cannot open with `]`, which closes a flow sequence in YAML: ']'k: }{'k': '}'}... `}`, which closes a flow mapping in YAML: '}'- ]/- }/k:\n j: ]/k: ]x/k: ] ak: ,{'k': ','}... `,`, which separates flow entries in YAML: ','k: @a{'k': '@a'}... `@`, which is reserved in YAML: '@a'k: `a`{'k': '`a`'}... a backtick, which is reserved in YAML: '`a`'k: %a{'k': '%a'}... `%`, which opens a directive in YAML: '%a'k: ?{'k': '?'}explicit keys are not supported: '?'k: -{'k': '-'}... `-`, which opens a sequence entry in YAML: '-'k: :{'k': ':'}... `:`, which opens a value in YAML: ':'#392, a flow collection.
An empty entry.
_split_flowdropped every empty part. It now drops only the one after a trailing comma ([a,],{a: 1,}) and refuses any other:a flow collection holds an empty entry, which YAML allows only after its last comma: 'a,,b'. That coversk: [,a],[a,,b],[,],[a,,],{,}and{a: 1,,b: 2}.[],[ ]and{}read as before.An entry that opens with an indicator. The new
_refuse_entry_indicatorrefuses an item, or a mapping entry whose key it opens, that opens with any of::,#,@, a backtick or%;-or?;?.It runs in
_flow_valuefor a sequence's items and in_split_key_flow, where it replaces scripts/repo_shape.py reads? 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?test and keeps its words.k: [:]/k: [: a][':']/[': a']a plain scalar cannot open with `:`, which opens a value in YAML: ...k: {:":}/k: {: a}{':"': None}/{'': 'a'}k: [#a]/k: {#a: 1}... `#`, which opens a comment in YAML: ...k: [- a]['- a']... `-`, which opens a sequence entry in YAML: '- a'k: [? a]['? a']explicit keys are not supported: '? a'[-],[-a],{-: 1},{a: -}and scripts/repo_shape.py reads? 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{?a: b}read as before.A single pair in a flow sequence.
_flow_valuerefuses an item that_split_keysplits, after repo_shape.py: a flow collection with an unmatched]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 bracket check (sok: [a: [b]]keeps repo_shape.py: a flow collection with an unmatched]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 words):a single-pair mapping in a flow sequence is not supported: 'a: b'.k: [a: b],[a: b, c],[a:]and["a": b]are refused;[a:b]and[{a: b}]read as before.The decision on
[a: b]: refuse it, not read it as PyYAML does ([{'a': 'b'}]). No file this reader serves can hold one:neutral_product_pins,allow_empty_legs,also_matches,referent_chain. There, a mapping reaches the tool as a name, andvalidate-manifest.py'sstr(pin)makes it the pin"{'openGlass': 'x'}".legs(role, repository, path),members(project, repository, path), the shape pin'sfiles(path, sha256),submodules, a plan's entries, a plan'spins(rows of five keys since aa4e1f5:product,repository,commit,tree_sha256anddigest_source; only an old plan's entry is a bare name), and the policies'rulesandfamilies.So
pins: [openGlass: x]is a typo, refused by name. It joins explicit keys in the header's REFUSED list, as valid YAML refused by design.The pinned copy.
scripts/repo_shape.pyis the only copy in this tree, and no file or test here records its digest or blob. That matches #301's, #370's, #441's and #521's changes to it, which touched no pin. Each project takes it throughupdate-shape.py, as anupstream-changedrow onscripts/repo_shape.py.Reach.
tests/and every tracked YAML file was read by the reader at main 8f453d0 and by this head: 41,675 inputs. No tracked YAML file reads differently (0 of 15). The 636 inputs whose answer changes are all prose that happens to parse: aFINDING <code>:message whose text opens with a backtick, or a docstring. None is a YAML fixture, and none crashes.adopt-project.py'syand_neutral_pin_entry, the scaffold's pin entry,family.py's id, andshape_materialize's misread check. Each catches theYamlErrorand double-quotes the value, or refuses before anything is written.scaffold-project.py --org @x --project Atlas --dry-run. It exits 2 withscaffold-manifest-invalidand nothing created, on main and here. Main's detail is the validator'smanifest-leg-repositoryfinding. Here it isthe manifest this run would write is a file the project's own YAML reader refuses: a plain scalar cannot open with `@`, which is reserved in YAML: '@x/Atlas'.--org @xreaches two more tools.SAFE_ARG_REadmits an@anywhere in an--org, and--orgis the one bare value a tool writes that can open with an indicator. Both were measured in standalone clones, on main 4cf5fb0 and on this head, with aghshim that refuses everything:adopt-project.py plan --org @xstill exits 2adopt-manifest-invalidand writes no plan. Its detail no longer names--org. Main said--org is '@x', which the project's own validate-manifest.py refuses: manifest-leg-repository: leg 'assembly': repository is '@x/Atlas', expected `<org>/<Name>`. ...; here it isthe manifest this run would write is a file the project's own YAML reader refuses: a plain scalar cannot open with `@`, which is reserved in YAML: '@x/Atlas'.family.py init --org @x --family Orbit [--dry-run]exited 0 on main. It printed a plan for@x/Orbitand askedgh repo view @x/Orbit, and without--dry-run(measured with--local-remote-dir) it went on to create the holder. Here it exits 2family-manifest-invalidbefore anything is created,--dry-runalike:the manifest this run would write is a file the holder's own YAML reader refuses: a plain scalar cannot open with `@`, which is reserved in YAML: '@x/Orbit'.Tests
New
tests/test_yaml_indicator_openers.py, ontests/test_yaml_flow_brackets.py's harness:_scalarreturns a BLOCK plain scalar that opens with]or}as text, sok: ],k: }and- ]read as the strings']'and'}', where YAML forbids a plain scalar to open with a flow indicator and PyYAML refuses, and the same fall-through readsk: ,,k: @a, `k:aandk: %a` #393's block rows: all 14, plus 7 more places a block plain scalar stands. A lone?as a block item is refused as an explicit key.k: [,a],k: [a,,b],k: {,}) or an entry that opens with:,#,-or?(k: [:],k: {:":},k: [#a],k: [- a]) is read, where PyYAML refuses, and a single pair in a flow sequence (k: [a: b]) reads as the string'a: b', where PyYAML reads[{'a': 'b'}]#392's rows:?as a whole flow-mapping entry,k: {?});["a": b], an item after another, a pin-list typo).? 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{?a: b}still reads as the key?a, and repo_shape.py: a flow collection with an unmatched]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 words are kept for[a: [b]].load_yaml:yaml-unparsable, the file named, the detail in full.scripts/validate-manifest.pyonproject.yamledited three ways: exit 2,REFUSED yaml-unparsable: <path>: <words>, nomanifest ok, no traceback, and the file as the edit left it.- :is left out (EMPTY_BLOCK_KEY): this reader reads it[{'': None}], PyYAML refuses it, and neither issue names it. The block table can draw it, and on seeds 1 and 3 it did, so the first round's fuzz was red there; a sweep of 303 other seeds on this head finds no disagreement.The new file on main's reader (4cf5fb0, whose
scripts/repo_shape.pyis 8f453d0's, unmodified): 89 failed, 139 passed. The 139 are the controls and the comparisons with PyYAML.Mutants. Each reverts one part of the fix, run against the new file; all killed:
_split_flowdrops every empty part again_scalar's opener test and_parse_mapping's lone-(all of #393)_refuse_entry_indicatorback to #252's?alone_refuse_entry_indicatordropsor text == "?", so{?}gives "flow mapping entry without a key"k: {?}(lone-question-mark-entry): 1 failed, 328 passed in the new file withtest_yaml_flow_mappings.pyandtest_yaml_flow_brackets.py; it survived the first round- :againTargeted run, first round, on 2c0b157's bytes (
scripts/repo_shape.pysha256c5492f10c726b7dd8829b16f4d7ed058e1914a9c8f3232e5b424f343f67af458, the test79d7e6ecd6a9acdf132c9ef0ecda5a1a45ef2b230b64de8edd01b2bfb0e1ea7e). Linux, Python 3.12.3, pytest 9.1.1, git-filter-repo 2.47.0 on PATH, PyYAML 6.0.3. One background run of 30 files: 2859 passed, 28 skipped, 0 failed in 13:14.Files: the new file, the reader's own files (
test_yaml_flow_brackets,test_yaml_flow_mappings,test_yaml_flow_quotes,test_yaml_quote_boundaries,test_yaml_block_scalar_keys,test_load_yaml_refusals,test_manifest_validator), and the files that call_scalaror ask the reader how a value reads (test_adopt_plan_yaml_quoting,test_leg_path_yaml_scalars,test_adopt_plan_escapes,test_template_quoted_values,test_template_values_single_pass,test_template_text_placeholders,test_scaffold_manifest_values,test_adopt_manifest_values,test_scaffold_free_text_values,test_family_init_values,test_adopt_spelled_unambiguous,test_adopt_pins,test_scaffold_referent_chain_links,test_shape_pin_files_not_a_list). Then the validators (test_pin_validator,test_neutral_pin_validator,test_validator_child_encoding), the plan (test_adopt_plan,test_adopt_entry_paths), andtest_repo_hygiene,test_cli_reference,test_agents_md_codes.The 28 skips:
test_leg_path_yaml_scalars.py: empty parameter sets, and "the templates read 2026 back";test_family_init_values.py: "this template quotes ...";test_adopt_plan.py:435:SHAPE_MEDXEHR_CLONEunset.Those two files collect the same 332 items on main and on this head.
Fix round 1 (
f852e6fd), on its own bytes (scripts/repo_shape.pysha256616955934b689a71477cb03e844bcfc8fb20e42410b2dac457f0b27732307af8, the test2ac8e7eee2a4ade68f68dab16091f19bce8c7a7d96ef1efc524b1706c75ce64a).scripts/repo_shape.pychanges in comments and docstrings only: its AST is equal to 2c0b157's once docstrings are stripped, so the 30-file run above holds for the reader's code. One background run of the 11 files this round changed or that read the YAML reader by name (the ninels tests | grep -i yamlfiles,test_repo_hygieneandtest_cli_reference): 1291 passed, 8 skipped, 0 failed in 1:53. The 8 skips aretest_leg_path_yaml_scalars.py's, as above. The new file is 228 tests, 225 before.Not run here: the full suite (CI is the gate). The test writes its files as bytes and passes nothing to git on stdin.
What it cannot tell
These still read where PyYAML refuses or reads another thing. Neither issue claims them, so they are left as they were, for #457–#461's checklists:
?,:or-followed by a space and more text, as a VALUE (k: - a,k: : a,k: ? a,{x: - a},{x: ? a}).tests/test_yaml_flow_mappings.py::test_a_question_mark_in_a_value_is_not_a_key_in_a_flow_or_a_blockpins{x: ? a}to read as its block twinx: ? a. Its docstring calls refusing them "a different change, the same one for the two forms if it is made". repo_shape.py:_scalarreturns a BLOCK plain scalar that opens with]or}as text, sok: ],k: }and- ]read as the strings']'and'}', where YAML forbids a plain scalar to open with a flow indicator and PyYAML refuses, and the same fall-through readsk: ,,k: @a, `k:aandk: %a` #393 claims only the lone indicator.:and a character ({a: :b}). Its block twina: :breads':b', as PyYAML does.?a([?a],{?a: b}). PyYAML reads an explicit key. scripts/repo_shape.py reads? 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 keeps the key?a, and repo_shape.py: a flow collection with an empty entry (k: [,a],k: [a,,b],k: {,}) or an entry that opens with:,#,-or?(k: [:],k: {:":},k: [#a],k: [- a]) is read, where PyYAML refuses, and a single pair in a flow sequence (k: [a: b]) reads as the string'a: b', where PyYAML reads[{'a': 'b'}]#392 claims it neither way.?inside a flow scalar ([e?], scripts/repo_shape.py reads? 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{a: b?}). PyYAML ends the scalar there and refuses.|and>opening a plain scalar. In a block,_opens_block_scalarreads>= 3as text by design; in a flow, the reader reads[|a].@a: 1,]: 1,- :), and&,*or!opening a flow key.-with a space after it, inside a flow ([- ]). The reader strips each entry, so it cannot tell[- ]from[-], which PyYAML reads.scaffold-project.py:1496's docstring usesid: -as "the text" a bare line reads. The reader now refusesid: -. The point it makes (ask the list entry) still holds; the example is one word out of date, in a file this PR does not touch.Shared files
Head
f852e6fd(fix round 1, on2c0b1573).git merge-tree --write-tree --name-only f852e6fd <ref>at 2026-10-16T10:40:34Z, each head read withls-remotefirst (10:40:10Z):6f760b7a: clean, treef8f27e95. Main has not touchedscripts/repo_shape.pyortests/test_yaml_indicator_openers.pysince this branch's base 8f453d0:git diff --name-only 8f453d0e 6f760b7a -- scripts/repo_shape.py tests/test_yaml_indicator_openers.pyis empty, and the two blobs ofscripts/repo_shape.pyare equal.--link-sourcekey are one product bypin_product_key, the pin file's own fold, so a tree declaringopenssno longer verifies a chain that saysopenß, a chain naming both is two links and not a cycle, and--link-source openss=no longer answers foropenß(#444) #638fix/444-chain-fold,bae24e3b(lane 4; the chain readers in this file): clean, tree849bac28. Its hunks inscripts/repo_shape.pybegin at base line 1377 (_pinned_basenames,_chain_link_findings,NamingPolicy,link_pins_from_trees); this PR's end at base line 852.--pin-source, the root's not-used note spells it too, and THE LOCKSTEP RULE'sbumpruns (#610) #634fix/610-pinned-text,6dad64bc(lane 2): clean, tree33727cfb. Its hunks inscripts/repo_shape.pybegin at base line 909 (ingit_out), then_chain_link_findings,link_declarations,LinkPinsandlink_pins_from_trees.This PR changes the YAML reader in
scripts/repo_shape.py(its hunks lie between lines 196 and 977 of the head) and no chain reader, which begin atCHAIN_RECORD_FIELD, line 1301.Closes #392
Closes #393
🤖 Generated with Claude Code
Summary by Sourcery
Align the repository's YAML subset reader with YAML parsing boundaries by refusing malformed and ambiguous constructs rather than silently accepting incorrect values.
Bug Fixes:
Enhancements:
Documentation:
Tests: