Repository navigation
scaffold-project.py: a --referent-chain link between the first and the last that is not a neutral product's name is refused before anything is created, under --dry-run alike, so openXdox,a #b,openDox no longer creates three repositories and pushes a manifest the project's own validate-manifest.py refuses as yaml-unparsable (#251) - #271
Conversation
…the last that is not a neutral product's name is refused before anything is created, under `--dry-run` alike, so `openXdox,a #b,openDox` no longer creates three repositories and pushes a manifest the project's own `validate-manifest.py` refuses as `yaml-unparsable` (#251) `naming_block` writes the chain into `project.yaml` as a plain flow sequence, and nothing looked at a MIDDLE link: `_chain_shape_problem` asks the length, a repeat, the first link (a `--pin` product) and the last (the referent the name claims), and a link whose tree is not on the disk is declared-unverified, a warning. On 150c41d `openXdox,a #b,openDox` exited 0 with all three repositories made and pushed and a manifest `validate-manifest.py` refused, and `it's`, `a]b`, `"q"`, `null` and `007` were each recorded as a different chain (read back as `it's, openDox`, `a]b, openDox`, `q`, None and 7). - `scaffold-project.py`: `_refuse_malformed_chain_links` refuses, as `chain-link-malformed` with a remediation, every link between the first and the last whose basename the naming policy does not classify as `neutral-product`: each such link is an entry of the link before it's `neutral_product_pins:`, so the policy's `open<Product>` form is what a link is, and a name of that form is one plain YAML word. It runs before `_declaration`, so before `gh api` or git reads a pin's tree and before the `--dry-run` return. `referent_chain_links` is the one reading of the argument, for the check and for `_declaration`. The first and last links keep their casefolded anchors and are unchanged. `--help` says so. - `docs/cli.md`: regenerated. - `tests/test_scaffold_referent_chain_links.py` (new, 30 tests). Closes #251 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in with `repr`, so a link holding a line break cannot forge a line of it, and the remediation's pattern and `--explain` hint and the one reading of the argument are now pinned by tests Review round 1 of the #251 change (APPROVE WITH NITS, no blocker) left these code-side items; all five are fixed here. The PR body edits it asked for are made in the PR description, not in this commit. - The refusal printed `--referent-chain <chain>` raw, so a link such as `a\nREFUSED fake` put a second line beginning `REFUSED fake,...` on stderr, and an ESC byte reached the terminal. The chain is now quoted with `repr`, the spelling `--pin` and `--id` already use in their refusals and the one each bad link already had. A newline, a carriage return, an ESC byte and U+2028 print as `\n`, `\r`, `\x1b` and ` `, and the refusal is two lines: the refusal and its remediation. This is the one escaping `scaffold-project.py` uses for an operator's argument; the other helpers (`repo_shape.double_quoted`, `adopt-project.py`'s `_spelled`) write a YAML scalar and a backticked path, which this line is neither. - A comment in the new test file cited a file in a lane's scratch directory, which no reader of the repository can open. The citation is removed. - "it's" for "its" in two comments of `scaffold-project.py` and in the new test file's docstring. The first commit's message keeps its own: a pushed commit is not amended. - The remediation's quoted policy pattern and its `--explain <link>` hint were pinned by no test, so dropping either passed. A test now asserts both against the policy's own `neutral-product` pattern, and another runs the named command and checks it says which form a name is. - `referent_chain_links`, the one reading of the argument that the check and `_declaration` share, was pinned by no test, so unstripped links or a check with its own split passed. Tests now cover the function directly (plain, padded, empty links, empty argument), a refusal that names a padded bad link as it is recorded, and two end-to-end scaffolds whose chains are padded or hold empty links, which record the same three links. The new file holds 44 tests, up from 30. Each of these, applied to `scaffold-project.py` alone, fails at least one of them: the pattern dropped from the remediation, the `--explain` sentence dropped, links not stripped, the check reading its own unstripped split, the chain printed raw, and a bad link printed raw. Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-11T23:05Z) @codex review |
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 1 day and 1 hour by commenting |
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. |
Reviewer's GuideThe PR prevents invalid referent-chain middle links from producing malformed manifests by validating their basenames against the neutral-product naming policy at the earliest point in scaffolding, with consistent diagnostics, documentation, and extensive regression tests while retaining existing endpoint behavior. Sequence diagram for early referent-chain validationsequenceDiagram
participant CLI as scaffold-project.py
participant Policy as NamingPolicy
participant Forge as gh api or git
participant RepoCreate as Repository creation
participant Manifest as Manifest writer
CLI->>Policy: load naming policy
CLI->>CLI: referent_chain_links(raw)
CLI->>Policy: matches(repo_basename(link))
alt malformed middle link
Policy-->>CLI: not neutral-product
CLI-->>CLI: Refusal chain-link-malformed
CLI-->>CLI: nothing created
else valid middle links
Policy-->>CLI: neutral-product
CLI->>Forge: read pin trees
CLI->>RepoCreate: create repositories
CLI->>Manifest: record referent_chain
end
Flow diagram for referent-chain link validationflowchart TD
A[Load naming policy] --> B[Parse referent_chain_links]
B --> C{Check middle links by basename}
C -->|All match neutral-product| D[Continue scaffolding]
C -->|Any link is malformed| E[Refuse chain-link-malformed]
E --> F[Report quoted chain and bad links]
F --> G[Nothing created]
D --> H[Read pin trees and create repositories]
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: Didn't find any major issues. Bravo. 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". |
…16ae) and this branch both add a sentence to scaffold-project.py's WHAT IT DOES, resolved as their union; the two refusals' code merged without conflict (#241) Main took 23e16ae (#271, issue #251: a `--referent-chain` link between the first and the last that is not a neutral product's name is refused as `chain-link-malformed`) after this pull request was green, and GitHub then reported it DIRTY. The one conflict was the module docstring's WHAT IT DOES item 1, where each side added a sentence after "just as early, for the same reason (#197),". Resolved as the union, both refusals named in one sentence in landing order: a `--pin` whose file the shape writes itself or another `--pin` writes (#241), and a `--referent-chain` link between the first and the last that is not a neutral product's name (#251). Every other hunk of scaffold-project.py auto-merged, and docs/cli.md is main's own: this branch changed no help text, and `render-cli-reference.py --check` says it is what the generator produces. THE TWO REFUSALS' ORDER in the merged `_scaffold`, which no conflict marker showed: `_refuse_colliding_pins` (#241) first, then `_refuse_colliding_leg_paths`, `_identity`, then `_refuse_malformed_chain_links` (#251), `_declaration` and `_check_declared_names`, with `--dry-run` returning after all of them. Both branches' tests keep holding: #256's pin refusal still comes before the leg path check, `_identity` and `_declaration`, and #251's still comes before `_declaration` reads a pin's tree. Neither branch's tests pin an order between the two refusals. BOTH AT ONCE, probed once on the merged tree with local remotes and a `gh` that exits 97 (never called): `--pin spec@<sha> --pin openXdox@<sha> --referent-chain 'openXdox,a #b,openDox'` is ONE refusal, `scaffold-pin-collides`, exit 2, nothing created, under `--dry-run` alike and whichever order the arguments are typed in. The `chain-link-malformed` refusal is not shown until the colliding pin is fixed; the same chain with only a clean pin is refused as `chain-link-malformed` on its own. The result is deterministic, and the operator meets the two mistakes one re-run apart. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, #272 via #274, #205 via #232) into fix/adopt-record-submodule-placement Brings in 23e16ae (#251), 60588d5 (#206), 018ce0d (#207), 67a398a (#272) and d94d155 (#205) on top of 5751460. No rebase. The first four change none of this branch's files. d94d155 changes shape-doctor.py and tests/test_shape_doctor.py, as this branch does. One conflict, in shape-doctor.py's module docstring, under EXIT CODES. Both sides added a clause to exit 1's list of findings: #205 "a manifest is there and does not load", and #189 "or, for a submodule, a leg other than the one `project.yaml` records for it". Both are kept: #205's in its place in the list, and #189's after the path-policy clause it qualifies. Only the docstring's first line reaches `--help`, so docs/cli.md is unchanged. Merged without a conflict, and checked: - `check_placement`: #205 adds its `waiting_row` gate after the family-holder return and before `ctx.legs()`. The merged function is this branch's plus exactly those eight lines. So a `project.yaml` that does not load makes the placement row `n/a`, waiting on the `manifest` row, before the record is read. The record is read from `ctx.manifest`, which #205 now loads once, with the same `load_yaml`, so a manifest that loads gives the record it always gave. - #205 removes `Context._declares`, and nothing on this branch names it. This branch leaves `placement_row` unchanged, and #205's gate calls it as (NA, reason, None, detail). - No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, shape-doctor.py, validate-manifest.py or update-shape.py. Each merged module's names are the base's plus what either side added. Every definition only one side changed is that side's text. The two that both changed are the docstring above and `check_placement`. - These commits leave adopt-project.py, scripts/repo_shape.py and scripts/shape_materialize.py untouched. So the record's writer and reader are the ones the round trip and the revert table ran on at a01ab02, and the record is still spelled by `double_quoted`. - AGENTS.md is untouched: 472 lines, under the cap of 472. - The branch's diff against main is still #189 alone: 12 files, +1670 -157. The one deletion more than against 5751460 is the EXIT CODES sentence above, which now rewraps two of #205's lines. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, #209, #252, #195, #210, #211, #248, #212, #251 and #206 merge with one hunk, the materializer import list, kept on both sides, and `_entry_findings` still hands `_leg_findings` the tree and ends with the root-symlink findings origin/main moved 39ac59c -> 60588d5: 06b5204 (#214 via #246), 60d08a1 (#217 via #230), 2727b08 (#209 via #218), 4b2add8 (#252 via #257), aaa40eb (#195 via #226), d89fffa (#210 via #219), eb6df75 (#211 via #220), c3f4987 (#248 via #265), 5751460 (#212 via #223), 23e16ae (#251 via #271) and 60588d5 (#206; its subject names no pull request). Conflict, resolved by keeping both sides: - adopt-project.py, the `shape_materialize` import list. This branch adds BOOTSTRAP, SYMLINK_HOPS and template_files; #214 adds escape_what_stdout_cannot_encode. The resolution is the union. Auto-merged: scripts/shape_materialize.py, where #214's escaping, #217's UTF-8 `run()` and #212's `first_and_more(paths, spell=None)` sit apart from this branch's #183 section, and adopt-project.py's other hunks. #195 adds `import os` at the line where this branch added it, and the merge keeps it once. Checked on the merged tree: - `_entry_findings` still calls `_leg_findings(plan, tree)` and ends with `_root_symlink_findings(plan, source, tree)`. `main` still calls `escape_what_stdout_cannot_encode()` first. - No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py or scripts/shape_materialize.py, and none is loaded but bound nowhere (AST). `first_and_more` gives its new `spell` a default, so this branch's call is unchanged. - Both sides are intact by +/- line sets: each side's change against the base reappears against the other side, apart from the import hunk and the shared `import os`. - #212 says this branch's `plan-root-symlink` names its paths raw, and that whichever pull request lands second should spell them. That was 04792d2. Since d11cf9f every path the finding names goes through `_spelled`. One clash the textual merge does not show is fixed in the next commit: #217's tests/test_git_output_decoding.py refuses a shipped text-mode `subprocess` call that names no codec, and this branch's `_checks_out_symlinks` probe names none. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #251 (comment)
What
scaffold-project.py --referent-chainnow refuses, aschain-link-malformed, any link betweenthe first and the last whose name the naming policy does not classify as
neutral-product(
open<Product>). The refusal names every such link, says nothing was created, and gives thespelling to use. It runs before
gh apior git reads a pin's tree, and under--dry-runalike. (With no
--elected-byand noGIT_AUTHOR_NAME, resolving the elector runsgit config user.namefirst.)scaffold-project.py: new_refuse_malformed_chain_links(policy, chain), called in_scaffoldright after the policy is loaded and before_declaration. That is beforegh apior git reads a pin's tree, before the--dry-runreturn, and before the workdirectory and the remotes exist. A link is judged at its basename, the way
resolve_referentreads it and
naming_blockrecords it. Newreferent_chain_links(raw)is the one placethe argument is read, used by the check and by
_declaration, so the links checked are thelinks recorded.
--helpfor--referent-chainsays the rule. The first and last links areunchanged. The refusal quotes the chain and each bad link with
repr, as--pinand--iddo in their refusals, so a link holding a newline, a carriage return or an ESC byte prints
as
\n,\ror\x1band cannot start a second line that reads as a refusal.docs/cli.md: regenerated withscripts/render-cli-reference.py(one help paragraph).tests/test_scaffold_referent_chain_links.py: new, 44 tests (30 in the first commit, 14 infix round 1).
A behaviour change for input that used to work. A middle link that is not of the form
open<Product>, with an uppercase letter afteropen(or afteropenx), is now refused. Thatincludes a case variant such as
openydoxorOPENYDOX, which main accepted:openXdox,openydox,openDoxscaffolded on main (the review ran it, against a layer whereopenXdoxdeclaresopenYdox, andvalidate-manifest.py,validate-repository-naming.pyandvalidate-pins.pyall exited 0), because a chain link is compared casefolded elsewhere. Thereason is that nothing else anchors a middle link: the run compares it with no pin and no
referent, so only its form can say what it is, and the policy's uppercase rule is what keeps
opensoft(the organisation login) from reading as a product. The refusal names the link and the spelling to use. The first and lastlinks are still compared casefolded, as before, because each is anchored to a name this run
holds.
Why
naming_blockwrites the chain as a plain flow sequence,referent_chain: [openXdox, openDox],and nothing checked a middle link.
_chain_shape_problemchecks the chain's length, repeats,the first link and the last. A link whose tree is not on the disk is declared-unverified, which
is only a warning. On 150c41d, scaffolding
codexDox(pinopenXdox,--pin-sourcea layerdeclaring
openDox) exited 0 and created and pushed all three repositories for every one ofthese chains:
--referent-chainload_yamlopenXdox,a #b,openDox[openXdox, a #b, openDox]REFUSED yaml-unparsable;validate-manifest.pyexit 2openXdox,it's,openDox['openXdox', "it's, openDox"]openXdox,a]b,openDox['openXdox', 'a]b, openDox']openXdox,"q",openDox['openXdox', 'q', 'openDox']openXdox,null,openDox['openXdox', None, 'openDox']openXdox,007,openDox['openXdox', 7, 'openDox']openXdox,Atlas,openDoxOn this branch, each of those exits 2 with
REFUSED chain-link-malformed, and no remotedirectory exists afterwards. The control
openXdox,openDox, a three-linkopenXdox,openYdox,openDox,opensoft/openXdox,openDox,OPENXDOX,openDoxandopenXdox,OPENDOXstill scaffold. The written projects'validate-manifest.pyandvalidate-repository-naming.py --projectexit 0.Why refuse, and not quote (the issue's two options). The naming policy already defines a
link. Every link after the first is read from the previous link's
neutral_product_pins:(
referent.chain.link_declared_byincontracts/repository-naming.yaml). The example chainsthere are
[openXdox, openDox]and[openXwallet, openWallet], andvalidate-manifest.pysays a recorded chain is "expected a list of neutral-product names". A name in the
neutral-productform is a single plain YAML word, so the manifest stays plain, and it isstill true that no writer quotes a flow item. Quoting would record
a #bfaithfully: a linkthe scaffold's own check calls broken, naming a repository that cannot exist. It would also
make this writer depend on the quoted-flow-item reader, which #249, #250 and #252 are changing
now. Checking only the repository-name alphabet is not enough either:
null,007andtrueare valid GitHub names, and the reader resolves them as None, 7 and True.
Why only the middle links. The first link must casefold to a
--pinproduct(
[A-Za-z0-9_.-]). The last must casefold to theopen<Product>that the project's name claims.Neither can therefore hold a
#, a quote, a bracket, a comma or a space, and the last cannot bea YAML bool, null or number. Both comparisons ignore case on purpose: a pin names which
repository, not how it was typed (
_pinned_basenames). RefusingOPENXDOXat an end would add a newspelling rule that this issue does not ask for. A test keeps the two ends as they were. A middle
link has no such anchor, so it is judged by its form, and therefore by its case (see the
behaviour change above).
Where else a chain is accepted.
adopt-project.pypasses no chain tonaming_block(:2887-2890).
scripts/family.pyandupdate-shape.pytake none.setup-project.pypasseseverything after
--to the scaffold, so it gets this check.validate-repository-naming.py --referent-chaintakes a chain but writes nothing, so it is unchanged.Evidence
5 passed. 23 in-process refusal tests fail with
ProcessStarted: ['gh', 'api', 'repos/opensoft/openXdox/git/trees/111…']: the run went past the chain to read the pin'stree. That
ghwas intercepted and never ran. The 2 end-to-end issue rows exit 0 with threebare remotes. The 5 that pass are the positive and unchanged-ends tests.
[1:-2]: 25 failed;null,Atlas,opensoft,OPENYDOX);_declaration: 23 failed (every in-process refusal hitsgh api);test_cli_reference.py3 failed;_declarationreading the argument inline again: 0 failed, as expected, because thatchange preserves behaviour.
*.yml/*.yamlfiles load the same on 150c41d and on this head(7 ok, and 7 templates refused on their unrendered
{{...}}).repo_shape.pyis nottouched.
naming_blockcalled from the scaffold is the only writer ofreferent_chain.git-filter-repo2.47.0 on PATH:tests/test_scaffold_referent_chain_links.py tests/test_scaffold_e2e.py tests/test_cli_reference.py tests/test_repo_hygiene.py tests/test_naming_policy.py:313 passed.
tests/test_scaffold_pin_and_reuse.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_leg_paths.py tests/test_scaffold_ignored_shape_files.py: 166 passed.scripts/render-cli-reference.py --checkprints
[ok],git diff --checkis clean, and no added line holds a host or scratch path.scaffold-project.pyalone with the whole new file run(no
-x), all caught: the policy pattern dropped from the remediation (1 failed), the--explainsentence dropped (1), links not stripped (3), the check reading its ownunstripped split (3), the chain printed raw (26: the 22 refusal rows that assert the quoted
chain, and the 4 line-break rows), and a bad link printed raw (7: the two single-quote
rows, the two-bad-links refusal and the 4 line-break rows).
files named above. Fix round 1 changed three comment or docstring lines and the refusal's
own message in
scaffold-project.py, and the new test file.tests/test_scaffold_referent_chain_links.py tests/test_scaffold_e2e.py tests/test_scaffold_pin_and_reuse.py tests/test_naming_policy.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_leg_paths.py tests/test_scaffold_ignored_shape_files.py: 402 passed.tests/test_yaml_flow_quotes.py tests/test_load_yaml_refusals.py tests/test_adopt_plan_escapes.py tests/test_manifest_validator.py tests/test_pin_validator.py tests/test_neutral_pin_validator.py tests/test_repo_hygiene.py tests/test_cli_reference.py tests/test_adopt_plan.py: 275 passed, 1 skipped. The skip istest_adopt_plan.py:393:SHAPE_MEDXEHR_CLONEis unset.tests/test_shape_doctor.py: 181 passed._build_assembly_rootrunsgit submodule addwithout--force, so a leg path an ignore rule matches (--code-path __pycache__or.DS_Storeunder the shape's own.gitignore, or the defaultspecunder an operator's global excludes) passes--dry-run, then dies at the mount AFTER all three repositories exist and both legs are pushed #229):tests/test_scaffold_referent_chain_links.py tests/test_scaffold_mount_ignored.py tests/test_scaffold_pin_and_reuse.py tests/test_cli_reference.pygave 105 passed.Merges
Re-checked at 5cb1d3e after fix round 1:
git merge-tree --write-treeagainstorigin/main4b2add8,
fix/yaml-block-key-nul9be6287 andfix/yaml-quote-boundaries21fc213 is clean foreach.
fix/yaml-flow-mapping-keys(#252) has no branch on origin or in the shared repositorynow; its earlier tip 2438190 is still in the object store and is clean too.
tests/test_scaffold_referent_chain_links.py tests/test_cli_reference.py tests/test_repo_hygiene.pyon the throwaway merge of 5cb1d3e with 4b2add8: 107 passed.git merge-tree --write-treeof 114bec9 against each sibling branch and against main:all clean, checked right before the first push.
fix/yaml-quote-boundaries(scripts/repo_shape.py treats a quote character in the MIDDLE of a plain scalar as opening a quote, sok: [it's, a]reads["it's, a"]where its block twin reads["it's", "a"],k: it's fine # notekeeps the comment in the value, and #244 turnsk: [a"b\", c]from right (by accident) into one item #249, scripts/repo_shape.py_scalartakes any text that starts and ends with a quote for a quoted scalar and anything else for plain text, so an unterminated"abcreads as"abc,- "a\"reads asa\, and"a" b "c"reads asa" b "c, where PyYAML refuses each #250) at 150c41d, so no commit yet: clean. It needs afresh check once it has commits.
fix/yaml-block-key-nul(scripts/repo_shape.py: the double-quoted reader that #247 adds (fix/adopt-plan-linebreak-names, not yet on main) resolves\x00inside a block-scalar KEY, and_parse_mappingsplits the NUL-joined sentinel record, so"a\x00b": |reads as the keyawith the bodyb<NUL>textwhere main reads the literal keya\x00b#248) at 9be6287: clean.fix/yaml-flow-mapping-keys(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) at 2438190: clean.origin/mainat 39ac59c (scaffold-project.py_build_assembly_rootrunsgit submodule addwithout--force, so a leg path an ignore rule matches (--code-path __pycache__or.DS_Storeunder the shape's own.gitignore, or the defaultspecunder an operator's global excludes) passes--dry-run, then dies at the mount AFTER all three repositories exist and both legs are pushed #229 via scaffold-project.py: the leg mount passes--forcetogit submodule add, so a leg path an ignore rule matches (__pycache__,.DS_Store, or the defaultspecunder an operator's own excludes) scaffolds instead of dying at the mount after all three repositories exist and both legs are pushed (#229) #235, which also editsscaffold-project.py, in_build_assembly_root): clean, and its tests pass on the merged tree (above). This changedoes not touch
scripts/repo_shape.py, so none of the reader fixes overlap it.Left out
--pinaccepts a product name the manifest reader resolves as something otherthan a string.
--pin null@<sha>,007andtrueeach scaffold with exit 0, and theproject's own
validate-pins.pythen exits 1 withneutral-pin-entry-malformed: … entry None(or
7,True). It reaches the chain's FIRST link too:--pin null@<sha> --referent-chain null,openDoxscaffolds three repositories, recordsreferent_chain: [null, openDox], readsback
[None, 'openDox'], andvalidate-pins.pyexits 1. That writer belongs to--pin, not tothe chain, and the eventual fix should cover the first link. It is a candidate follow-up issue.
validate-manifest.pyandvalidate-repository-naming.pydo not flag a recorded middle link that is not aneutral-product name, such as
[openXdox, Atlas, openDox]written by an older scaffold or byhand (all three validators exit 0 on it). Both are shipped, digest-pinned copies, and neither
writes anything. The policy file also says the chain rule is data, while the middle-link form
is a constant in
scaffold-project.py(CHAIN_LINK_FORM). It is a candidate follow-up issue.<owner>/before a link is still dropped without being checked, for every link, as_chain_linkshas always done. The manifest records basenames.nothing on stdin, and write the stand-in repositories' files as bytes.
Closes #251
🤖 Generated with Claude Code
Summary by Sourcery
Validate intermediate referent-chain links before scaffolding so malformed chains are refused safely and cannot create invalid repositories or manifests.
Bug Fixes:
--referent-chainbefore any repositories, work directories, or external reads are created, including during dry runs.Enhancements:
Documentation:
Tests: