Skip to content

validate-manifest.py, the naming gate and setup-project.py: a chain link no tree answers for is offered what each caller reads, so the root's validator and setup's step (4) no longer offer --link-source, which neither takes, and the gate spells every --link-source value and path it prints - #744

Merged
brettheap merged 3 commits into
mainfrom
fix/661-chain-remedy-callers
Oct 16, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: #661 (comment)

Lands after #634 (on main as 30676c9). This branch is cut from it. It must land before the v1.1.0 tag: DB8 option 3 and DB9 option b of the checklist triage of 2026-10-16, which Brett Heap approved at 12:14:51Z, and D6, one re-pin for v1.1.0.

A chain link whose own tree nothing could read is declared-unverified, and its WARNING offers what would let that tree be read. Since #634 the scaffold offers its own sources (link_pins_from_trees(..., remedies=)). The two other callers still printed the naming gate's words, "a sibling checkout, SHAPE_PIN_SOURCE_, or --link-source =", and neither of them takes --link-source:

  • L2-68. The root's own validate-manifest.py, on every make validate, bootstrap and CI validate run of a root that records a chain. Typed as printed, the offer gives unrecognized arguments.
  • L2-75. Setup's step (4), through the gate. It also labelled the --pin-source it hands the gate (--link-source). Typed after --, the offer ends step (5) in the scaffold's unrecognized arguments, exit 2.

Each caller now offers the sources it reads. The gate also names every --link-source value and path it prints as _shown names one (L2-69).

What changed

templates/assembly-root/scripts/validate-manifest.py (L2-68; a shape copy).

scripts/validate-repository-naming.py (L2-75, L2-69; a shape copy).

  • --link-source-label=FLAG is the caller's own name for the bare --link-source it passes. Setup passes --pin-source. The value starts with -, so it is given after an =. With a label:
    • link-source-unreadable says (FLAG), not (--link-source). The gate replaces the library's label, so an older library needs nothing.
    • The "not used" NOTE, and its "the later … replaced it", name the value FLAG <value>.
    • Neither line says a value holding = "was read as a PATH". The caller's flag is always a path, and setup spells a <word>= value ./<word>=.
    • The NOTE's list of more specific sources leaves out "a keyed --link-source".
    • The help says exactly that: "the link-source-unreadable WARNING and the not-used NOTE name that value by FLAG, and neither says a value holding = was read as a PATH". Two lines keep the gate's own flag, and the module docstring names them: the link-source-no-path REFUSED line, and the NOTE's advice name the link it is, as --link-source <link>=PATH where the chains begin at more than one link. Setup reaches neither (see Not changed).
  • --link-remedy LINK=TEXT (repeatable) gives the caller's words for what would let LINK's tree be read.
    • The value is split at its first =. A value with no =, no link or no words is refused by argparse, exit 2.
    • The words are kept by pin_product_key(link); the last value for a link wins.
    • They are passed as remedies= only when some were given and the library's signature takes them, so a project's own run calls the library exactly as before, beside any library.
    • _chain_link_findings is unchanged. It already looks a holder's entry up by pin_product_key, and a holder with no entry keeps the gate's own words.
  • L2-69. These are now spelled by the gate's own _shown (Pinned shape copies: AGENTS-shape.md's CONTRACTS_DIR sentence is false for every scaffolded root, and the chain WARNING offers remedies the scaffold lacks (one shape bump before the v1.1.0 tag) #610): read_as_path's key, _also_a_directory's value (in the sentence and in the ./ command), the path of a SHAPE_PIN_SOURCE_<LINK> or a checkout beside the project that could not answer, and the path inside that checkout's (sibling checkout (…)), no_path_refusal's value, and the "not used" NOTE's value and its replacement. A printable value is named byte for byte as before.
  • The module docstring says which prints are _shown. Every --link-source value and path the gate prints, and the path of a SHAPE_PIN_SOURCE_<PRODUCT> or of a checkout beside the project it names, is _shown. A caller's FLAG and the words it offers for a link, a chain link's name, and the --project and --policy paths are printed as given (see Not changed).

setup-project.py (L2-75).

scripts/shape_materialize.py, scaffold-project.py.

docs/cli.md is regenerated: the gate's two new options. validate-manifest.py's and setup's --help are unchanged.

What step (4) says now, measured through setup-project.py:

run, after -- before after
--pin-source '', 2 links (a sibling checkout, SHAPE_PIN_SOURCE_OPENXDOX, or --link-source openXdox=<path> would let it be read) (SHAPE_PIN_SOURCE_OPENXDOX, or a --pin-source clone of openXdox that holds its project.yaml, would let it be read)
--pin-source ' ' WARNING link-source-unreadable: (--link-source) could not answer for openXdox: … … (--pin-source) could not answer…
--pin-source openXdox=layer, absent (--link-source), then "It was read as a PATH: ./openXdox is not a link…" openXdox=layer (--pin-source) could not answer…, and no "read as a PATH"
3 links, the first tree given … or --link-source openYdox=<path> would let it be read (SHAPE_PIN_SOURCE_OPENYDOX would let it be read), the plan's own line byte for byte
3 links, --pin-source '' two offers of --link-source one offer per holder, each in the plan's words
the first link's variable set, and --pin-source <tree> NOTE --link-source <tree>: not used — … (a keyed --link-source, SHAPE_PIN_SOURCE_<PRODUCT>, …) NOTE --pin-source <tree>: not used — … (SHAPE_PIN_SOURCE_<PRODUCT>, or a checkout beside the project).

Decisions the issue left open

Tests

New file tests/test_chain_remedy_callers.py, 30 tests:

