Skip to content

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

Merged
brettheap merged 2 commits into
mainfrom
fix/scaffold-referent-chain-links
Oct 11, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-5 (openRepoShape-5)
Claim: #251 (comment)

What

scaffold-project.py --referent-chain now refuses, as chain-link-malformed, any link between
the 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 the
spelling to use. It runs before gh api or git reads a pin's tree, and under --dry-run
alike. (With no --elected-by and no GIT_AUTHOR_NAME, resolving the elector runs git config user.name first.)

  • scaffold-project.py: new _refuse_malformed_chain_links(policy, chain), called in
    _scaffold right after the policy is loaded and before _declaration. That is before
    gh api or git reads a pin's tree, before the --dry-run return, and before the work
    directory and the remotes exist. A link is judged at its basename, the way resolve_referent
    reads it and naming_block records it. New referent_chain_links(raw) is the one place
    the argument is read, used by the check and by _declaration, so the links checked are the
    links recorded. --help for --referent-chain says the rule. The first and last links are
    unchanged. The refusal quotes the chain and each bad link with repr, as --pin and --id
    do in their refusals, so a link holding a newline, a carriage return or an ESC byte prints
    as \n, \r or \x1b and cannot start a second line that reads as a refusal.
  • docs/cli.md: regenerated with scripts/render-cli-reference.py (one help paragraph).
  • tests/test_scaffold_referent_chain_links.py: new, 44 tests (30 in the first commit, 14 in
    fix 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 after open (or after openx), is now refused. That
includes a case variant such as openydox or OPENYDOX, which main accepted:
openXdox,openydox,openDox scaffolded on main (the review ran it, against a layer where
openXdox declares openYdox, and validate-manifest.py, validate-repository-naming.py and
validate-pins.py all exited 0), because a chain link is compared casefolded elsewhere. The
reason 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 last
links are still compared casefolded, as before, because each is anchored to a name this run
holds.

Why

naming_block writes the chain as a plain flow sequence, referent_chain: [openXdox, openDox],
and nothing checked a middle link. _chain_shape_problem checks 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 (pin openXdox, --pin-source a layer
declaring openDox) exited 0 and created and pushed all three repositories for every one of
these chains:

--referent-chain recorded read back by load_yaml
openXdox,a #b,openDox [openXdox, a #b, openDox] REFUSED yaml-unparsable; validate-manifest.py exit 2
openXdox,it's,openDox as typed ['openXdox', "it's, openDox"]
openXdox,a]b,openDox as typed ['openXdox', 'a]b, openDox']
openXdox,"q",openDox as typed ['openXdox', 'q', 'openDox']
openXdox,null,openDox as typed ['openXdox', None, 'openDox']
openXdox,007,openDox as typed ['openXdox', 7, 'openDox']
openXdox,Atlas,openDox as typed as typed, a project-leg name recorded as a product link

On this branch, each of those exits 2 with REFUSED chain-link-malformed, and no remote
directory exists afterwards. The control openXdox,openDox, a three-link
openXdox,openYdox,openDox, opensoft/openXdox,openDox, OPENXDOX,openDox and
openXdox,OPENDOX still scaffold. The written projects' validate-manifest.py and
validate-repository-naming.py --project exit 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_by in contracts/repository-naming.yaml). The example chains
there are [openXdox, openDox] and [openXwallet, openWallet], and validate-manifest.py
says a recorded chain is "expected a list of neutral-product names". A name in the
neutral-product form is a single plain YAML word, so the manifest stays plain, and it is
still true that no writer quotes a flow item. Quoting would record a #b faithfully: a link
the 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, 007 and true
are 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 --pin product
([A-Za-z0-9_.-]). The last must casefold to the open<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 be
a YAML bool, null or number. Both comparisons ignore case on purpose: a pin names which
repository, not how it was typed (_pinned_basenames). Refusing OPENXDOX at an end would add a new
spelling 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.py passes no chain to naming_block
(:2887-2890). scripts/family.py and update-shape.py take none. setup-project.py passes
everything after -- to the scaffold, so it gets this check. validate-repository-naming.py --referent-chain takes a chain but writes nothing, so it is unchanged.

Evidence

  • Fails on 150c41d. The new file, run in a throwaway worktree at 150c41d: 25 failed,
    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's
    tree. That gh was intercepted and never ran. The 2 end-to-end issue rows exit 0 with three
    bare remotes. The 5 that pass are the positive and unchanged-ends tests.
  • Mutation probes (each hunk reverted or bent in a throwaway worktree, new file re-run):
    • call removed: 25 failed;
    • slice widened to every link: 2 failed (the capitalised first and last links);
    • slice narrowed to [1:-2]: 25 failed;
    • any classified name accepted: 8 failed (null, Atlas, opensoft, OPENYDOX);
    • basename not taken: 1 failed (the qualified middle link);
    • check moved after _declaration: 23 failed (every in-process refusal hits gh api);
    • plural dropped: 1 failed;
    • help text reverted without regenerating: test_cli_reference.py 3 failed;
    • _declaration reading the argument inline again: 0 failed, as expected, because that
      change preserves behaviour.
  • Reach. All 14 tracked *.yml/*.yaml files load the same on 150c41d and on this head
    (7 ok, and 7 templates refused on their unrendered {{...}}). repo_shape.py is not
    touched. naming_block called from the scaffold is the only writer of referent_chain.
  • Fix round 1 (5cb1d3e, on top of 114bec9), run at 5cb1d3e with the CI-pinned
    git-filter-repo 2.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.
    • The new file alone holds 44 tests (30 before). scripts/render-cli-reference.py --check
      prints [ok], git diff --check is clean, and no added line holds a host or scratch path.
    • Six more mutants, each applied to scaffold-project.py alone with the whole new file run
      (no -x), all caught: the policy pattern dropped from the remediation (1 failed), the
      --explain sentence dropped (1), links not stripped (3), the check reading its own
      unstripped 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).
    • The streams under "Targeted runs at 114bec9" below were NOT re-run at 5cb1d3e, except the
      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.
  • Targeted runs at 114bec9: all green.

Merges

Re-checked at 5cb1d3e after fix round 1: git merge-tree --write-tree against origin/main
4b2add8, fix/yaml-block-key-nul 9be6287 and fix/yaml-quote-boundaries 21fc213 is clean for
each. fix/yaml-flow-mapping-keys (#252) has no branch on origin or in the shared repository
now; 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.py on the throwaway merge of 5cb1d3e with 4b2add8: 107 passed.

git merge-tree --write-tree of 114bec9 against each sibling branch and against main:
all clean, checked right before the first push.

Left out

  • Follow-up 1: --pin accepts a product name the manifest reader resolves as something other
    than a string.
    --pin null@<sha>, 007 and true each scaffold with exit 0, and the
    project's own validate-pins.py then exits 1 with neutral-pin-entry-malformed: … entry None
    (or 7, True). It reaches the chain's FIRST link too: --pin null@<sha> --referent-chain null,openDox scaffolds three repositories, records referent_chain: [null, openDox], reads
    back [None, 'openDox'], and validate-pins.py exits 1. That writer belongs to --pin, not to
    the chain, and the eventual fix should cover the first link. It is a candidate follow-up issue.
  • Follow-up 2: the validators still accept such a chain. validate-manifest.py and
    validate-repository-naming.py do not flag a recorded middle link that is not a
    neutral-product name, such as [openXdox, Atlas, openDox] written by an older scaffold or by
    hand (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_links has always done. The manifest records basenames.
  • The full suite and the macOS and Windows legs were not run locally. The new tests send git
    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:

  • Reject malformed middle links in --referent-chain before any repositories, work directories, or external reads are created, including during dry runs.
  • Prevent invalid or YAML-ambiguous chain values from producing manifests that the project's validators cannot parse or accept.

Enhancements:

  • Centralize referent-chain parsing and validate middle links against the neutral-product naming policy while preserving existing first- and last-link behavior.
  • Make refusal messages identify all malformed links safely and provide the required spelling and validation command.

Documentation:

  • Update the CLI reference to document the neutral-product naming requirement for intermediate referent-chain links.

Tests:

  • Add comprehensive coverage for malformed, qualified, padded, escaped, dry-run, end-to-end, and valid referent-chain scenarios.

brettheap and others added 2 commits October 11, 2026 22:03
…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>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-10-11T23:05Z) @codex review

@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

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 @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T23:14:08.128025Z 5cb1d3e Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

The 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 validation

sequenceDiagram
    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
Loading

Flow diagram for referent-chain link validation

flowchart 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]
Loading

File-Level Changes

Change Details Files
Validate referent-chain middle links against the neutral-product naming policy before any scaffold work begins.
  • Added a shared parser for stripped, non-empty chain links.
  • Rejects every malformed middle link with a quoted, actionable refusal; preserves existing first/last-link behavior.
  • Runs validation before declaration, tree reads, dry-run planning, work-directory creation, or remote creation.
  • Uses the link basename for policy classification and the policy pattern in remediation text.
  • Updated CLI help and generated documentation to describe the middle-link requirement.
scaffold-project.py
docs/cli.md
Added comprehensive regression coverage for malformed-chain refusal timing, diagnostics, accepted inputs, and unchanged behavior.
  • Covers YAML-sensitive values, qualified links, multiple malformed links, whitespace normalization, and control characters.
  • Verifies real and dry-run refusals create no repositories or work directory and start no subprocesses.
  • Confirms valid neutral-product chains scaffold and pass shipped validators.
  • Confirms case-insensitive and qualified first/last links remain accepted.
  • Adds a 44-test focused suite for the new behavior.
tests/test_scaffold_referent_chain_links.py

Assessment against linked issues

Issue Objective Addressed Explanation
#251 Refuse any malformed middle link in --referent-chain before repository creation, network or git operations, and do so consistently under --dry-run. ✅
#251 Prevent --referent-chain inputs containing YAML-special or otherwise invalid values from producing an unparsable or semantically different project manifest. ✅
#251 Add regression coverage and update the CLI documentation for malformed links, refusal behavior, no-created-repository guarantees, and valid chains. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sonarqubecloud

Copy link
Copy Markdown

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 5cb1d3e638

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

@brettheap
brettheap merged commit 23e16ae into main Oct 11, 2026
5 checks passed
@brettheap
brettheap deleted the fix/scaffold-referent-chain-links branch October 11, 2026 23:36
brettheap added a commit that referenced this pull request Oct 11, 2026
…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>
brettheap added a commit that referenced this pull request Oct 12, 2026
…, #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>
brettheap added a commit that referenced this pull request Oct 12, 2026
…, #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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant