Skip to content

family.py: add refuses a --member whose owner or project does not begin with a letter or a digit as member-malformed before the member is read, so testorg/_Zed, testorg/.Zed, testorg/-Zed, _org/Zed and .org/Zed no longer exit 0 with a commit that the holder's own validate-family.py fails as member-repository (#366) - #399

Merged
brettheap merged 3 commits into
mainfrom
fix/family-member-re
Oct 12, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)
Claim: #366 (comment)

What

family.py add refuses a --member whose owner or project does not begin with a letter or a digit, as member-malformed, before the member is read. MEMBER_RE took QUALIFIED_RE's shape (the validator's, in validate-family.py and validate-manifest.py); until now it shared the character class but not the first-character rule, so testorg/_Zed, testorg/.Zed, testorg/-Zed, _org/Zed and .org/Zed were mounted, pinned and committed with exit 0, and the holder's own validate-family.py then failed the commit as member-repository.

The rulings (the issue's three decisions):

  1. MEMBER_RE takes QUALIFIED_RE's shape. _checked_member's ./.. clause is KEPT for its more specific sentence and read FIRST, so it is still reachable (only on a name with a /; a bare . is still the general sentence, which asks for the owner). Its remedy no longer offers .github as the example of a name with a dot, since .github is refused now; it says Ze.d.
  2. One rule, pinned, not moved: family.py spells the pattern a third time, and tests/test_family_member_name.py READS every spelling (MEMBER_RE and every tracked module's QUALIFIED_RE outside tests/, parsed, never run) and fails when they differ. The constant stays out of repo_shape.py, a copied file that would reach an existing holder only through update-shape.py.
  3. The validator's rule is unchanged.

The general member-malformed sentence now states the rule, and its remedy says why (a commit that wrote such a name would fail the gate the holder ships).

Behaviour that moves beside the five

Tests

tests/test_family_member_name.py, members from local bare remotes (a bare copy of the family fixture's spare member under each name, so a refusal is for the NAME and never for a missing remote):

  • add --member over the issue's five spellings plus -org/Zed and .github: member-malformed, exit 2, empty stdout, no gh call, no throwaway clone, and the holder byte for byte (HEAD, ls-files -s -z, status --porcelain=v1 -z, family.yaml, the rest of the snapshot).
  • Controls: a name with an inner ., _ or - (and an owner with an inner .) still adds in ONE commit, the row names the argument verbatim, and the holder's validate-family.py passes.
  • The pinned-equality test, and a corpus of names every spelling must judge alike.

Mutant: the old MEMBER_RE restored fails the seven refusal cases (six exit 0 with a committed row, and -org/... is unsafe-value, exit 2) and both pin tests, 9 of 13; the four controls pass.

Linux, /usr/bin/python3 -m pytest tests/test_family_member_name.py tests/test_family.py tests/test_family_add_refusals.py tests/test_family_member_path.py -q: 220 passed. CI's three OSes are the gate.

Closes #366

🤖 Generated with Claude Code

Summary by Sourcery

Align family.py add member-name validation with the holder validator so malformed repository names are refused before any member access or commit.

Bug Fixes:

  • Reject member names whose owner or project begins with a non-alphanumeric character before reading or modifying the member repository, preventing commits that fail the holder’s validator.
  • Preserve support for dots, underscores, and hyphens appearing after the first character of either name.

Enhancements:

  • Keep the member-name pattern synchronized with the validator patterns through source-level equality and corpus tests.
  • Retain specific handling for . and .. path segments while updating their guidance for the tightened naming rule.

Tests:

  • Add coverage for malformed member names, side-effect-free refusal behavior, valid inner punctuation, validator compatibility, and consistency across all pattern spellings.

… begin with a letter or a digit as `member-malformed` before the member is read, so `testorg/_Zed`, `testorg/.Zed`, `testorg/-Zed`, `_org/Zed` and `.org/Zed` no longer exit 0 with a commit that the holder's own `validate-family.py` fails as `member-repository` (#366)

`MEMBER_RE` and the validator's `QUALIFIED_RE` shared one character class;
the validator also requires each segment's first character to be a letter or
a digit. `add` writes `--member` verbatim as the row's `repository:`, so a
name the looser pattern admitted was mounted, pinned and committed with
exit 0, and the gate the holder ships then failed the commit `add` had just
reported. Two tools of this standard disagreed about one rule, and the one
that writes the file was the looser.

`MEMBER_RE` now takes `QUALIFIED_RE`'s shape. The rulings:

1. THE `.`/`..` CLAUSE IS KEPT, AND MOVED FIRST. The pattern would refuse
   `<org>/..` too, for beginning with a dot, but in its general sentence;
   what is wrong with `.` and `..` is that they are no name at all, and
   #299's more specific sentence says that. It is read before the pattern, so
   it is still reachable, and only on a name with a `/` (a bare `.` has no
   owner to blame, and the general sentence asks for one). Its remedy no
   longer offers `.github` as the example of a name with a dot, since
   `.github` is refused now, by the validator too; it says `Ze.d`.
2. ONE RULE, PINNED, NOT MOVED. `repo_shape.py` is a copied file: a constant
   moved there would reach an existing holder only through `update-shape.py`,
   and both validators spell `QUALIFIED_RE` themselves and ship into projects
   that run them from their own copy. So `family.py` spells it a third time,
   and `tests/test_family_member_name.py` reads every spelling (the source,
   never running the validators: it finds `MEMBER_RE` and every tracked
   module's `QUALIFIED_RE` outside `tests/`, so a fourth copy is pinned the
   day it is written) and fails when they are not the same pattern. A corpus
   of names says what that pattern means, so a change that keeps the
   spellings equal and moves the rule has to move the corpus too.
3. THE VALIDATOR'S RULE IS UNCHANGED. Which names GitHub accepts is not
   claimed; `testorg/.github` was refused by the validator already, so
   nothing a holder can keep depended on `add` admitting it.

The general sentence now says the rule (each name begins with a letter or a
digit, then letters, digits, `.`, `_`, `-`) and its remedy says why: a
commit that wrote such a name would fail the gate the holder ships.

BEHAVIOUR THAT MOVES BESIDE THE FIVE. `--member=-org/Zed`, the one way a
leading `-` reaches the owner, passed the old pattern and was refused later
by `checked_value` as `unsafe-value`; the pattern is asked first, so it is
`member-malformed` now (exit 2, nothing read, either way). `testorg/.github`
and `testorg/...` are refused. `tests/test_family_member_path.py`'s
`test_add_still_takes_a_name_that_contains_a_dot` asserted `.github` was
admitted and its remedy string offered `.github`: both are corrected, `a.b`
stays.

TESTS, `tests/test_family_member_name.py`, members from local bare remotes
(a bare copy of the fixture's spare member under each name): `add --member`
over the five spellings of the issue plus `-org/Zed` and `.github`, each
`member-malformed`, exit 2, empty stdout, no `gh` call, no throwaway clone,
and the holder byte for byte (HEAD, `ls-files -s -z`, `status
--porcelain=v1 -z`, `family.yaml`, the rest of the snapshot); controls that a
name with an inner `.`, `_` or `-` (and an owner with an inner `.`) still adds
in ONE commit and the holder's `validate-family.py` passes; the
pinned-equality test and the corpus. MUTANT: the old `MEMBER_RE` restored
makes the seven refusal cases and both pin tests fail (9 of 13; the four
controls pass), the refusals with exit 0 and a committed row.

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

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

@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

family.py add now applies the holder validator’s leading-letter-or-digit repository naming rule before reading or mounting a member, while preserving valid internal punctuation and the specific ./.. diagnostics. New tests verify refusal side effects, successful controls, validator consistency across tracked copies, and regression coverage for the affected name corpus.

Sequence diagram for refusing malformed members before side effects

sequenceDiagram
    participant User
    participant Family as family.py
    participant Member as Member repository
    participant Git as Holder git state
    User->>Family: add --member owner/project
    Family->>Family: _checked_member(raw)
    alt owner/project begins with . _ or -
        Family-->>User: Refusal member-malformed
        Note over Family,Git: No member read, clone, gh call, or commit
    else valid leading characters
        Family->>Member: Read member metadata
        Family->>Git: Mount, pin, and commit row
        Git-->>User: Successful add
    end
Loading

Flow diagram for member name validation before reading

flowchart TD
    A["family.py add receives --member"] --> B["_checked_member parses owner/project"]
    B --> C{"owner or project is . or ..?"}
    C -->|Yes| D["Refuse member-malformed with directory-reference diagnostic"]
    C -->|No| E{"MEMBER_RE matches leading letter or digit on both names?"}
    E -->|No| F["Refuse member-malformed"]
    E -->|Yes| G["checked_value validates the member"]
    G --> H["Read and mount member"]
Loading

File-Level Changes

Change Details Files
Align member-name validation with the holder validators and reject invalid names before any repository access or mutation.
  • Require both owner and project to begin with an ASCII letter or digit while retaining letters, digits, dots, underscores, and hyphens afterward.
  • Keep the dedicated ./.. directory-reference refusal ahead of the general malformed-name check.
  • Expand the malformed-name diagnostic to explain the validator compatibility requirement and remediation.
  • Preserve acceptance and successful single-commit behavior for names containing punctuation after the first character.
scripts/family.py
tests/test_family_member_path.py
Add regression coverage for refusal side effects, accepted controls, and consistency of the naming rule across standard and holder validators.
  • Test seven invalid spellings, including leading punctuation and .github, for exit 2, empty stdout, no GitHub or clone activity, and byte-for-byte holder preservation.
  • Use local bare remotes to ensure failures are caused by name validation rather than unavailable members.
  • Verify dotted, underscored, and hyphenated names with valid initial characters still add, are recorded verbatim, and pass the holder validator.
  • Parse tracked validator sources to pin MEMBER_RE and every QUALIFIED_RE spelling to the same regex, and exercise an accepted/rejected corpus.
tests/test_family_member_name.py

Assessment against linked issues

Issue Objective Addressed Explanation
#366 Make family.py add reject owner or project names beginning with ., _, or - as member-malformed before reading the member, so it cannot commit a row that the holder’s validate-family.py rejects. ✅
#366 Preserve valid names containing ., _, or - after the first character, and retain the more specific refusal for . and .. path segments. ✅
#366 Keep MEMBER_RE aligned with the validator’s QUALIFIED_RE rule and add regression coverage for the refusals, valid controls, and consistency among the pattern definitions. ✅

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

brettheap added a commit that referenced this pull request Oct 12, 2026
…n so CI tests the two stacked (#326)

#399 (`MEMBER_RE`, `_checked_member`, #366) lands before this PR and also edits
`scripts/family.py`. A Windows CI run now takes about 55 minutes, so this branch
is stacked on #399 here: one CI cycle tests main, #399 and #400 together, and
this PR can land right after #399 without a second one. The merge was clean (no
conflict); `add --dry-run` calls `_checked_member` as `add` does, so the new
member-name rule is asked under `--dry-run` through the same function.

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
…add-dry-run (#326)

Main took #396 (`tests/test_agents_md_codes.py`) after #399's last merge of it, so
it is merged here as well: the branch now holds main, #399 and #400 and CI tests
all three. The merge was clean (no conflict); the one file it adds is the new
test that holds AGENTS.md's code-shaped names to the shipped tools.

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
…icate-id` and to #399's `member-malformed` names, so a dry run that skipped either check no longer previews a row the real `add` refuses, a copy of a pinned project under a new name or a name beginning with `.`, `_` or `-` included (#326)

Lane 3's #381 landed as f0a8bf6 before this PR: `add` refuses a member whose
`project.yaml` records an `id` a row already pins, as `member-duplicate-id`,
before it mounts anything. The review of 8bcc57c said whichever of the two
landed second must give that refusal a row in `ADD_REFUSALS`, and that is
this PR. #399 (`MEMBER_RE`, #366) also lands first and is merged here, so CI
tests main, #399 and #400 together; both merges were clean.

No behaviour changes. `cmd_add` asks `_checked_member_id` of the probe's
reading just before the dry-run return, so the dry run reaches the check
through the same function as the real run, with the same id, and
`member-duplicate-id` comes out of `--dry-run` with the same stdout, the same
stderr and exit 2. Only the `_add_dry_run` docstring, which listed the
holder's refusals and the member's, now names it.

- `ADD_REFUSALS` gains `duplicate-id`, last, in `add`'s own order: the member
  is `IRRS2`, a bare copy of the pinned `IRRS` made beside the holder from the
  remote its `.gitmodules` names and never in the module's shared remotes,
  as f0a8bf6's own tests build the renamed repository. A set-up may now return
  the directory of remotes the add reads from; the others return None.
- It gains five `malformed-*` rows for #399's rule, after `malformed-dot`:
  a project beginning with `_`, `-` and `.`, and an owner beginning with `_`
  and `.`. `<org>/..` and a bare name, the table's old two, are refused by
  clauses that predate the rule, so none of them covered it. An owner
  beginning with `-` cannot be said as `--member <owner>/<project>` (argparse
  reads it as an option): it stays with the real run's test in
  `test_family_member_name.py`.

Mutants, each run on the parametrized refusal test over the `malformed` and
`duplicate-id` rows:
- the dry run skips `_checked_member_id`: `[duplicate-id]` fails (the dry run
  exits 0 and prints the row for `IRRS2`; the real run exits 2), the other
  seven pass;
- the dry run takes `--member` without `_checked_member`: the seven
  `malformed*` rows fail, `[duplicate-id]` passes;
- `MEMBER_RE` loosened to the pre-#399 class: exactly the five new rows fail.

Targeted set (Linux, git 2.43.0, CI's git-filter-repo pin):
`test_family_dry_run.py`, `test_family_add_refusals.py`,
`test_family_add_member_id.py`, `test_family_member_name.py`,
`test_family_member_path.py`, `test_family.py`, `test_cli_reference.py` and
`test_agents_md_codes.py`: 352 passed.

Not changed: a linked `project.yaml` whose target records an id a row pins
still gets exit 0 from the dry run, and the real run refuses it after the
mount and unwinds, inside the module docstring's stated limit.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap
brettheap merged commit f7f4b14 into main Oct 12, 2026
5 checks passed
@brettheap
brettheap deleted the fix/family-member-re branch October 12, 2026 18:23
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…-bump-remove-rollback, so this PR is tested on top of #400, which lands first and conflicts with it (#365, #360)

#400 (`add --dry-run`, #326) carries #399 and main 3ef059c, and lands
before this PR. Both rewrote the end of the module docstring's "WHAT ONLY
THE WRITE ITSELF CAN ANSWER IS NOT ASKED" paragraph, and both changed
`tests/test_family_dry_run.py`'s import list from
`test_family_add_refusals`. Resolved as review 365-5d56e75 gives it:

- `scripts/family.py`: #400's `.gitmodules` sentence, then this PR's
  held-`index.lock` sentence, then #400's symbolic-link `project.yaml`
  sentence, and after a blank line this PR's "A REFUSED `bump` OR
  `remove` PUTS THE HOLDER BACK" paragraph, unchanged;
- `tests/test_family_dry_run.py`: the union of the two import lists.

main is not merged separately: #400's head carries it.

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 12, 2026
…mounts, reads the member from throwaway clones outside the holder for the row's id and digest, and prints the row, gitlink, `.gitmodules` section and trailers without mounting, writing or committing anything, and `add` takes `--trailer` as `bump` does, so every subcommand has the dry run the module's `EXIT CODES` line promises (#326) (#400)

The second and last part of #326. Part 1 (#345, 3ae0109) gave `bump` and
`remove` a `--dry-run`, and left `add` out until the changes to `cmd_add`
then in flight had landed. Until now the only way to see what `add` would
pin was to run it: clone the member, mount it, rewrite `family.yaml` and
commit.

`add --dry-run` leaves `cmd_add` after the probe and before
`MountRollback` is taken or `git submodule add` runs. So every refusal
`add` makes before its first write is the real run's own code, in its
order, with the same code, the same words and exit 2. That covers the
holder's refusals and the member's, which are asked of the probe's
throwaway clone. Lane 3's #381 (f0a8bf6) added `member-duplicate-id` right
after the probe, and `cmd_add` asks it before the dry-run return, through the
same function, so `--dry-run` refuses it too: real and dry run give
byte-identical output, exit 2, and a table row pins it. The same holds for
#399's `member-malformed` names beginning with `.`, `_` or `-`.

The dry run reads the member a second time, as the real run does. The
real run's second reading is the mount, whose reading the row records.
The dry run's is a second bare clone, made as the probe makes its own: in
a temporary directory outside the holder, deleted whatever the answer. It
is read at the commit the probe resolved. The `id:` comes from
`_member_id_at`, through the function the checkout is read with. The
digest comes from `tree_digest`, which reads objects and never a
checkout, so it is the digest the real run computes, never a guess. A
`project.yaml` that is a symbolic link is left to the real run's mount,
which reads through it; the dry run says so and guesses no id.

It prints, in `bump --dry-run`'s layout, and exits 0:
- `commit none -> <sha>` and the digest;
- the gitlink;
- the `.gitmodules` section `git submodule add` writes: name, path, url,
  and whether the file is new;
- the registration in `.git/config`, written or with its url rewritten;
- the directory and object store the mount makes;
- the row as `family.yaml` would carry it;
- the trailers.
Without `--at`, a note says the real run pins the tip it finds when it
mounts, and gives the `--at` that pins the commit shown. The dry run never
runs `git submodule add`. It never writes `.gitmodules`, `family.yaml`,
the index, the work tree, `.git/config` or `.git/modules`.

`add` takes `--trailer`, `bump`'s repeatable flag. Ruling 4 asks
`--trailer` with `--dry-run` to print the trailers it would write, and
`add` had none. The real run carries the lines into its one commit
through `_commit_one`. Without the flag, the commit is the one `add` has
always written.

Git's own refusal at the mount is named, not made. With no `.gitmodules`
in the work tree and one in the index or HEAD, `git submodule add` refuses
before it writes anything, and the real run exits 2 there with the holder
unchanged. The dry run says so in a note, with how to put the file back,
and exits 0. That is how part 1 treats the checkout's own refusal.

The module docstring's `EXIT CODES` line now says every subcommand takes
`--dry-run`. `add`'s help names the dry run and says it reads the member.
`docs/cli.md` is regenerated. AGENTS.md is not changed (line cap); its
family section naming the dry runs and `add --trailer` is a follow-up.

The tests are appended to tests/test_family_dry_run.py; part 1's tests
are untouched.
- A real add previewed, for a holder with members and for a family's
  first member (absent `.gitmodules` stays absent). These stay
  byte-identical: HEAD, the index, `git status`, `family.yaml`,
  `.gitmodules`, `.git/config`, the `.git/modules` listing, the work tree
  and the holder's objects. No `gh` call and no temporary clone left. The
  real add then writes exactly the printed row, digest, gitlink, section,
  registration and directory, and the holder validates.
- 19 refusals before the first write, each the same stdout, stderr and
  exit 2 from the real run and the dry run, with the holder unchanged:
  the holder's, the member's, `member-duplicate-id` (a copy of a pinned
  project under a new name) and five `member-malformed` names beginning
  with `.`, `_` or `-`.
- `--trailer` printed, and carried by the real commit.
- `--at` previewed and pinned.
- A registration the mount rewrites.
- The `.gitmodules` note, for an unstaged and a staged deletion.
- A linked manifest (POSIX only).
- In process: both clones are outside the holder, under the run's TEMP.
  The holder's `git status`, asked while the dry run's clone exists, is
  unchanged. No git command in the holder writes, and no `git submodule`
  runs.
- A member whose `project.yaml` records `id: routing-core` under the name
  `IRDI`: the dry run prints that id, read from the member and not made
  from its name, and the real add writes exactly the printed row.
- The dry run's own clone failing (the member's remote moved away between
  the two readings): exit 2, its clone removed, the holder unchanged. And
  a holder that is a linked worktree: the object store printed is the one
  the real add makes.

Mutants, each killed:
- the dry-run return moved after `git submodule add`: `index changed`;
- the temporary clone made inside the holder: the status asked while the
  clone exists;
- the temporary directory not removed: the leftover check;
- an id made from the name (`project.lower()`): the id test;
- the dry run's own clone leaked on its failure path: the failing-clone
  test;
- the store assumed to be `.git/modules/<path>`: the linked-worktree test.
- the dry run skipping `_checked_member_id`: the `duplicate-id` row;
- the dry run skipping `_checked_member`, or `MEMBER_RE` loosened to its
  pre-#399 class: the `malformed*` rows.

Closes #326

Lane: openreposhape-4 (openRepoShape-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 12, 2026
… into fix/adopt-record-submodule-placement

Brings in f7f4b14 (#366), cb7063f (#390), 5678f68 (#357) and 53bee5b
(#289) on top of 3ef059c, which 563ea16 had merged. Merged by sha, no
rebase.

One conflict, kept on both sides:

- tests/test_manifest_validator.py, the import block. cb7063f adds
  `import os`, `import subprocess` and `import sys`; this branch had
  already added `import os` and `import sys`, so only `subprocess` was in
  conflict. The merge keeps all three, once each, as cb7063f's message
  says it would, with this branch's `from conftest import REPO, git,
  run_script` and `from repo_shape import parse_yaml,
  path_spelling_fault` after them.

`git merge-tree --write-tree` of the two parents gives the same tree in
every file but that one.

Checked:

- Every top-level definition of the eleven modules either side changed
  is the changing side's text, byte for byte. The only ones both sides
  changed are that import block and shape-doctor.py's module docstring,
  where the merged docstring differs from main's by exactly this branch's
  two hunks, and from this branch's by exactly 5678f68's one.
- No name is bound twice by the merge. scripts/shape_materialize.py
  imports `YamlError` and `parse_yaml` twice, but main 3ef059c does that
  too.
- Each module's names are the base's plus what each side added.

Lane: openreposhape-1 (openRepoShape-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 12, 2026
…plan-manifest-invalid` joins `_entry_findings` after #255's values, and this branch's `plan-root-symlink` stays after the source's own submodules

origin/main moved 92e29d9 -> dd8d0ea (twelve commits), merged by sha:
- f0a8bf6 (#330 via #381), scripts/family.py;
- 87d2eb7 (#333 via #379), update-shape.py;
- cd1fc79 (#327 via #375), shape-doctor.py;
- 45e01f4 (#348 via #396), tests/test_agents_md_codes.py: every
  code-shaped name AGENTS.md spells is a code a tool prints;
- 3ef059c (#258 and #261 via #314), adopt-project.py, scaffold-project.py
  and scripts/shape_materialize.py: the project's YAML reader and its
  `validate-manifest.py` are asked about the manifest's values before
  anything exists; `check` reports `plan-manifest-invalid` through
  `_manifest_findings` in `_entry_findings`; `_repository_urls` refuses
  `adopt-name-too-long`;
- f7f4b14 (#366 via #399), scripts/family.py;
- cb7063f (#301 and #280 via #390), scripts/repo_shape.py and the
  template's validate-manifest.py: a bracket or brace in a plain flow
  scalar is refused;
- 5678f68 (#278, #297 and #298 via #357), shape-doctor.py;
- 53bee5b (#289 via #352), scripts/bump-leg.py;
- f5cb616 (#369 via #398), scripts/shape_materialize.py: the docstrings
  of `force_stage` and `stage_written`;
- a92e60b (#326 via #400), scripts/family.py;
- dd8d0ea (#292 via #353), adopt-project.py: `_elector` keeps a
  `user.name` byte that is not UTF-8, `_remote_repository` reads an
  origin URL that is not UTF-8 as none, and `adopt-no-org` is asked
  before `checked_value`.

scripts/shape_materialize.py, scripts/repo_shape.py and every other file
merge with no conflict. adopt-project.py conflicts in two hunks of
`_entry_findings`, each resolved by keeping both sides:
1. The docstring. Main's order, with #261's manifest findings after
   #255's values and before #190's relative urls; this branch's symlinks
   stay after the source's own submodules.
2. The return. Main's `_manifest_findings(plan)` call, and the findings
   in main's order: each entry's path (#196), coverage, the legs and what
   the root keeps where the shape writes, the source's own submodules
   (#166), this branch's `_root_symlink_findings`, #255's values, #261's
   manifest findings, and #190's relative urls last. The remediation and
   the NOTEs are main's.

How each of main's commits that reach this branch's code meets it:
- 3ef059c: `_manifest_findings` asks the validator the template ships
  about the values `execute` writes; it reads no symlink and no path
  the shape writes. Its materializer half adds the validator's helpers,
  `refuse_unstorable_names` and `MANIFEST_VALIDATOR`, and holds
  `election_date` to the validator's `DATE_RE`; it leaves `place`,
  `_materialize`, `copy_tree`, `template_files` and `_refuse_unwritable`
  as they were.
  In `execute`, its `adopt-name-too-long` (in `_repository_urls`) comes
  before #255's clone-URL refusal, `_refuse_what_check_finds` and this
  branch's `core.symlinks` probe, all before any leg. Its new paragraph
  of the module docstring follows the one 6b53a6d corrected; both merge
  with no conflict.
- f5cb616: docstrings only.
- dd8d0ea: `_remote_repository`, `_checked_plan_inputs`' organisation and
  `_elector`, none of them this branch's.
- cb7063f: the plans `check` and `execute` read go through this reader.
  The plans `plan` writes for this branch's tests hold their entries as
  block mappings, each path `double_quoted` where it needs to be, and
  the rehearsal below read every one of them.

Checked on the merged tree:
- AST (fin183g/names.py, base 92e29d9): no top-level name is bound twice
  in adopt-project.py, scaffold-project.py or scripts/repo_shape.py, and
  none is loaded but bound nowhere. scripts/shape_materialize.py binds
  `YamlError` and `parse_yaml` twice, as main dd8d0ea does (1f90a0f's
  import and 3ef059c's, the same objects); the merge adds none.
- Both sides intact (fin183g/sides_intact.py): the only lines that differ
  from either side's own change are those of the two hunks.
- A rehearsal of this merge (tree d21f8b4, the same as this one) passed
  tests/test_adopt_root_symlinks.py with main's
  test_adopt_{manifest_values,git_reads,relative_submodule_url}.py,
  test_scaffold_manifest_values.py, test_agents_md_codes.py and
  test_yaml_flow_brackets.py: `440 passed`.

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