test what it pins
test_the_manifest_gate_offers_the_sources_it_reads (two-links, three-links, qualified) the root's own validate-manifest.py: exit 0, each holder's WARNING exactly, in its own words, no --link-source. qualified records [opensoft/openXdox, openDox], which is offered by the name openXdox
test_each_source_the_manifest_gate_offers_reads_the_link (variable, beside-the-root) each offered source, given the link's tree, verifies the chain
test_a_shared_variable_is_not_offered_by_the_manifest_gate (lowercase, mixed-case) _link_remedies for two holders sharing a variable, keyed by pin_product_key
test_the_new_copies_run_beside_an_older_library (pre-634 de926142, v1.0.1) the skew rule: the root's validator, and its gate with --link-remedy, beside each older repo_shape.py (read with git show, asserted to hold no remedies): exit 0, no traceback, the older words
test_the_gate_offers_each_holder_the_callers_words three links, one offer per holder, each its own words (one typed OPENYDOX=)
test_a_holder_given_no_words_keeps_the_gates_own, test_the_last_words_for_a_link_win fall-back and last-wins
test_words_that_are_not_link_equals_text_are_refused (no-equals, no-link, no-words) exit 2, argument --link-remedy: … is not LINK=TEXT
test_a_labelled_bare_source_is_named_by_its_label, test_a_labelled_source_holding_an_equals_is_not_said_to_be_a_path, test_an_unused_labelled_source_is_noted_by_its_label the label in the unreadable line, no "read as a PATH", the NOTE's head and its list
seven L2-69 rows, each with an ESC and a forged line where the system can hold one: test_a_key_read_as_a_path_is_spelled, test_a_keyed_value_that_is_also_a_directory_is_spelled, test_a_variable_the_gate_could_not_read_is_spelled, test_a_checkout_beside_the_project_the_gate_could_not_read_is_spelled, test_a_value_that_names_no_path_is_spelled, test_an_unused_link_source_and_its_replacement_are_spelled; and the control test_a_printable_value_is_named_as_typed every print site; no raw ESC and no forged line on the terminal

Setup's tests (shared helpers in tests/conftest.py, one-call wrappers in both setup files):

