Repository navigation
validate-repository-naming.py: a --link-source key is the longest chain link before an = that leaves a path after it, so the offer the gate prints for a link holding = verifies that link when typed as printed (#980) - #1044
Conversation
…hain link before an `=` that leaves a path after it, so the `--link-source open=y=<path>` the gate offers a link holding `=` verifies that link when typed as printed, and `open=y=` is refused as naming no path (#980) The gate offered a declared-unverified chain link whose name holds `=` the source `--link-source open=y=<path>`, and read every `--link-source` key at the value's first `=`. Typed as printed, the key was `open`, which no chain names, so the whole value was a bare PATH for the chain's first link: a `link-source-unreadable` WARNING, and `open=y` stayed declared-unverified under the same offer, exit 0. In a chain naming `open` and `open=y` both, `open=y=<path>` was a source for `open`, with the path `y=<path>`, and it replaced `open=<path>`. `LinkSources._read_source` now reads a key the way `_read_remedy` reads a remedy's link since #949, the issue's direction (a): the longest text before an `=` that names a chain link, by `pin_product_key`, and leaves a path after it. The key is read whole, never stripped or cut at a `/`, so `./PATH` still asks for a directory spelled like the whole value, the keyed reading still wins over such a directory (with the `./` line), and `<link>=./<path>` gives a link a path that begins like a longer link's name. A value that ends at a link, `open=y=`, is that link's with no path, and is refused as `link-source-no-path`, as `openXdox=` is (#442). For a chain whose links hold no `=`, every value is read as before. The module and class docstrings, the `--link-source` help and docs/cli.md say so, and so does setup-project.py's note on why step (4)'s absolute `--pin-source` is one path to the gate. `validate-pins.py` still ends a `--pin-source` key at the first `=`; no scaffold writes a pin holding one, and the docstring says it is left. What a project sees: `update-shape.py check` lists `upstream-changed scripts/validate-repository-naming.py` (sha256 ec167fb0e259f907...), and `apply` re-pins it. Closes #980 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 1 day and 12 hours by commenting |
|
(lane openRepoShape-5, 2026-10-20T06:34Z) @codex review |
Reviewer's GuideThis PR fixes Sequence diagram for validating a printed source offersequenceDiagram
participant Gate
participant LinkSources
participant ChainLink
Gate->>LinkSources: _read_source(open=y=<path>)
LinkSources->>LinkSources: Match longest key open=y
LinkSources-->>Gate: link=open=y, path=<path>
Gate->>ChainLink: Verify source tree
ChainLink-->>Gate: verified
Flow diagram for --link-source key resolutionflowchart TD
A["--link-source value"] --> B{"Find matching chain link before ="}
B --> C["Select longest case-folded match that leaves a path"]
C --> D["Use matched link and remaining text as path"]
B --> E["No matching link"]
E --> F["Treat entire value as bare PATH"]
D --> G{"Path is empty?"}
G -->|Yes| H["Refuse link-source-no-path"]
G -->|No| I["Verify the matched link's tree"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
Independent review of #1044 (#980) at Reviewed from 09:38Z to 09:59Z, read-only. The head is Findings, most severe firstThere is no MUST and no SHOULD. NIT-1 (test strength): four of my seven mutants survive the five closest test files (404 passed each time). Each one changes output, and three of them break the body's claim that a chain whose links hold no
NIT-2 (docs, left with the follow-up): Rulings on the writer's open points:
Checked and true, with numbers
Could not verify
Checklist candidates
Reviewer: rv980, lane 5's independent reviewer, not the writer (w980). Lane: openreposhape-5 (openRepoShape-5) |
|
…`--link-source` reading at the old first-`=` split's corners (a keyed path that begins with `=`, a keyed blank path, a key in capitals) (#1054) (#1058) Three test rows hold what #980 (#1044) left unchanged in how `--link-source` reads a value (a keyed path that begins with `=`, a keyed blank path, a key typed in capitals), so each of rv980's four surviving mutants of `scripts/validate-repository-naming.py` fails a test. Fix round 1 (rv1058's NIT 1) adds a fifth mutant to what the rows kill. #1044 said a chain whose links hold no `=` is read exactly as before. That is true (rv980 compared 800 runs on the base and the head), but it rested on that probe: `tests/test_chain_remedy_callers.py` held the #980 rows for the new reading and none for the corners of the old first-`=` split. Of rv980's seven mutants, R1, R5, R10 and R14 survived the five closest test files (404 passed under each). Tests only. The one file changed is `tests/test_chain_remedy_callers.py` (72 lines added, none removed, over two commits), in the "#980" section after `test_a_key_is_read_whole`, reusing the file's `THREE`, `EQUALS`, `naming`, `tree`, `gate_warnings`, `UNREACHED` and `clear_ambient_pin_sources`. No assert joins conditions with `and` or `or`. No script, template or doc changes, so no pinned copy moves and a root's `update-shape.py check` gains no row. Each pins bytes the gate already prints on main (the new parameter of row 3 passes on `8a057215`'s script, which is byte-identical to the one here). 1. `test_a_keyed_path_may_begin_with_an_equals`, two parameters: `openXdox==x` with `THREE` (`a-plain-link`), and `open=y==` with `EQUALS` (`a-link-holding-an-equals`, the issue's parenthetical). Exit 0; the WARNING `link-source-unreadable: =x (--link-source openXdox=...) could not answer for openXdox: no such file or directory` (and `= (--link-source open=y=...) ... for open=y`); no "read as a PATH" line. Kills R10. 2. `test_a_keyed_blank_path_is_one_blank_and_is_not_refused`: `'openXdox= '` with `THREE`. Exit 0; no `REFUSED`; the WARNING `link-source-unreadable: (--link-source openXdox=...) could not answer for openXdox: no such file or directory` (the path is one blank); no "read as a PATH" line. Kills R1 and R14. 3. `test_a_key_typed_in_capitals_is_noted_as_the_chain_spells_it`, two parameters, with the name `brett-wip`, which reads no declaration. Exit 0, and the one `NOTE ` line is compared as a whole line: `NOTE --link-source <typed>: not used - no name classified here reads <link>'s own declaration.` (the em dash of the gate's text, as the file's other NOTE rows spell it). - `a-link-holding-an-equals`: `OPEN=Y=<tree>` with `EQUALS`, the NOTE ends `reads open=y's own declaration.`. Kills R5. - `a-capital-inside-a-link` (new in fix round 1, rv1058's NIT 1): `OPENXDOX=<tree>` with `THREE`, the NOTE ends `reads openXdox's own declaration.`. The first parameter's chain, `open=y`, is all lowercase, so it could not tell "the link as the chain spells it" from "the link lowercased"; `openXdox` has a capital X. Kills R5 as well, and M2 (`return link.lower(), ...`), which survived the first head. Bytes against `5b41d44f`, the base of #1044 (checked with `git archive 5b41d44 contracts scripts` and the same values: stdout, stderr and exit code compared): - Identical to main: `openXdox==x` (row 1), `'openXdox= '` (row 2) and `OPENXDOX=<tree>` (row 3, the `a-capital-inside-a-link` parameter). - Different, because they are the new reading of a link holding `=`: `open=y==` with `EQUALS` (row 1's second parameter; at `5b41d44f` it is a bare PATH, `link-source-unreadable: open=y== (--link-source) ... for openXdox`, with "It was read as a PATH: `open` is not a link ...") and `OPEN=Y=<tree>` with `EQUALS` (row 3's first parameter; at `5b41d44f` its NOTE says `reads a chain link's own declaration`, with the same "read as a PATH" sentence). Left alone, as the issue's "Optional" part says it may be: the "It was read as a PATH" line (`LinkSources.read_as_path`, which names only the text before the first `=`). It is a NIT for a follow-up and #1053's neighbour (`validate-pins.py` has the twin), and changing it touches the pinned `scripts/validate-repository-naming.py`. Each applied alone to `scripts/validate-repository-naming.py` at the line the issue (or rv1058, for M2) names, in the worktree, restored with the saved copy and read back (sha256 `ec167fb0e259f90705db895a1a37f4adebd4cbf03ca2d5ae18f6d9a8df896548` after each; `git diff` of the script empty). R10's text is also at `:479` in `_read_remedy`, which is not mutated. The whole file (68 tests) is run with each, on git 2.43, at the fix round's head. | Mutant | The one-line edit | What it prints for the row's value | Fails | Count | |---|---|---|---|---| | R10 `:439` | `typed.rfind("=", 0, at)` to `typed.rfind("=", 0, at - 1)` | `openXdox==x` is a bare PATH: `link-source-unreadable: openXdox==x (--link-source) ...` and "It was read as a PATH: `openXdox` is not a link ..." | row 1, both parameters, on the WARNING assert | 2 failed, 66 passed | | R1 `:437` | `typed[at + 1:]` to `typed[at + 1:].strip()` in `if link is not None and ...` | `'openXdox= '` is a bare PATH: `openXdox= (--link-source)`, with the same false line | row 2, on the WARNING assert | 1 failed, 67 passed | | R14 `:385` | `self._read_source(typed)[1]` to `self._read_source(typed)[1].strip()` in `no_path` | `'openXdox= '` is `REFUSED link-source-no-path`, exit 2 (main: 0) | row 2, on `returncode == 0` | 1 failed, 67 passed | | R5 `:438` | `return link, typed[at + 1:]` to `return typed[:at], typed[at + 1:]` | the NOTE says `reads OPEN=Y's own declaration` (and `OPENXDOX's`) | row 3, both parameters, on the NOTE assert | 2 failed, 66 passed | | M2 `:438` (rv1058) | `return link, typed[at + 1:]` to `return link.lower(), typed[at + 1:]` | the NOTE says `reads openxdox's own declaration` | row 3, `a-capital-inside-a-link` only, on the NOTE assert | 1 failed, 67 passed | Each mutant fails only the row it is named for, and the file passes (68) with the script restored. At the first head (`b775c739`, 67 tests) R10 was 2 failed, R1 1, R14 1 and R5 1, and M2 survived with 67 passed. `EMAIL=service@opensoft.dev`, a stub `gh` first on PATH (it refuses every call), `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`; through `$Q/v110/run9.sh` (`-q -rs -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp each). Never the full suite. - git 2.43.0 (`/usr/bin/git`), `tests/test_chain_remedy_callers.py` alone: 68 passed (63 before this PR, plus 5: row 1 has two parameters, row 3 has two, row 2 is one). - git 2.43.0, that file with `tests/test_link_source_chain_defaults.py` and the trio (`tests/test_repo_hygiene.py`, `tests/test_agents_md_codes.py`, `tests/test_cli_reference.py`): 261 passed. - git 2.47.1 (exactly `$Q/v115/w764/git247/bin/git`, first on PATH), `tests/test_chain_remedy_callers.py`: 68 passed. - The five new test ids on `8a057215`'s tree (a scratch copy of its `git archive`, with this file copied in): 5 passed. The whole file there: 66 passed, 2 skipped (the two older-library rows need history the scratch copy lacks). - `git diff --check` clean; no line over 79 characters. `git merge-tree --write-tree origin/main HEAD`, origin/main `833af144` (fetched with `-c gc.auto=0 -c maintenance.auto=false`, and `git ls-remote` agrees; main moved from `8a057215` by #1041, which touches `adopt-project.py`, `scaffold-project.py` and device-name tests, none of this PR's files): clean, exit 0, tree `bd706fdacef2`. The branch is two commits ahead of the old base `8a057215`. Review: rv1058, lane 5's independent reviewer, not the writer (w1054): READY at `b775c739` (issuecomment-6289571980) with no MUST or SHOULD; its two NITs were taken in one new commit, and the re-check is READY at `af390352` (issuecomment-6289945831), with no MUST or SHOULD and one optional docstring NIT. CI (Brett Heap's ruling 27): H0 is the head af39035 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 8a05721; and the local gate passed on its predicted squash tree 60843b9 over main 9e154d8: the chain_remedy_callers, repo_hygiene, agents_md_codes, cli_reference sets, 238 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #1054. Closes #1054. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fore an `=` that leaves a path, as a `--link-source` key is since #980, so `open=y=<path>` verifies the pin `open=y` and `open=y=` is refused as naming no path (#1053) (#1067) Found by lane 5's independent review of #1044 (#980), rv980, and filed as #1053. Two commits on origin/main `6476c170` (made on `8a057215`; the rebase was clean and `docs/cli.md` needed no regeneration): `2e6dd9e5`, the change, and `7ff61666`, fix round 1 (rv1067's SHOULD-1: one more test row, no code). Five files change (+396, -52): `templates/assembly-root/scripts/validate-pins.py`, `scripts/validate-repository-naming.py`, `docs/cli.md`, `tests/test_neutral_pin_validator.py` and `tests/test_link_source_chain_defaults.py`. Both scripts are pinned copies (`COPIED_VERBATIM` and `COPIED_FROM_SHAPE`), so every existing root takes two `upstream-changed` rows, `scripts/validate-pins.py` and `scripts/validate-repository-naming.py`, and re-pins. No digest of either file is kept anywhere in `tests/`: the texts that pin the help and the docstrings are held by `test_cli_reference.py` (regenerated) and `test_pinned_copy_text.py` (runtime), and both pass. `validate-pins.py` reads a `--pin-source` key as the naming gate has read a `--link-source` key since #980 (the longest declared pin before an `=` that leaves a path after it), so a pin whose name holds `=` can be a key, and the sentence saying the two copied tools read `[PRODUCT=]PATH` one way is true. This is consistency, not a live failure. No scaffold writes a pin holding `=` (`PIN_ARG_RE` admits `[A-Za-z0-9_.-]`), only a hand-edited `project.yaml` declares one, and a pin's digest stops a wrong source from passing: a wrong tree is a finding and never a pass. `PinSources.__init__` cut every value at its FIRST `=` (`typed.partition("=")`), and two more places cut at the first `=` on their own: the no-path refusal and `read_as_path`. - With `open=y` declared, `--pin-source open=y=<tree>` was a bare PATH that is not there: the `neutral-pin-source-unreadable` FINDING, exit 1. - With `open` and `open=y` both declared, `open=y=<tree>` was `open`'s, with the path `y=<tree>`. `open=y` read neither value, and of `open=<a>` and `open=y=<b>` the later replaced the earlier. - `open=y=` was a bare PATH, where `openGlass=` is refused `pin-source-no-path` (#442). - **`PinSources._read_source(typed)`** (new, `validate-pins.py`), returning `(product, before, path)`. It reads a key as the naming gate's `LinkSources._read_source` reads a link: the LAST `=` whose text names a declared pin by `pin_product_key`, with a non-empty path after it, ends the key. The key is read whole (never stripped, never cut at a `/`) and the pin is spelled as `project.yaml` spells it. A value that ends at a declared pin, with no shorter one leaving a path, is that pin's with an empty path. Anything else is a bare PATH, whole. - **`__init__`, the no-path refusal and `read_as_path` all use it**, so nothing cuts at the first `=` on its own. `self.names` (was a local) is the table it reads against. `read_as_path` keeps its words (#1054's wording improvement is out of scope): the text it names is `_read_source`'s `before`, which for a bare value is the text before the first `=`. - **Words made true:** the `PinSources` docstring (THE READING, the no-path paragraph, a new "THE TWO COPIED TOOLS READ `[PRODUCT=]PATH` ONE WAY" paragraph that now holds with no exception), the `--pin-source` help, and `docs/cli.md` regenerated from it by `scripts/render-cli-reference.py` (only that option's block changed). - **Item 5, the gate's `LinkSources` docstring** (`scripts/validate-repository-naming.py`, the only change in that file): the "still ends a key at the first `=`" sentence is gone, and so is the `casefold()` passage (false since #712: both tools key by `pin_product_key`) and the "one way OVER ASCII ONLY (#444)" clause. Four lines say what is true, and name `PinSources._read_source` (#1053). - **Tests, 17 new rows:** 14 in `tests/test_neutral_pin_validator.py` beside the #295 rows (section "#1053"), and 3 in `tests/test_link_source_chain_defaults.py` beside the #442 rows. A pin holding `=` cannot be scaffolded, so each row hand-writes the `neutral_product_pins:` entry and its pin file with the helper `declare_by_hand` (the link file imports it). The existing #442 row now builds its expected stderr through a shared `pin_source_no_path` helper, with the same assertion. The rows: - a declared pin `open=y` verified by `--pin-source open=y=<tree>`, as declared and in capitals; - `open` and `open=y` both declared, each given its own tree, in either order, and with a path that begins like the longer pin (`open=./y=dir`): both verify, neither replaces the other; - `open=y=` with `open` declared too is `open`'s, with the path `y=`, never refused; - `open=y=` and `OPEN=Y=` with only `open=y` declared are refused `pin-source-no-path`, exit 2, in `openGlass=`'s words; - a value naming no declared pin (a leading blank, an owner prefix, `open=z=x`) is a bare PATH, whole, and so is one that ENDS in an `=` no declared pin names (`open=z=`, rv1067's SHOULD-1), never refused and never read as a pin's; - the note for a bare value names the text before its first `=`; - a keyed `OPEN=Y=<tree>` names the pin as the manifest spells it, `open=y`, where the digest was never recomputed; - the old split's corners, `openGlass==x` and `openGlass= `, are still `openGlass`'s. `PinSources(values, declared)`, read at `8a057215` and at the head (`probe-base.log`, `probe-head.log`): declared [open=y], value open=y=/p at 8a05721 reading {'open=y=/p': None} (a bare PATH) with this reading {'open=y=/p': 'open=y'}, keyed to open=y with the path /p declared [open, open=y], values open=y=/p then open=/q at 8a05721 both 'open'; keyed {'open': ('open=/q', '/q')}, open=y read nothing with this {'open=y=/p': 'open=y', 'open=/q': 'open'}, each pin its own declared [open=y], value open=y= at 8a05721 a bare PATH, no refusal with this REFUSED pin-source-no-path controls, the same at both: openGlass= REFUSED pin-source-no-path; openss= with a declared open + sharp s is a bare PATH; ' open=y=x', 'opensoft/open=y=x' and 'open=z=x' with open=y declared are bare PATHs, whole. The final test files copied onto a clone of `8a057215`, the 17 new rows: **11 failed, 6 passed.** Each failure is on the defect (the source read as a bare PATH, or `open=y` read none). The 6 that pass pin readings that did not change, by design: the four bare-PATH rows (a leading blank, an owner prefix, no declared pin, and a value that ends at no declared pin) and the two old-split corners. The base and the head copies of `validate-pins.py` were run with `--root`, over the same root and directory, comparing stdout, stderr and exit code: 7 pin lists, none holding `=` (one pin, two, none, capitals, a sharp s, one variable, a scalar), and 40 values singly plus 182 ordered pairs of 14 values (capitals, a leading blank, an owner prefix, `./openGlass=x/openGlass`, the #295 rows' values). **1554 runs, 0 differing**; the head's exit codes were 0 (502), 1 (1037) and 2 (15). The rebase did not touch `validate-pins.py`. Each applied alone to a scratch tree's copy of the change, run against the three files that exercise `PinSources` (`test_neutral_pin_validator.py`, `test_link_source_chain_defaults.py`, `test_validate_pins_named_and_spelled.py`), through `run9.sh`. **17 of 18 killed; the 18th is equivalent.** The counts are the passes and fails of 408 tests (the files as of `2e6dd9e5`); R5, and M1, M2 and M16a rerun on the final files (409 tests), are noted under the table. | mutant | result | failed / passed | killed by (a few) | |---|---|---|---| | M1 back to `typed.partition("=")` | killed | 11 / 397 | the own-key, shorter-pin and both-declared rows | | M2 the shortest match instead of the longest | killed | 5 / 403 | the shorter-pin and both-declared rows | | M3 the "leaves a path after it" check dropped | killed | 1 / 407 | the shorter-pin row | | M4 the key stripped | killed | 1 / 407 | the bare-PATH row (a leading blank) | | M5 the key not folded by `pin_product_key` | killed | 21 / 387 | the capitals rows, the #295 rows | | M6 the no-path refusal back to its own first-`=` cut | killed | 2 / 406 | the two `open=y=` refusal rows | | M7 `read_as_path` back to `typed.partition("=")[0]` | survived | 0 / 408 | none: equivalent, below | | M8 `before` the longest text tried, not the shortest | killed | 8 / 400 | the note row, the corners row | | M9 the empty-path fallback dropped | killed | 10 / 398 | the refusal rows | | M10 the fallback keyed at the first `=` | killed | 2 / 406 | the two `open=y=` refusal rows | | M11 the key cut at its last `/` | killed | 2 / 406 | the owner-prefix row, the directory row | | M12 the keyed path read after the first `=` | killed | 8 / 400 | the both-declared and own-key rows | | M13 the scan skips an adjacent `=` | killed | 1 / 407 | the `openGlass==x` corner | | M14 a blank path names none | killed | 1 / 407 | the `openGlass= ` corner | | M15 the no-path refusal strips the path | killed | 1 / 407 | the `openGlass= ` corner | | M16a the keyed pin spelled as typed | killed | 2 / 406 | the manifest-spelling row | | M16b the no-path pin spelled as typed | killed | 2 / 406 | the capitals refusal rows | | R5 (rv1067's) the `if product is not None:` guard dropped from the empty-path fallback | killed, after the fix; it survived all 408 tests at `2e6dd9e5` | 1 / 408 | the bare-PATH row `ends-at-no-declared-pin` | **R5 and the fix round.** rv1067's mutant R5 returns `product, typed[:-1], ""` for every value ending in `=`, so `--pin-source open=z=` with `open=y` declared (or `x=`, `../build=` with `openGlass`) ended in an `AttributeError` traceback in `_shown(None)`. It survived because no row held a bare PATH ending in `=`. `7ff61666` adds the fourth value `open=z=` (id `ends-at-no-declared-pin`) to `test_a_value_naming_no_declared_pin_is_a_bare_path_whole`: it passes at `8a057215` and at the head, by design, and fails under R5 (1 failed, 408 passed). Rerun on the final files (409 tests): M1 killed 11 / 398, M2 killed 5 / 404, M16a killed 2 / 407. The code is unchanged by the round. **M7 is equivalent.** `read_as_path` says anything only for a value no declared pin is keyed to, and for such a value the scan's last turn is at the value's first `=`, so `_read_source`'s `before` IS the text before the first `=`: the two spellings print the same words for every value. Not a gap. At the committed head `2e6dd9e5` (the 88-file shards below; `7ff61666` changes one parameter of one test, run next), through `run9.sh` (gh stub first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`, no `LANES_*` variable): - **git 2.43.0** (`/usr/bin/git`): the 88 test files that name `validate-pins` or `validate-repository-naming`, which include the three mutant files, `test_chain_remedy_callers.py`, `test_pinned_copy_text.py` and the trio (`test_repo_hygiene.py`, `test_agents_md_codes.py`, `test_cli_reference.py`), in four shards: **1376 passed, 4 skipped; 1702 passed; 1963 passed, 2 skipped; 1812 passed, 6 skipped. 6853 passed, 12 skipped, 0 failed.** - **git 2.47.1** (exactly `$Q/v115/w764/git247/bin/git`, first on PATH): the three mutant files: **408 passed.** - **At `7ff61666`:** the three mutant files, **409 passed on git 2.43.0 and 409 passed on git 2.47.1.** - The rest of the suite is left to CI's `tests` job. From a root scaffolded at `8a057215` (`scaffold-project.py` with local remotes, cloned with submodules), `update-shape.py` at the head: - `check --at 2e6dd9e` lists `upstream-changed` for `scripts/validate-pins.py` and `scripts/validate-repository-naming.py` (`upstream-changed 2, unchanged 9`); - `apply --yes --branch shape/update-2e6dd9e56eb8` exits 0, copies both files, re-pins `8a0572157723 -> 2e6dd9e`, and the root's own validators print `pins ok` and `manifest ok: Atlas (atlas), 3 legs`; - a fresh `check` prints `unchanged 11` and `nothing to do`; the root's two copies are byte for byte the head's (sha256 `adc1f7d8...` and `fc5c9f81...`). `git merge-tree --write-tree` of the head `7ff61666` against: origin/main `9cbba9d5` clean (tree `d776b7d7`), and the one open PR that still touches one of these files, #1062 (`fix/1040-windows-start-errors`, `957ba038`, which edits `validate-pins.py` around `_git_runs`, not the `PinSources` region) clean (tree `aa6fc31b`). #1063 (`d996ebf1`, `docs/cli.md`) is no longer in the open list. At `2e6dd9e5` the same three were clean against `00f6d7f7`, #1062 `01a8b759` and #1063. Since the rebase main has changed `docs/cli.md` (8 lines added, 2 removed, outside the `--pin-source` block) and none of the other four files, and the merge-tree against it is clean, so the branch needs no rebase. Review: rv1067, lane 5's independent reviewer, not the writers (w1053 and f1053): READY at `2e6dd9e5` (issuecomment-6297358113) with no MUST; its SHOULD-1 was taken in one new commit, and the re-check is READY at `7ff61666` (issuecomment-6297678467), with no MUST or SHOULD. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 7ff6166 passed tests, tests-git-bash and SonarCloud; the head 5bc0d30 is a clean update-branch merge of main 6c86163, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 9a936b7 over main 6c86163: the link_source_chain_defaults, neutral_pin_validator, repo_hygiene, agents_md_codes, cli_reference sets, 235 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green". Closes #1053 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #980 (comment)
Closes #980.
Goal
For a declared-unverified chain link whose name holds
=, the naming gate offers only sources that verify that link's tree when typed as printed. The gate offeredopen=ythe source--link-source open=y=<path>, and read every--link-sourcekey at the value's first=: the key wasopen, which no chain names, so the value was a bare PATH for the first link,open=ystayed declared-unverified under the same offer, exit 0.What changed
The issue's direction (a), in
scripts/validate-repository-naming.py.LinkSources._read_sourcereads a--link-sourcekey the way_read_remedyreads a--link-remedylink since #949: the longest text before an=that names a link of a chain this run reads, bypin_product_key, and leaves a path after it.__init__andno_pathboth take the key and the path from it, so the path refused as empty is the one after the key's own=. For a chain whose links hold no=, every value is read exactly as before: a text holding=can only name a link that holds one.What (a) keeps from the documented rules, and how each case was checked (probes below, and one test row or more each):
=:open=y=<tree>verifiesopen=y, as printed and in capitals (OPEN=Y=<tree>, bypin_product_keyas every key is).openandopen=yin one chain):open=y=<tree>isopen=y's andopen=<tree>isopen's, in either order. On main both wereopen's and the later replaced the earlier, soopen=yread none.=:open=y=<dir>/a=b/treeisopen=y's with that whole path. A text holding/names no link, since a link's name isrepo_basename'd, so an=after a/never ends the key. A path that begins like a longer link's name is given after./:open=./y=dirisopen's in a chain namingopen=ytoo.open=y=isPRODUCT=foropen=y, refused aslink-source-no-path, exit 2, asopenXdox=is (scripts/validate-repository-naming.py_link_sources_from_args(:406) and templates/assembly-root/scripts/validate-pins.pyPinSources(:409): a key with an empty path (--link-source openXdox=,--pin-source openXdox=) isPath(""), which is the current directory, so run from a project root the naming gate reads the project's OWNproject.yamlas the link's declaration and reports a chain that holds as broken, a FINDING (exit 1) for a name with no direct pin #442). On main it was a bare PATH asked foropenXdox, a link the person did not name. Where a shorter link leaves a path (open=y=withopenin the chain too), that link is the key,openwith the pathy=, as--link-remedy openXdox=y=givesopenXdoxthe wordsy=where a chain namesopenXdox=y: the longest link that leaves a path wins, andopen=yleaves none.open=y=xbeside a directoryopen=y=xis stillopen=y's, decided by the chain and not the disk, with_also_a_directory's line;./open=y=xasks for the directory, as a bare PATH for the first link./:open=y=<tree>andopensoft/open=y=<tree>name no link and stay bare PATHs, as on main.One difference from
--link-remedy, deliberately.--link-remedy open=y=is a NOTE (its link falls back to the first=,open).--link-source open=y=is refused, because the #442 rule refusesPRODUCT=for every link, and--link-remedy's ownopenXdox=refusal is made by argparse before the chains are known, which is why it cannot catchopen=y=._read_source's docstring says so.Not (b). (a) makes the offer the gate already prints true, so the person who types it gets the verification; (b) would only stop offering it. Setup's step (4) is unchanged: it passes the gate its
--pin-sourceas an absolute path, with a separator before every=, so no text before one names a link.Docs. The module docstring (a new paragraph, "A KEY IS THE LONGEST LINK A VALUE NAMES"), the
LinkSources,_link_remedyand_read_remedydocstrings, the--link-sourcehelp, anddocs/cli.mdregenerated from it.setup-project.py'sas_the_plan_reads_itdocstring stated the first-=rule, and now states this one. README, the handbook and AGENTS.md state no rule for how a--link-sourcekey is read (README names--link-source PRODUCT=<path>only), so they are unchanged. The class docstring records thatvalidate-pins.pystill ends a--pin-sourcekey at the first=; no scaffold writes a pin holding one (--pinnames are[A-Za-z0-9_.-]), and it is left as its own change.What a project sees (the upgrade row)
Measured from a root scaffolded at main
5b41d44f(local remotes, no network):update-shape.py check --upstream <this checkout> --at c64e85fealistsupstream-changed scripts/validate-repository-naming.py, withupstream-changed 1 unchanged 10.update-shape.py apply --yes --branch ...exits 0, commitscontracts/shape-pin.yaml,project.yamland the copy, and the project's validators printpins okandmanifest ok. A freshcheckprintsunchanged 11, and the root's own naming gate exits 0.scripts/validate-repository-naming.py:ec167fb0e259f90705db895a1a37f4adebd4cbf03ca2d5ae18f6d9a8df896548(78664 bytes). No other pinned copy and no template changes.Probes, before and after
Run from empty folders,
<D>a fresh folder, trees as named (trees/ydeclaresopenInk,trees/xdeclaresopen=y; for the chain withopen,trees/x2declaresopenandtrees/odeclaresopen=y). The gate prints an arrow between links; it is written->here.5b41d44fc64e85fe--referent-chain openXdox,open=y,openInk --link-source open=y=<D>/trees/ylink-source-unreadable ... could not answer for openXdox, "It was read as a PATH:openis not a link", andopen=ydeclared-unverified with the same offer; exit 0openXdoxdeclared-unverified (no source named it); nolink-source-unreadable; exit 0openXdox=<D>/trees/x,--explainCHAIN openXdox -> open=y -> openInk [declared-unverified][verified], nothing warned or notedopenXdox,open,open=y,openInk,open=<o>thenopen=y=<y>link-source-unreadable: y=<D>/trees/y (--link-source open=...), NOTEopen=<o>replaced; 2 links declared-unverified[verified], nothing warned or notedopen=y=(chain withoutopen)open=y=asked foropenXdox; exit 0REFUSED link-source-no-path: --link-source open=y= names no path for open=y; exit 2open=y=(chain withopen)y= (--link-source open=...) could not answer for openopen=y=<D>/trees/a=b/y[declared-unverified][verified]open=y=xbeside a directoryopen=y=xopenXdoxlink-source-unreadable: x (--link-source open=y=...) could not answer for open=y, then "open=y=xis also a directory;--link-source ./open=y=xreads it as one."./open=y=xopenXdoxopen:open=./y=dir,open=y=<y>open=y=<y>read foropen, replacingopen=./y=dir[verified]Tests
Eleven new rows in
tests/test_chain_remedy_callers.py, section "#980", reusing its helpers (naming,tree,gate_warnings,UNREACHED,GATE_OFFER,EQUALS), with no assert that joins conditions withand:test_the_offer_for_a_link_holding_an_equals_verifies_it[as-printed, in-capitals, a-path-holding-an-equals]: reads the offer the gate prints foropen=y, types it with a tree, and asserts the one WARNING left isopenXdox's, with nolink-source-unreadableand no NOTE.test_each_link_reads_its_own_source_where_one_name_begins_another[open-first, open=y-first, a-path-that-begins-like-the-longer-link]test_a_value_that_ends_at_a_link_holding_an_equals_names_no_pathtest_a_shorter_link_that_leaves_a_path_is_the_keytest_a_link_holding_an_equals_wins_over_a_directory_spelled_like_ittest_a_key_is_read_whole[a-blank, an-owner]Failing at main. The test file copied onto a
git archiveof5b41d44f: 8 of the 11 fail, each on the defect (the offer row'sopen=yWARNING andlink-source-unreadable, the prefix rows'[declared-unverified],open=y=exiting 0, the keyed reading not winning over the directory). The other 3 pass at main by design: they pin behaviour main already had (test_a_shorter_link_that_leaves_a_path_is_the_key,test_a_key_is_read_whole[a-blank]and[an-owner]), and each kills a mutant below.Mutants, each applied alone to the fixed gate in a scratch repository, then
test_chain_remedy_callers.py,test_link_source_chain_defaults.py,test_naming_policy.py,test_chain_link_fold.pyandtest_chain_pin_sources.py(400 passed, 4 skipped with no mutant). All 11 killed:typed.partition("=")open=y=, the directory rowtest_a_shorter_link_that_leaves_a_path_is_the_key./PATHrule dropped (./stripped from the key)test_a_labelled_source_holding_an_equals_is_not_said_to_be_a_path;test_naming_policy.py::test_a_link_named_before_the_equals_is_the_key_even_over_a_directory/, as a remedy's link istest_a_key_is_read_whole[an-owner]open=y=;test_a_value_that_names_no_path_is_spelled; 4 rows oftest_link_source_chain_defaults.py::test_a_link_source_that_names_no_path_is_refused=, as a remedy'sopen=y=no_pathtaken after the first=open=y==test_a_key_is_read_whole[a-blank]pin_product_keyin-capitalsand the prefix rows, and 52 existing rowsRuns, all on head
c64e85fe,/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed, one reused basetemp per run, withEMAIL=service@opensoft.dev, a stubghfirst on PATH,GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=falseandGIT_CONFIG_GLOBAL=/dev/null:/usr/bin/git), 31 files: the 25 that run the naming gate (grep -l validate-repository-naming tests/, the pinned-copy filestest_pinned_copy_text.py,test_update_shape_kept_helper.py,test_update_shape_unreadable.pyandtest_adopt_reencoded_files.pyamong them, andtest_repo_hygiene.py), the files that scan every script's source (test_git_output_decoding.py,test_shape_advisory.py,test_no_auto_maintenance.py,test_non_utf8_git_failures.py), and the trio's other two,test_agents_md_codes.pyandtest_cli_reference.py: 2964 passed, 5 skipped in 1749 s, exit 0.No template changes.
grepoftests/found no fixed digest or^id:-style pattern over the gate: the tests that read a root's copy of it (test_shape_doctor.py,test_doctor_validator_verdicts.py) compare it at run time, and both are in the first run, as are the source scanners above andtest_cli_reference.py, which holdsdocs/cli.mdto the help.Merge-trees
git merge-tree --write-tree origin/main HEAD, origin/main5b41d44f(fetched with-c gc.auto=0 -c maintenance.auto=false, andgit ls-remoteagrees): clean, tree82780dc63d7f, the head's own tree.family bootstrap okbeside git's exit 128 (#892) #954, the family root's bootstrap.py and siblings.py: under GNU make 4.4's named-pipe jobserver, a member's make that says it is 4.4 or later is handed the holder'sMAKEFLAGSas it is, so it runs its jobs together again; every other make still gets no jobserver and no-j(#987) #1023, validate-family.py: a hand mount'sgit rmline works typed:git rm --cachedwhere a plain one refuses a staged gitlink, dies on a clone no section names or refuses what is no checkout, with what it leaves said, and a.gitmodulesgone from the work tree put back before thegit rmthat takes a section out (#936) #1025, family.py:bump's fetch leaves out every refspec of the mount's whose source can name a tag, a pattern such asrefs/*and a short name too, so a mirror in a folder,+refs/*:refs/remotes/origin/all/*, no longer fails the fetch on a branch and a tag of one name or pins a commit only a tag reaches (#1009) #1027, validate-family.py, validate-pins.py and both bootstrap.py copies: agitWindows cannot execute ([WinError 193], a namelessOSError) isgit-not-runnable, exit 2, with the put-git-on-PATH remedy, where each ended in a traceback, exit 1, and one test holds the four copies to one answer and one text (#1001) #1028, adopt-project.py: the split commit's message andplan's report name each path, question and rule id one way only and never raw, the history scan'sgit-failedis one line, and the table pads a name as stdout prints it (#900) #1030, adopt-project.py:check's throwaway repositories are made in the work directory with the source's object format and no hook of this machine's; a path git refuses isplan-path-git-refuses, a repository git cannot make thereadopt-git-unasked(#910) #1031, adopt-project.py, shape_materialize.py and standard.py: the Free-plan hint no longer calls an internal root private, adopt's docstring says its note for an internal root with an unread plan changed in #969, andprepare's docstring has no 104-character line (#994) #1038, family.py and tests:Family.rewritten's refusal of twomembers:lines is asked of the method again, its docstring says no verb reaches that count since #974, and a refusal that leaves a ref behind failsassert_unchanged(#1029) #1039) touchesscripts/validate-repository-naming.py,tests/test_chain_remedy_callers.py,setup-project.pyordocs/cli.md, so there is no merge-tree to run against them.🤖 Generated with Claude Code
Summary by Sourcery
Fix --link-source parsing so printed verification offers work for chain links whose names contain
=, while documenting and testing the updated behavior.Bug Fixes:
=by selecting the longest matching link that leaves a non-empty path.Enhancements:
=, directory ambiguity, empty paths, and literal key handling.Documentation:
=.Tests:
=.