file test what it pins
tests/conftest.py assert_step_four_speaks_in_the_plans_words (5 cases: three-links-first-tree, three-links-no-source, blank, holds-an-equals, variable-first) no --link-source in step (4) or on stderr; the plan's words; the --pin-source label; the plan's own WARNING line byte for byte. One assert per condition (S9073)
tests/conftest.py assert_an_empty_pin_source_is_the_plans_to_refuse changed. Its re.sub of the gate's offer is gone: it now asserts no --link-source at all, and the plan's words in step (4)
tests/test_setup_project_py.py, tests/test_setup_sh.py test_step_four_speaks_in_the_plans_words the helper through setup-project.py and through setup.sh
tests/test_setup_project_py.py test_each_holder_is_given_the_plans_words (6) the argv: label and one --link-remedy per holder, the chain read as the scaffold reads it, nothing for one link
tests/test_setup_project_py.py test_step_four_and_the_plan_share_one_set_of_words setup's, the scaffold's and shape_materialize's names are one function
tests/test_naming_policy.py test_the_projects_own_manifest_gate_keeps_its_warning_for_a_tree_it_reached changed. It expects the validator's own words now, and no --link-source
  • Red at the base (main 30676c90 with only the tests): 46 failed and 3 passed. The 3 are the controls: test_each_source_…[variable, beside-the-root] and test_a_printable_value_is_named_as_typed.
  • Mutants: 27 of 27 killed on round 1's head f5e1867d, each by a named test, and the files restored byte for byte afterwards (sha256 checked).
    • validate-manifest.py:
      • V1, no remedies passed;
      • V2, remedies passed unguarded. It dies by the TypeError beside both older libraries.
      • V3, keyed by the raw link;
      • V4, a shared variable offered;
      • V5, shared asked of the raw link;
      • V6, the chain's end given an entry.
    • The gate:
      • G1, --link-remedy not passed on;
      • G2, keyed by the raw link;
      • G3, the first words win;
      • G4, remedies passed unguarded;
      • G5, the label ignored in the unreadable line;
      • G6, "read as a PATH" under a label;
      • G7, the NOTE ignores the label;
      • G8, "a keyed --link-source" kept under a label;
      • G9, malformed --link-remedy values taken.
    • Setup:
      • S1, no words passed;
      • S2, no label;
      • S3, the chain read unstripped;
      • S4, words of its own.
    • L2-69:
      • E1-E7, each print site raw again;
      • E8, _shown always repr.
  • Targeted, on Linux, at round 1's head: 36 files, one process, 2595 passed, 6 skipped and 0 failed in 1973 s. The files are:
    • the new file;
    • test_naming_policy, test_link_source_chain_defaults, test_pinned_copy_text, test_pin_source_env_collisions and test_chain_link_fold;
    • test_manifest_validator and test_doctor_validator_verdicts;
    • test_cli_reference;
    • test_setup_project_py and test_setup_sh;
    • test_scaffold_manifest_values, test_neutral_pin_validator and test_template_text_placeholders;
    • test_repo_hygiene;
    • test_branch_left_bump_leg_setup (bump-leg.py and setup-project.py: the git switch -c line says under it what to do where an earlier act left that branch, so already exists is no dead end (#607's sentence, in the two tools it left) #657's) and test_doctor_next_is_a_command;
    • test_scaffold_leg_path_collisions;
    • the 17 test_update_shape*.py files and test_shape_doctor.
      After that run, an unused import shutil was dropped from the new file (29 passed again).
    • Main moved during the run, to 6e37d0e. git merge-tree of this head with it is clean. The predicted merge was committed in a scratch tree and passed 532 tests in 7 files: the new file, test_naming_policy, test_pinned_copy_text, test_link_source_chain_defaults, test_derived_id_refusals, test_scaffold_manifest_values and test_cli_reference.
  • scripts/render-cli-reference.py --check: ok.
  • pyflakes: the same warnings as main in the changed files, and none in the new test file.

Round 2 (rv661), at 657cb233:

  • M4 is killed. M4 reads the chain in validate-manifest.py's _link_remedies without repo_basename (rv661: 373 tests let it live). Against the two rows f5e1867d shipped it still lives (2 passed, the control). Against the new qualified row it fails on the WARNING list (test_chain_remedy_callers.py:107), and against the whole new file it fails too. V2, G5 and E3 from round 1 are killed again, each on its assertion. Files restored byte for byte (sha256 checked).
  • Targeted, on Linux, one process at a time: 18 files, 1525 passed, 0 failed, 0 skipped.
    • 821 in 372 s: the new file (30), test_naming_policy, test_pinned_copy_text, test_link_source_chain_defaults, test_manifest_validator, test_cli_reference, test_setup_project_py and test_setup_sh.
    • 704 in 412 s: test_repo_hygiene, test_template_text_placeholders, test_scaffold_manifest_values, test_neutral_pin_validator, test_doctor_validator_verdicts, test_pin_source_env_collisions, test_chain_link_fold, test_scaffold_leg_path_collisions, test_doctor_next_is_a_command and test_branch_left_bump_leg_setup.
  • scripts/render-cli-reference.py --check: ok at this head, and ok on the predicted merge with main 3726ada2 (git merge-tree --write-tree, clean).
  • pyflakes: no warning that f5e1867d did not have (main's two in shape_materialize.py).

The re-pin

Because of this PR, assembly roots re-pin scripts/validate-manifest.py and scripts/validate-repository-naming.py; family holders re-pin nothing new; against v1.0.1 both already differ at main.

  • Both are in an assembly root's copy list (COPIED_VERBATIM holds MANIFEST_VALIDATOR, COPIED_FROM_SHAPE holds VALIDATE_NAMING). A family holder's lists (FAMILY_COPIED_VERBATIM, FAMILY_COPIED_FROM_SHAPE) hold neither.
  • git diff --quiet v1.0.1 origin/main -- templates/assembly-root/scripts/validate-manifest.py and -- scripts/validate-repository-naming.py both exit 1 (re-measured at 55c6ba7d). So this adds no file to a project's v1.1.0 update.
  • scripts/repo_shape.py is untouched.

Not changed (known limits)

Closes #661

🤖 Generated with Claude Code

Summary by Sourcery

Align chain-link remediation messages across manifest validation, naming checks, and setup planning with the sources each caller actually reads.

New Features:

  • Allow the naming validator to accept caller-specific link-source labels and per-link remediation text.
  • Make setup planning and manifest validation report only the link sources available to the caller.],
  • bug_fixes:[
  • ],
  • enhancements:[
  • ],
  • build as? cannot
  • ci as? cannot
  • deployment as? cannot
  • docs as? cannot
  • tests as? cannot
  • chores as? cannot
  • overview as? cannot raise JSON invalid.
  • overview unclear but required one sentence imperative.
  • docs as? cannot
  • tests as? cannot
  • build as? cannot
  • ci as? cannot
  • deployment as? cannot
  • chores as? cannot
  • enhancements as? cannot
  • bug_fixes as? cannot
  • new_features as? cannot
  • overview as? cannot

…ink no tree answers for is offered what each caller reads, so the root's validator and setup's step (4) no longer offer `--link-source`, which neither takes, and the gate spells every `--link-source` value and path it prints (#661)

A chain link whose own tree nothing could read is declared-unverified,
and its WARNING offers what would let that tree be read. Since #634 the
scaffold offers its own sources (`link_pins_from_trees(..., remedies=)`).
The root's validate-manifest.py and setup's step (4) still printed the
naming gate's words, "a sibling checkout, SHAPE_PIN_SOURCE_<LINK>, or
--link-source <link>=<path>", and neither takes `--link-source`.

L2-68: validate-manifest.py offers what it reads, SHAPE_PIN_SOURCE_<LINK>
(unless two links share it, #275) and a checkout beside the root, keyed by
`pin_product_key`. It passes them as `remedies=` only where the root's
repo_shape.py signature takes the keyword (`inspect.signature`), so beside
an older library (pre-#634, v1.0.1) it runs and keeps that library's
words; an unguarded `remedies=` there was a TypeError.

L2-75: the naming gate takes `--link-source-label=FLAG`, the caller's name
for the bare source it passes, and `--link-remedy LINK=TEXT`, the caller's
words for a holder, passed on as `remedies=` under the same guard. With a
label the unreadable line says `(FLAG)`, the not-used NOTE names it, no
line says the value was read as a PATH, and the NOTE lists no keyed
`--link-source`. Setup's step (4) passes `--link-source-label=--pin-source`
and one `--link-remedy` per holder of a chain of two links or more, in the
scaffold's own words: its `_link_remedies` moves, unchanged, to
shape_materialize.scaffold_link_remedies, which both load. The test
helper's `re.sub` of the gate's offer is gone.

L2-69: the gate spells with its own `_shown` the key `read_as_path`
names, `_also_a_directory`'s value, the path of a variable or a checkout
beside the project that could not answer (and that checkout's `how`),
`no_path_refusal`'s value, and the not-used NOTE's value and the value
that replaced it.

docs/cli.md is regenerated for the gate's two options. Both shape copies
already differ from v1.0.1 on main, so no file is added to a project's
v1.1.0 update; scripts/repo_shape.py is untouched.

Lands after #634.

Closes #661

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 16, 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 4 hours and 8 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR aligns chain-link warnings across the root validator, naming gate, scaffold, and setup by supplying caller-specific remedies and labels, safely rendering every link-source value/path, and conditionally supporting older repo-shape libraries without changing their call contract.

Sequence diagram for caller-specific chain-link remedies

sequenceDiagram
    participant Setup as setup-project.py
    participant Gate as validate-repository-naming.py
    participant Library as repo_shape.py
    participant Scaffold as shape_materialize.py

    Setup->>Scaffold: scaffold_link_remedies(chain)
    Scaffold-->>Setup: remedies keyed by pin_product_key
    Setup->>Gate: naming_declarations(..., --link-source-label=--pin-source, --link-remedy=LINK=TEXT)
    Gate->>Gate: _link_remedy(value)
    Gate->>Gate: _library_takes_remedies()
    alt library accepts remedies
        Gate->>Library: link_pins_from_trees(..., remedies=remedies)
    else older library
        Gate->>Library: link_pins_from_trees(...)
    end
    Library-->>Gate: chain findings and warnings
    Gate-->>Setup: caller-labelled warnings using supplied remedies
Loading

File-Level Changes

Change Details Files
Make each chain-link caller describe only sources it actually reads, while preserving compatibility with older repo-shape libraries.
  • Added validator-local remedies for environment and sibling-checkout sources, keyed by normalized product identity.
  • Added signature-based detection so remedies= is passed only to libraries that support it.
  • Moved shared scaffold remedy generation into shape_materialize.py and reused it from the scaffold and setup.
templates/assembly-root/scripts/validate-manifest.py
scripts/shape_materialize.py
scaffold-project.py
setup-project.py
Allow the naming gate to accept caller-specific source labels and per-link remedy text, and make all emitted source values and paths safely printable.
  • Added repeatable --link-remedy LINK=TEXT with validation, normalized keys, fallback behavior, and last-value-wins semantics.
  • Added --link-source-label so setup warnings identify --pin-source rather than the gate's internal flag.
  • Removed misleading PATH diagnostics and keyed-source suggestions when a caller supplies its own label.
  • Applied _shown consistently to link-source values and paths across warnings, notes, refusals, and replacement messages.
scripts/validate-repository-naming.py
docs/cli.md
Pass setup's scaffold wording and source label into the naming check so plan step (4) matches what step (5) can perform.
  • Derive the chain using the scaffold's trimming and ordering rules.
  • Pass one remedy per chain-link holder for chains with at least two links, even when no source was supplied.
  • Preserve existing output for callers that do not provide remedies.
setup-project.py
tests/conftest.py
Add regression coverage for caller-specific remedies, library skew, setup output, and safe value/path rendering.
  • Added end-to-end tests for root validation, naming-gate overrides, malformed remedy arguments, source labels, shared variables, and older pinned libraries.
  • Covered every link-source print site with printable and control-character inputs.
  • Updated setup and naming-policy expectations and verified CLI-reference regeneration.
tests/test_chain_remedy_callers.py
tests/test_naming_policy.py
tests/test_setup_project_py.py
tests/test_setup_sh.py
tests/conftest.py

Assessment against linked issues

Issue Objective Addressed Explanation
#661 Make the root's validate-manifest.py offer only link sources it actually reads, while remaining compatible with older repo_shape.py copies. ✅
#661 Make setup's naming-gate output use setup's --pin-source label and offer remedies that match the scaffold's readable sources, rather than suggesting unsupported --link-source options. ✅
#661 Spell every --link-source value and path printed by the naming gate so non-printable characters cannot reach the terminal raw. ✅

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

…and its label's help say only what is true, the validator's docstring covers a shared variable, and a qualified first link pins the validator's `repo_basename`

rv661 on #744 at f5e1867, CHANGES REQUIRED:

- S9073, tests/conftest.py: `assert offered and offered[0].endswith(...)`
  in `assert_step_four_speaks_in_the_plans_words` is two asserts now, one
  per condition. No other added assert joins conditions.
- validate-repository-naming.py's docstring said every value and path it
  prints is `_shown`. It is now about the `--link-source` values and
  paths, and the path of a variable or a checkout beside the project it
  names, and it says a caller's FLAG and words, a chain link's name and
  the `--project` and `--policy` paths are printed as given.
- The `--link-source-label` help said "a line about that value names it by
  FLAG", which the `link-source-no-path` REFUSED line and the NOTE's
  `--link-source <link>=PATH` (chains that begin at two links) do not do.
  It now names the two lines that do: the link-source-unreadable WARNING
  and the not-used NOTE. The docstring names the two that keep the gate's
  own flag. The code is unchanged; setup reaches neither line.
  docs/cli.md is regenerated.
- validate-manifest.py's `_link_remedies` docstring said every holder is
  offered both sources; a holder whose variable another link shares is
  offered the checkout alone (#275), as the code does.
- tests/test_chain_remedy_callers.py: a `qualified` row,
  `referent_chain: [opensoft/openXdox, openDox]`, so the validator
  reading the chain without `repo_basename` (rv661's M4, which 373 tests
  let live) fails.
- shape_materialize.py: the comment above the repo_shape import names
  `scaffold_link_remedies` as the reader of `pin_source_env_name`,
  `pin_source_env_shared` and `repo_basename`.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-16T19:14:28Z)

#744 round 2: rv661's findings, fixed at 657cb233 (one commit on f5e1867d, six files, +36/−15; main not merged).

  1. Should-fix, S9073, tests/conftest.py. assert offered and offered[0].endswith(...) in assert_step_four_speaks_in_the_plans_words is now two asserts, one per condition: assert offered, step_four, then the endswith with offered as its message. I grepped every assert the branch adds for an and or or that joins conditions: this was the only one.
  2. Should-fix, pinned copy, scripts/validate-repository-naming.py docstring. The sentence now covers only what _shown spells: "Every --link-source value and path this prints, and the path of a SHAPE_PIN_SOURCE_<PRODUCT> or of a checkout beside the project it names, is named as it is, or as Python's repr spells it where a character in it is not printable (_shown)." A second sentence names what is printed as given: "A caller's FLAG and the words it offers for a link, a chain link's name, and the --project and --policy paths are printed as given."
  3. Nit, pinned copy, the --link-source-label help. I narrowed it and left the code as it was. The help now says: "the link-source-unreadable WARNING and the not-used NOTE name that value by FLAG, and neither says a value holding = was read as a PATH".
    • The docstring names the two lines that keep the gate's own flag: the link-source-no-path REFUSED line, and the NOTE's --link-source <link>=PATH where the chains begin at more than one link.
    • I did not honour the label in either line. A caller's bare flag cannot name a link, so the second line would need words of its own, and that is not plainly right.
    • Setup reaches neither line. It gives step (4) no --link-source for an empty last --pin-source (naming_declarations' names_a_source), and it passes one --referent-chain.
    • docs/cli.md is regenerated, and render-cli-reference.py --check is ok.
  4. Nit, pinned copy, validate-manifest.py's _link_remedies docstring. It now reads "Every holder is offered both, or the checkout alone where another link shares its variable; the chain's end holds no pin and has no entry." That is the code's shared branch, which gives (a checkout of open-x beside this root would let it be read) on open-x, open_x, openDox.
  5. Nit, test gap, M4. test_the_manifest_gate_offers_the_sources_it_reads has a third row, qualified. It records referent_chain: [opensoft/openXdox, openDox] and expects each holder by its basename.
    • M4 is links = [str(link) for link in chain if str(link).strip()] in _link_remedies. Against the two rows round 1 shipped it still lives (2 passed: that run is the control).
    • Against [qualified] it is killed, on the WARNING list at test_chain_remedy_callers.py:107, and against the whole new file it is killed too.
    • M3 is left as you said: setup passes no keyed --link-source.
  6. Nit, scripts/shape_materialize.py. The comment above the repo_shape import now names its new reader: "scaffold_link_remedies reads a chain with the same names: each link's variable (pin_source_env_name), whether another link shares it (pin_source_env_shared), and the link's name (repo_basename) (the chain WARNING offers --link-source to callers that do not take it (validate-manifest.py, setup's step (4)), and the naming gate prints --link-source values raw #661)."

Nothing else changed. E1-E5 are held for #459.

Verification at 657cb233 (Linux, stub gh, one process at a time):

  • 18 files, 1525 passed, 0 failed, 0 skipped.
    • 821 passed: the new file (30), test_naming_policy, test_pinned_copy_text, test_link_source_chain_defaults, test_manifest_validator, test_cli_reference, test_setup_project_py and test_setup_sh.
    • 704 passed: test_repo_hygiene, test_template_text_placeholders, test_scaffold_manifest_values, test_neutral_pin_validator, test_doctor_validator_verdicts, test_pin_source_env_collisions, test_chain_link_fold, test_scaffold_leg_path_collisions, test_doctor_next_is_a_command and test_branch_left_bump_leg_setup.
  • Mutants: M4 killed, and its control lives as expected. Round 1's V2, G5 and E3 are killed again, each on its assertion. Each file was restored byte for byte (sha256 checked).
  • render-cli-reference.py --check is ok at the head and on the predicted merge with main 3726ada2, whose git merge-tree is clean.
  • pyflakes shows no warning that f5e1867d did not have.

The re-pin is unchanged: assembly roots re-pin scripts/validate-manifest.py and scripts/validate-repository-naming.py, family holders re-pin nothing new, and against v1.0.1 both already differ at main.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit dde7ef8 into main Oct 16, 2026
5 checks passed
@brettheap
brettheap deleted the fix/661-chain-remedy-callers branch October 16, 2026 20:02
brettheap pushed a commit that referenced this pull request Oct 17, 2026
main gained 25 pull requests since de92614: #634, #699, #671, #719,
#701, #717, #700, #702, #722, #720, #725, #721, #728, #705, #727, #718,
#744, #743, #749, #740, #737, #734, #742, #751 and #736. No conflict:
AGENTS.md, adopt-project.py and docs/cli.md merged as text, and
README.md and docs/handbook.html are unchanged by the merge. README.md
is 1473 lines, its cap; AGENTS.md 494, its cap; every handbook marker is
current; `render-cli-reference.py --check` is ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…es every reason THIS checkout cannot answer, and the notes are current to main bf1f099 with the 25 pull requests since de92614 (#563, #683)

- W683 and RV-f's N4 on #742: README's exit-3 list (Keeping a project's
  shape current, :1208-1220) and its handbook twin (`doctor`) named only a
  pin whose commit this checkout does not carry. They now say, as
  shape-doctor.py's EXIT CODES does: a pin's commit it lacks, a partial
  clone missing what is compared (#683), a read that fails
  (`update-upstream-unreadable`), a crashed validator of its own (#278,
  #410), or its own naming policy not loading (#678). +1 line, with
  tests/readme-cap.d/683.txt; README.md is 1474, its cap (1422 + 2 + 5 +
  12 + 22 + 10 + 1). The two `Keeping a project's shape current` markers
  move: `doctor` text and marker, `current` marker only.
- The notes, "What changed since v1.0.1", one entry per pull request from
  `git log --first-parent de92614..bf1f099`, each from its squash commit and
  the issues GitHub says it closes:
  - adopt-project.py: #699, #719, #749, #727, #717, #728 (with #621's
    hint), #743 + #720 + #718, #722, #721;
  - update-shape.py, bump-leg.py and a project's own scripts: #702 (with
    the pinned validators), #634;
  - scaffold, setup, family and the shared readers: #725, #671, #744;
  - shape-doctor.py: #742, #701, #737, #751, #736, #734, #700 + #705,
    #740;
  - install, CI and the docs: the exit-3 sentence above.
- Counts (99 pull requests since bfed143; 109 of 135 milestone issues
  closed at 10:28Z), the "current to main" sha and the open list (26) are
  at main bf1f099, still placeholders for tag time.

Left for round B: lane 5's eight lines (#630 issuecomment-6227129879),
whose pull requests #726, #747, #682, #724, #746, #745 and #738 are open
drafts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
Main's 28 new commits since 8062810 merged with no conflict. Four of them touch a file this branch changes: #744, #728, #671 and #634 edit scaffold-project.py, and #728 and #671 edit scripts/family.py too, each in other functions than the clone URL's remedy, its refusal and the README values. Nothing this branch changes is a pinned copy, so nothing was re-pinned.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 17, 2026
…d A3 (three SHOULDs, four nits), and notes entries for the seven pull requests landed since bf1f099, current to main 2000488 (#563, #683)

Round A4, from lane 2's review (#630 issuecomment-6233033833, READY):
- SHOULD 1: the doctor's exit-3 list names "a validator of its own that
  crashed or could not be run", so the case of one that cannot be given
  its `PYTHONPATH` (shape-doctor.py's EXIT CODES, #278) is listed too:
  README.md:1208-1222, its handbook twin (`doctor`) and the notes' docs
  line. The paragraph re-wraps to one more line:
  tests/readme-cap.d/683.txt is +2, and its text now says which causes
  the EXIT CODES block names and that #742 added the partial clone and
  #701 the naming policy. README.md is 1475 lines, its cap (1422 + 2 + 5
  + 12 + 22 + 10 + 2). Both `Keeping a project's shape current` markers
  move to c728c970: `doctor` text and marker, `current` marker only.
- SHOULD 2: #727's entry names the leg description, whose em dash stopped
  every `execute` against GitHub at the first leg.
- SHOULD 3: #749's entry names `source-ambiguous`, for an `org/repo` that
  is also a directory here.
- Nits: the doctor section's three near-duplicate pairs are merged (#636
  with #737, #632 with #700 and #705, #586 with #736); #718 has its own
  bullet; #634's entry names `git_out`'s spelling for every caller and
  #744's the gate's `--link-source-label` and `--link-remedy`; the header
  comment says 2026-10-17 at main 2000488.

Wave B1, `git log --first-parent bf1f099..2000488`: #706 (#664), #703
(#696), #670 (#646), #682 (#648), #726 (#667), #730 (#666) and #724
(#665), each from its squash commit; #726's and #682's from lane 5's lines
(#630 issuecomment-6227129879), #682's with the unmerged `.gitmodules`
its last round added, and #724's with lane 5's note that v1.0.0's
test-hygiene line on the generator's `.pyc` files closes. The counts (106
pull requests; 116 of 135 milestone issues closed at 11:53Z), "current to
main" and the open list (19) are at main 2000488, still placeholders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 17, 2026
…efusals and `family-operation-in-progress` name a path as `shown_source` names it, so an ESC or a line break in a path no longer reaches the terminal (#692) (#731)

#622's folder refusals printed a path as it was. `family.py init --into <a file named wd-ESC[31mRED> --dry-run` gave `REFUSED family-folder-cannot-be-made: the family folder .../wd-^[[31mRED/Orbit cannot be made: ...` through `cat -v`. A name with a line break and then `WARNING forged-line: pwned` printed that line as a line of its own. R612's `family-operation-in-progress` printed the mark's path across two lines for a holder whose path holds a newline.

Each path is now spelled as `shape_materialize.shown_source` spells a `--pin-source`: as it is, and as Python's `repr` spells it where a character in it is not printable.

- `scripts/shape_materialize.py`: both answers of `folder_cannot_be_made` (a file or a bad stat in the way, and a folder this user cannot write in), the folder in `refuse_unmakeable_folder`, and the folder in `make_folder`.
- `scaffold-project.py`: `scaffold-target-exists`.
- `scripts/family.py`: `family-target-exists`, and the mark's path in `family-operation-in-progress` (`where.as_posix()`).

A printable path is named byte for byte, so no existing row changes and `why.endswith(CANNOT_WRITE_IN)` in `refuse_unmakeable_folder` still holds. No help text changed, so `docs/cli.md` is untouched. No README, handbook or template sentence says how a refusal names a path (`git grep` of `shown_source`, `target-exists` and `folder-cannot-be-made` in AGENTS.md, README.md, `docs/handbook.html` and the templates found none), so no doc changes.

- **`shown_source`, not `shown_path`.** Since #634, `shape_materialize.shown_source` returns `repo_shape.shown_path(source)` (`scripts/shape_materialize.py`: `return shown_path(source)`), so the seven call sites this change adds (four in `shape_materialize.py`, one in `scaffold-project.py` and two in `family.py`) spell a path as `shown_path` does. Renaming them to `shown_path` stays out of this PR: it would only move imports in `scaffold-project.py` and `family.py` and reword docstrings in `family.py`, `shape_materialize.py` and the test file, and the review's N1 left it optional. I did not take it. The predicted merge with main works with the alias: `family.py --help` and `scaffold-project.py --help` exit 0 on it, and `shown_source` and `shown_path` return the same string for an ESC name, a line-break name and a plain path.
- **`shown_source` is imported by name into each file's existing `from shape_materialize import (...)` block.** The first commit put it on a line of its own after the block; lane 4's #660, #688 and #693 add an import at that same place, and `merge-tree` reported add/add conflicts in `scripts/family.py` and `scaffold-project.py`. The second commit moves the name into the block, and those conflicts are gone.
- **"The holder in the other refusals of the same block" is one refusal.** The `# What the holder is in the middle of, and what its sparse checkout leaves out` block holds `family-operation-in-progress`, `family-sparse-excluded` and `member-path-unaddable`. Only the first prints a path of the holder's. `family-sparse-excluded` names `members/<Project>` and `.gitmodules`, and `member-path-unaddable` names `members/<Project>`.
- **No scaffold row for `--local-remote-dir`.** A control character there is refused earlier as `scaffold-unwritable-value`, which already names the path with `repr`. The unit test covers `make_folder`, which only a race reaches by the command line.

`tests/test_dry_run_parity.py`:
- `SCAFFOLD_CASES` gains five rows: ESC and line break in the `--work-dir` name, ESC in a folder name above it, and ESC and line break in a work dir that holds a file named `Atlas` (`scaffold-target-exists`).
- `FAMILY_CASES` gains five rows: ESC and line break in `--work-dir`, ESC in `--into` (`the family folder`), and ESC and line break in a work dir that holds a file named `Orbit` (`family-target-exists`).
- Two rows join `UNWRITABLE_FOLDER_AT` (ESC in the scaffold's and in `init`'s `--work-dir`, a folder this user cannot write in).
- Rows named `ESC-...` and `line-break-...` are skipped on Windows, whose file names cannot hold them (`POSIX_NAMES`, `case_params`). Every expectation spells the path with `repr`, and not with `shown_source`.
- `assert_refused` now fails any stderr line that starts with `WARNING forged-line`.
- Three unit tests: `test_each_folder_question_spells_a_name_that_is_not_printable` (ESC, line break; `folder_cannot_be_made`, `refuse_unmakeable_folder` and `make_folder` by themselves), `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`, and `test_a_printable_name_is_named_byte_for_byte` (a name with a space and an accented letter stays unquoted).

`tests/test_family_refuse_up_front.py`: `test_a_holder_path_with_a_newline_still_shows_its_merge` asserted the raw newline in the refusal (`"Ink\nRouter/.git/worktrees/" in ran.stderr`). It is now `test_a_holder_path_with_a_control_character_still_shows_its_merge`, over a line break, an ESC and a forged line, and asserts the one-line `repr` form (`refusal()` fails unless the stderr is two lines). It is the one existing test this change had to edit.

Results at head `ab55c29f` (Python 3.12.3, Linux):
- `tests/test_dry_run_parity.py tests/test_dry_run_asks_remotes.py tests/test_family_dry_run.py tests/test_family_init_landing.py tests/test_repo_hygiene.py tests/test_pin_source_unreadable.py tests/test_scaffold_leg_path_collisions.py tests/test_scaffold_reuse_seeded_legs.py`: **494 passed**.
- `tests/test_family.py tests/test_family_refuse_up_front.py tests/test_family_init_values.py tests/test_family_add_refusals.py`: **356 passed, 19 skipped** (the skips are `test_family_init_values.py:219` and `:291`, template-quoting premises).
- Not run: the full suite (the coordinator's gate), and macOS and Windows (CI is Linux-only since #597). The new hostile-name rows are POSIX only by design.

On the base code: the review overlaid this head's two test files on main `2fac9790` and ran `tests/test_dry_run_parity.py` and `tests/test_family_refuse_up_front.py` with `-k "dry_run_parity or control_character"`. The result is **20 failed, 87 passed** (76 deselected, 43.5 s). The 20 are exactly the new and edited tests: the 5 scaffold and 5 `init` rows, the 4 unwritable-folder rows, the 3 unit tests (`test_each_folder_question_spells_a_name_that_is_not_printable` in two cases, and `test_a_folder_this_user_cannot_write_in_is_spelled_safe_to_show`), and the 3 cases of the edited `test_a_holder_path_with_a_control_character_still_shows_its_merge`. The one new test that passes on main's code is `test_a_printable_name_is_named_byte_for_byte`, by design: a printable name is named byte for byte on both. An earlier draft of this body said 21 failed, 2 passed. That run was on `8062810d` with a wider selection, and it held a scaffold row for ESC in the local remote dir's name (`ESC-in-the-local-remote-dir-name`) that this head's tests no longer have; its second pass was the existing `test_the_refusal_quotes_gh_s_first_line_and_escapes_what_is_unprintable`, which also passes in the new run, one of the 87.

`family.py init` and `scaffold-project.py --dry-run` against a file named `wd-ESC[31mRED`, a file named `wd-\nWARNING forged-line: pwned`, and a work dir with a file `Atlas` or `Orbit` in it, run with a stub `gh` and no real `gh`:
- On main, through `cat -v`: `... the family folder /.../wd-^[[31mRED/Orbit cannot be made: /.../wd-^[[31mRED is a file, not a folder`, and a stderr line `WARNING forged-line: pwned cannot be made: ...` of its own.
- On this head: `REFUSED family-folder-cannot-be-made: the family folder '.../wd-\x1b[31mRED/Orbit' cannot be made: '.../wd-\x1b[31mRED' is a file, not a folder`, and one line for the forged name: `--work-dir '.../wd-\nWARNING forged-line: pwned' cannot be made: '...' is a file, not a folder`. `scaffold-target-exists` and `family-target-exists` are one line each in the same form.

Each spelled site reverted alone in a throwaway clone at `19d126cd` (the call sites are the same at `ab55c29f`); the new tests that fail for each:
- `folder_cannot_be_made`, the `_not_a_folder` answer: 8 failed (six CLI rows and the two unit tests).
- `folder_cannot_be_made`, the `CANNOT_WRITE_IN` answer: 5 failed (the four unwritable rows and the unit test).
- `refuse_unmakeable_folder`: 12 failed.
- `make_folder`: 2 failed (the unit test only).
- `scaffold-target-exists`: 2 failed (its two rows).
- `family-target-exists`: 2 failed (its two rows).
- `family-operation-in-progress`: 3 failed (line break, ESC, forged line).

`git merge-tree --write-tree` of this head `ab55c29f` against origin/main `20c8873b` and against every ref on origin that could meet it, fetched 2026-10-16: the 41 branches (`fix/*`, `l3-*`, `wip/*`, `docs/*` and `release/v1.0`) and the 55 PR heads from #630 up. That is 96 refs. (The same list was run for #730's head `7f2bb36b`.)
- **origin/main `20c8873b`: clean** (the predicted tree `00b5b24c`, which differs from main in these five files only: 216 lines added, 25 removed).
- **#630's head `6fd33a93`: clean.** It conflicted with main in `docs/handbook.html` when this body was first drafted, and no longer does, with or without this change. This change touches no README or handbook.
- **The refs that edit one of this change's five files**, and so could meet it, all merge clean with this head: `fix/646`, `fix/651`, `fix/654`, `fix/660`, `fix/661`, `fix/684`, `fix/686`, `fix/688`, `fix/689` and `fix/693`, the heads of #670, #725, #728, #743 and #744, and the heads of the landed #634, #638, #639, #640, #643 and #671. #634 has landed (`30676c90`), so the `fix/660` note about a conflict with it is gone.
- **Four refs conflict with origin/main on their own**, and so with this head: `docs/release-notes-v1.0.0` and `release/v1.0`, and the pre-squash heads of the landed #637 and #641. Their conflicts do not come from this change. Every other ref, 92 of the 96 in all, is clean with this head, including the refs that conflict with main in `templates/family-root/scripts/validate-family.py` (#682, #745, #747, `fix/648`, `fix/681`, `fix/704`) or in `shape-doctor.py` (#742, `l3-683`) on their own. So do the old heads of the landed #638 and #639, and lane 3's `l3-677-pins-hand-step-wip` branch, and each of them merges clean with this head too.
- **Tests on the predicted merge**, run by the review on the merge with main `2fac9790`: `test_dry_run_parity`, `test_family_refuse_up_front`, `test_pinned_copy_text`, `test_pin_source_unreadable`, `test_dry_run_asks_remotes`, `test_family_dry_run` and `test_family_init_landing`: 445 passed in 215 s. `test_family`, `test_family_init_values`, `test_family_add_refusals`, `test_derived_id_refusals`, `test_repo_hygiene`, `test_naming_policy`, `test_scaffold_leg_path_collisions` and `test_scaffold_reuse_seeded_legs`: 758 passed, 19 skipped in 243 s. I did not re-run them on `20c8873b`; the merge is clean, and the two commits since `2fac9790` (#700 and #702) change only `shape-doctor.py`, `templates/assembly-root/scripts/validate-pins.py` and four test files, none of them a file of this change.
- Before the second commit, `fix/660`, `fix/688` and `fix/693` each conflicted with this head in the import region of `scripts/family.py` or `scaffold-project.py` (a separate import line each side added at the same place). That is why the import joins the existing block.

#634 is on main, and this change does not edit `shown_source` or its docstring in `scripts/shape_materialize.py`.

The review approved with nits. Its S1, the stale base-run numbers and merge lines, is taken here, in this body: no code changed, and the head is still `ab55c29f`. Its N1, renaming the call sites to `shown_path`, is not taken (see "Decisions and their evidence"). It also re-ran 5 of the 7 mutation probes on `ab55c29f` and got the same failure counts as above (12, 2, 3, 2 and 2), and ran 144 hostile-name dry runs (16 names in 9 scenarios): on main 69 rows put a control character on stderr and 6 print a forged line of their own; on this head all 144 exit 2 with no traceback, no control character and no forged line.

- `scripts/family.py` `init`'s plan prints `folder` and `landing` lines with the path raw (`print(f"landing      {holder}   ...")`, `print(f"folder       {folder}   ...")`). On a dry run or a refused run the plan is on stdout before the refusal, so `WARNING forged-line: pwned/Orbit   (--work-dir: no family folder)` still appears there. It is the plan, not a refusal, and not one of the issue's lines.
- Other refusals that name a holder or folder path raw, none of them in the issue's lines: `family-holder-detached` and `family-on-tracking-branch` (`{family.root}`), `family-index-locked` (`lock.as_posix()` in the detail), `family-landing-in-standard` (`{holder}`, `{checkout}`, `{beside / family / family}`), `family-holder-already-here` (`{into}`), and `family-remote-exists` and `scaffold-remote-exists` (`{target}`, a local remote path).

1. **family.py `init`'s plan prints the holder and folder paths raw.** `scripts/family.py` `landing` and `folder` rows (about :1756-1759) print `holder` and `folder` as they are, so a forged line or an ESC reaches stdout in a `--dry-run` and in a run refused after the plan. Direction: spell them with `shown_source`.
2. **The same raw spelling in the refusals listed above.** `family-holder-detached`, `family-on-tracking-branch`, `family-index-locked`, `family-landing-in-standard`, `family-holder-already-here`, `family-remote-exists` and `scaffold-remote-exists` each print a path the person supplied or git keeps. Direction: the spelling this change gives its sites.

Review: rv692, lane 5's independent review, approved ab55c29 with nits, and its body fix (S1) is published. CI (Brett Heap's ruling 27 and his fast-refresh ruling of 2026-10-15T22:48:35Z): H0 ab55c29 passed tests, tests-git-bash and SonarCloud; the head 13f643d is a clean update-branch merge of main 431c8cf, with tests-git-bash and SonarCloud green; and the local gate passed on its predicted squash tree 4327947 over main 431c8cf: the dry_run_parity, family_refuse_up_front, repo_hygiene, agents_md_codes, cli_reference sets, 330 passed. Landed on Brett Heap's word of 2026-10-17T10:59:39Z, relayed by lane 3 on #473: "merge lane 5's approved v1.1.0 drafts".

Closes #692

Lane: openreposhape-5 (openRepoShape-5)
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