Skip to content

contracts/path-classification.yaml: a source's own single-repository.yaml is asked drop or root, drop by default, under its own rule, so adopt-project.py plan no longer asks a record no leg reads the spec, code or root question (#528) - #566

Merged
brettheap merged 7 commits into
mainfrom
fix/528-adopt-single-repository-question
Oct 15, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

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

Branch fix/528-adopt-single-repository-question, head 19ffe4a, on main 7d9bec9 (merged as 0d8336e).

What changed, and why

plan asked about a source's own single-repository.yaml under ambiguous-top-level-yaml: "a declaration the project has DECIDED (spec), configuration the implementation reads (code), or project-level wiring (root)". The file is none of those. It is this standard's optional record that a project STAYS one repository (contracts/single-repository-record.yaml), so a repository that carries one and is adopted into the Triad holds a record of the decision the adoption reverses. Its one reader, the Triad advisory, is silent in an assembly root whatever the file holds, so neither leg has any use for it.

The record is asked its own question, drop or root, and drop is written in as the default. Brett Heap ruled on 2026-10-12 (18:15:05Z, the A7 line of #457), choosing "Ask; default drop (Recommended)": "The default answer is drop". He did not choose "Ask; no default" ("the person must answer drop or root").

  • contracts/path-classification.yaml: ambiguous-single-repository-record, above ambiguous-top-level-yaml, names the file, asks drop or root, and carries default: drop. The policy header says that a rule that asks may name a default, and only drop.
  • plan writes the entry as leg: drop, with review_required: true, the rule and the question. check passes it. execute drops the file unless a person writes root, and the split commit names it among the paths it dropped. plan prints the default and its question just before the unresolved paths, and the plan's header says execute takes it as written.
  • scripts/path_classify.py: Verdict carries the default beside the question and keeps leg None. The doctor's placement row walks a leg with adopt-project.py's own walk and calls a path misplaced when its leg names another leg (walked_findings), so a leg's copy stays a question there. A default is refused when the policy loads (path-policy-default) on a rule that does not ask, or as anything but drop. root unanswered would keep a path in the assembly root that nobody chose to keep, and a rule that answers spec or code says so in leg:.
  • adopt-project.py: the default is written into an entry after both re-asks. ask_about_submodules and ask_about_refused_paths ask questions of their own, so they carry no default, and ask_about_refused_paths passes over a path that is already answered drop.
  • Nothing refuses root, unlike ambiguous-project-yaml (adopt-project.py check passes a plan that keeps the source's own project.yaml in the root, and execute writes the shape's manifest beside it as shape/project.yaml and says adoption verified, so shape-doctor.py calls the adopted assembly NOT A SHAPE ROOT and its first make bootstrap fails #305): the file blocks nothing the shape writes, and no validator, gate or doctor row reads it. spec and code are not offered, and a hand edit to them is not refused either, because nothing breaks.

kind: decides, not the name. A file of that name declaring another kind, one that is not a mapping, and one that does not parse are not the record. The advisory says so in a NOTE in the same plan run. plan now reads the file at the commit it plans, through shape_advisory.read_record(Tree(path, commit)). Where the file is not the record, it gets the general question and leg: null, as before this rule. An uncommitted record does not count, as the advisory says too. release.yaml, a record spelled .yml and a nested single-repository.yaml keep the rule they had.

Docs. AGENTS.md step 2 gains one sentence: an entry whose leg: is its rule's default also carries a question, and execute takes it as written. The AGENTS.md cap moves from 490 to 492, with its dated reason in tests/test_repo_hygiene.py. No --help text changed, and scripts/render-cli-reference.py --check passes.

Tests

tests/test_adopt_single_repository_record.py (30 tests, run with git-filter-repo 2.47.0 on PATH):

  • the rule, its place above the general question, and adopt-project.py's RECORD_RULE;

  • the verdict: leg None, review_required, default == "drop";

  • plan writes leg: drop with review_required: true, the rule and the question, and prints it apart from the unresolved paths;

  • check passes the default unanswered, and execute gets past every plan refusal with it;

  • execute adopts it with the record in no repository and named in the split commit's dropped list, and root keeps the source's blob at the assembly root;

  • each non-record body is asked the general question; a record that leaves out a field is still the record; an uncommitted record is not;

  • a default other than drop, or on a rule that does not ask, is refused (6 cases); a folded directory keeps its default;

  • neither re-ask carries a default, and in plan a default over a name execute will not extract is not written in;

  • the doctor's own walked_findings reads a leg's copy as a question.

  • On the old head (60a258f's code, merged with main): 23 of the 30 fail, each for the reason its name gives. There is no Verdict.default, Entry.default or RECORD_RULE; a bad default is not refused; plan writes leg: null for the record; a file that is not the record still gets the record's question; and execute refuses plan-unresolved. The other 7 are guards that hold there too: the rule's order, the question's words, both answers passing check, root kept, the doctor's reading, and no default over a name execute will not extract.

  • Mutants: 16, all caught (one to nine tests each). They are: the default filling Verdict.leg (drop as a class, which the doctor then calls misplaced); with_defaults doing nothing; it running before ask_about_refused_paths; either re-ask carrying a default; review_required written as leg is None; the policy naming no default; classify_file dropping it; a folded directory losing it; no validation; no printed default; with_its_default dropping the question; the record check reading nothing, reading the working tree, or taking any file present for the record; and skip ignored.

Targeted runs, 46 files: every tests/test_adopt_*.py, plus test_shape_doctor.py, test_scaffold_leg_path_collisions.py, test_cli_reference.py, test_repo_hygiene.py, test_doctor_next_is_a_command.py, test_stdout_encoding.py, test_shape_advisory.py and test_agents_md_codes.py.

  • On the main merge 0d8336e: 3912 passed, 5 skipped, 0 failed.
  • On the head 19ffe4a: 3930 passed, 5 skipped, 0 failed. That is the merge's 3912 plus the 18 new tests. gh was stubbed and never called.

The full suite and the three-OS CI run were not run here.

README follow-up (#519 has landed; this waits for its own README turn)

README.md:776-779 says that entries the policy cannot call "get leg: null, review_required: true and the QUESTION; execute refuses while any remain, because an unanswered question is never an implicit root". One sentence could follow it, with its mirror in docs/handbook.html: "One question comes answered: a source's own single-repository.yaml record is written leg: drop, its rule's default, still review_required: true with its question beside it, and execute drops it unless a person writes root." The paragraph is still true without the sentence, but it is incomplete.

docs/release-notes/v1.0.0.md lists this as a known limit ("a dedicated rule ... is planned (#528)"). That line belongs to the v1.0.0 notes and is left as written.

Closes #528

🤖 Generated with Claude Code

Summary by Sourcery

Handle a source repository’s own single-repository.yaml as a dedicated, reviewable adoption decision that defaults to dropping the record.

New Features:

  • Add dedicated classification and adoption handling for a source repository’s own single-repository.yaml record, defaulting it to drop while retaining an explicit root option.
  • Read the record’s kind at the planned commit so only genuine single-repository records receive the specialized question.

Bug Fixes:

  • Prevent adopt-project.py plan from incorrectly asking whether the single-repository record is specification, code, or root-level wiring.

Enhancements:

  • Support validated rule-level defaults for questions while preserving review requirements and ensuring follow-up questions do not inherit unrelated defaults.
  • Make plan reporting and execution honor defaulted answers, including dropping the record from split repositories unless the plan is changed.
  • Keep doctor placement analysis treating defaulted classifications as questions rather than finalized leg assignments.

Documentation:

  • Clarify that entries carrying a rule default remain questions and that execution uses the default unless changed.

Tests:

  • Add comprehensive coverage for record detection, default validation, plan/check/execute behavior, alternate answers, non-record files, commit-state reads, re-asked paths, and doctor placement handling.

Chores:

  • Update repository hygiene limits to account for the expanded adoption guidance.

…tory.yaml` is asked drop or root, drop by default, under a rule of its own, so `adopt-project.py plan` no longer asks it the top-level data file's `spec`, `code` or `root` question for a record that no leg reads (#528)

`plan` asked about a source's own `single-repository.yaml` under
`ambiguous-top-level-yaml`: "a declaration the project has DECIDED (spec),
configuration the implementation reads (code), or project-level wiring
(root)". The file is none of those. It is this standard's optional record
that a project STAYS one repository (`contracts/single-repository-record.yaml`),
so a repository that carries one and is adopted into the Triad holds a record
of the decision the adoption reverses, and its one reader, the Triad
advisory, is silent in an assembly root whatever the file holds
(`shape_advisory._read` answers `TRIAD_ROOT` from `project.yaml` first).

`ambiguous-single-repository-record`, above `ambiguous-top-level-yaml`,
names the file and asks drop or root, drop by default (Brett Heap's ruling of
2026-10-12, the A7 line of #457). The default is the question's own words and
not a value the plan fills in: `plan` still writes `leg: null`, and `execute`
still refuses with `plan-unresolved` while it is open, so nothing is dropped
that a person did not answer `drop`. Nothing refuses `root`, unlike
`ambiguous-project-yaml`: the file blocks nothing the shape writes. Only the
name the advisory reads is asked this way; `release.yaml`, a record spelled
`.yml` and a nested `single-repository.yaml` keep the rule they had.

tests/test_adopt_single_repository_record.py (12): the rule names the file
`shape_advisory.RECORD_FILE` reads and sits above the general question; the
question offers drop and then root, says drop is the default and offers
neither `spec` nor `code`; `plan` writes it with `leg: null`; `check` and
`execute` say `plan-unresolved` while it is unanswered and `check` passes
either answer; and `execute` adopts each answer, with the record in no
repository for `drop` and the source's blob at the assembly root for `root`.
Five are red on main bfed143 (the rule and question ones).

Main: bfed143.

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

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

@sourcery-ai

sourcery-ai Bot commented Oct 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR implements the policy entirely as a path-classification data change: the source’s own top-level single-repository.yaml receives a dedicated drop-or-root question, while preserving explicit-answer semantics and existing handling for other files. A comprehensive test module validates rule precedence, plan/check/execute behavior, and the resulting repository contents for both answers.

Sequence diagram for single-repository record adoption decisions

sequenceDiagram
    actor Person
    participant Plan as adopt-project.py
    participant Classification as path-classification.yaml
    participant Execute as execute
    participant Assembly as AssemblyRoot

    Person->>Plan: plan
    Plan->>Classification: ambiguous-single-repository-record
    Classification-->>Plan: drop or root (drop is the default)
    Plan-->>Person: single-repository.yaml with leg: null

    Person->>Plan: check
    Plan-->>Person: plan-unresolved
    Person->>Execute: execute
    Execute-->>Person: plan-unresolved

    alt Person answers drop
        Person->>Plan: answer drop
        Person->>Execute: execute
        Execute-->>Assembly: omit single-repository.yaml
    else Person answers root
        Person->>Plan: answer root
        Person->>Execute: execute
        Execute-->>Assembly: write single-repository.yaml at assembly root
    end
Loading

File-Level Changes

Change Details Files
Adds a dedicated path-classification rule for a repository’s top-level single-repository.yaml, separating it from the generic top-level YAML question.
  • Matches exactly the advisory’s RECORD_FILE name and places the rule before the general YAML rule.
  • Classifies the file as an unresolved ambiguous path with a drop-first question offering only drop or root.
  • Documents that the displayed default does not populate leg, so unanswered plans remain unresolved and execution still requires an explicit decision.
contracts/path-classification.yaml
Adds focused coverage for classification, planning, validation, and execution behavior of the single-repository record.
  • Verifies rule identity, precedence, wording, default ordering, and exclusion of spec and code.
  • Confirms other YAML files, .yml records, and nested records retain their existing classification.
  • Checks that unresolved plans fail while either explicit answer passes validation.
  • Verifies drop removes the record from all output repositories, while root preserves the source blob only at the assembly root.
tests/test_adopt_single_repository_record.py

Assessment against linked issues

Issue Objective Addressed Explanation
#528 Add a dedicated path-classification rule for a source repository’s top-level single-repository.yaml, so adopt-project.py plan asks whether to drop it or keep it at the assembly root, with drop stated as the default instead of using the generic ambiguous-top-level-yaml question. ✅

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

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-2, 2026-10-14T23:55Z)

Lane openRepoShape-2 reviewed its own PR #566 at 60a258f, read-only in its own detached worktrees, and authored nothing.

CHANGES NEEDED. A fix round follows on this branch. The PR lands only on Brett Heap's word.

  1. P2: the person must answer drop or root. At contracts/path-classification.yaml:434-437 and tests/test_adopt_single_repository_record.py:141-165, plan writes leg: null and execute refuses plan-unresolved. On 2026-10-12 Brett Heap chose "Ask; default drop" ("The default answer is drop") over "Ask; no default" ("the person must answer drop or root"). Fix: a rule default that plan writes as leg: drop, keeping review_required: true, the question and Verdict.leg None (scripts/path_classify.py:38, adopt-project.py:1302). execute then drops the file unless someone writes root. Otherwise, get Brett's word for the data-only reading.
  2. P3: a top-level single-repository.yaml with another kind gets the record's question (:454-461). In the same plan run, the advisory says it "does not silence this". Default and word it only for kind: single-repository-record.
  3. Nit: the PR body misquotes README.md:779 ("never an implicit root").

Tests at 60a258f: 3851 passed, 5 skipped across 45 targeted files, with git-filter-repo and no real gh. On bfed143 the new file gives 5 failed and 7 passed, each failure for its named reason, and every guard catches a mutant.

Merges: clean with main bfed143, #567 (bdcf300), #570 (79dfd33) and fix/522 (9ddb7ef).

The full report is in opensoft/brett-wip at handoffs/xFactory/attachments/lane-openRepoShape-2/review566-60a258f/final-report.md.

…ust_pushed`, #553's adoption line, #561) so the PR's CI merge ref and this fix round run on current main

Merges origin/main 7d9bec9 into the #528
branch at 60a258f. Since the branch's base bfed143 main gained #551 (the
handbook's per-section README hashes), #552 (shape-doctor.py's machine
row), #554 (`clone_just_pushed` at every test that clones a just-pushed
local remote), #553 (shape-doctor.py's adoption and scaffold lines) and
#561 (bump-leg.py's dry run). The fix round's design leans on
shape-doctor.py's placement row, which #553 moved, so it is read and
tested on this tree.

No conflicts. tests/test_adopt_single_repository_record.py clones no
just-pushed remote (it reads the bare remotes with `git ls-tree`), so no
call site moves to `clone_just_pushed`.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 2 commits October 15, 2026 11:19
…y as its `leg:`, still `review_required: true` with the question beside it, so a source's own `single-repository.yaml` comes as `leg: drop`, which `check` passes and `execute` drops unless a person writes `root`, as Brett Heap ruled (#528)

Brett Heap's ruling of 2026-10-12 (18:15:05Z, lane 3, the A7 line of #457)
chose "Ask; default drop (Recommended)": "The default answer is drop". At
60a258f the default was only words inside the question: `plan` wrote
`leg: null`, `check` found `plan-unresolved` and `execute` refused until a
person answered, which is the option he declined, "Ask; no default" ("the
person must answer drop or root"). The classifier could not express a
default: `LEGS` has no `drop`, and `review_required` was written as
`leg is None` (rv528 F1).

- contracts/path-classification.yaml: the rule gains `default: drop`, and
  the policy header says a rule that asks may name a default, `drop` and
  nothing else, which the plan writes as `leg:` with the question kept.
- scripts/path_classify.py: `Verdict` carries `default` beside the
  question and keeps `leg` None, so `review_required` is unchanged. A
  default is refused at load time (`path-policy-default`) on a rule that
  does not ask, or as anything but `drop`: an unanswered question is
  never an implicit `root`, and a rule that answers `spec` or `code` says
  so in `leg:`. A folded directory keeps its rule's default.
- adopt-project.py: `Entry` keeps the verdict's `default` and
  `review_required`, and `with_defaults` writes the default in as `leg:`
  after both re-asks, so `ask_about_submodules` and
  `ask_about_refused_paths`, which carry none, are never answered by it,
  and `ask_about_refused_paths` cannot take a proposed `drop` for a
  person's. `walk` still leaves every question's `leg` None: the doctor's
  placement row walks a leg with it and calls a path misplaced when its
  `leg` is another, so a leg's copy stays a question there. `plan` prints
  each default with its question before the unresolved paths, and the
  plan's header says `execute` takes it as written.
- AGENTS.md step 2: one sentence, so an assistant answers a default like
  any other question; the AGENTS.md cap moves 490 -> 492 with its dated
  reason in tests/test_repo_hygiene.py.

Tests (tests/test_adopt_single_repository_record.py): `plan` writes
`leg: drop` with `review_required: true`, the rule and the question, and
prints it apart from the unresolved paths; `check` passes it unanswered;
`execute` gets past every plan refusal with it, and adopts with the
record in no repository and named among the split commit's dropped
paths. The default is refused unless it is `drop` on a rule that asks; a
folded directory keeps it; neither re-ask carries one, and in `plan` a
default over a name `execute` will not extract is not written in; and
the doctor's own `walked_findings` reads a leg's copy as a question.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, only of a top-level `single-repository.yaml` that declares `kind: single-repository-record` at the commit it plans, so a file of that name that is no record is asked the general question again and is not dropped by a default meant for the record (#528)

In the standard `kind:` decides and not the filename
(scripts/shape_advisory.py). At 60a258f a top-level
`single-repository.yaml` declaring `kind: release-config` got the
record's question: one `plan` run printed "NOTE single-repository.yaml
does not silence this: it declares kind 'release-config'" and then told
the person the file "records that the repository stays a single
repository" (rv528 F2). With the default the previous commit writes in,
that file would also have been dropped unread.

- adopt-project.py: `ask_about_a_record_only` reads the file at the
  planned commit with `shape_advisory.read_record(Tree(path, commit))`,
  the advisory's own reading, and where it is no record (another kind,
  not a mapping, unparsable, not a file) classifies the entry as if the
  policy held no `ambiguous-single-repository-record` (`RECORD_RULE`):
  for a top-level YAML file, `ambiguous-top-level-yaml`'s question and
  `leg: null`, as before the rule existed. A source with no entry that
  rule answered is not read. It runs on `walk`'s entries before anything
  reads them, so the follow-ups, the seeding and the re-asks see the
  same entry the plan writes.
- scripts/path_classify.py: `rule_for` and `classify_file` take `skip`,
  rule ids to pass over.
- contracts/path-classification.yaml: the rule's comment says `kind:`
  decides, and that the doctor, which reads no file here, asks a leg's
  copy by name.

Tests: each non-record body (another kind, not a mapping, does not
parse) is planned under the general rule with its question and
`leg: null`, and `check` finds it `plan-unresolved`; a record that
leaves a field out is still the record; an uncommitted record over a
committed non-record is not the record; and `RECORD_RULE` names the
policy's rule.

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-15T13:35Z)

Lane openRepoShape-2 re-verified its own PR #566 at 19ffe4a, read-only in its own detached worktrees, and authored nothing.

MERGEABLE WITH NITS. F1, F2 and F3 are fixed. The PR lands on Brett Heap's word, after a refresh on main.

  1. P3, follow-up on Tracking: shape-doctor.py, reporting a repository's compliance #460: shape-doctor.py:4492 walks a leg by name only, so a leg's single-repository.yaml that declares another kind gets the record's question in a note row. On main it got the general question.
  2. Nit: README.md:776-779 still says every ambiguous entry gets leg: null. docs/handbook.html: stop every README PR rewriting the handbook's README blob and line cap, so open README PRs stop conflicting with each other #519 has landed, so the sentence can go in now or be filed.

Tests at 19ffe4a: 3930 passed, 5 skipped and 0 failed across 46 targeted files, with no real gh. On the predicted merge with main dd3f23a: 713 passed and 5 skipped in 9 files. Against the old code, the new file gives 23 failed and 7 passed, each failure for its named reason.

Merges: clean with main 64fc9a8 and dd3f23a (AGENTS.md 492, cap 492), #572 (9ddb7ef, 22ce69b) and #570 (b1db67c).

The full report is in opensoft/brett-wip at handoffs/xFactory/attachments/lane-openRepoShape-2/review566-60a258f/reverify-19ffe4a.md.

@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap merged commit 1d809ea into main Oct 15, 2026
3 checks passed
@brettheap
brettheap deleted the fix/528-adopt-single-repository-question branch October 15, 2026 18:44
brettheap pushed a commit that referenced this pull request Oct 15, 2026
Brings in main bcfa364: #588 (709eec1, POSIX-quoted paste lines and
README's Windows sentence) and #569 (bcfa364, setup-project.py's paste
lines). One conflict:
	docs/handbook.html: the seven markers that read README.md § Starting a
	  project in a new organisation (masthead, start, partway, ci, legs,
	  doublepin, park) conflicted, this branch's a0e0f23c against #588's
	  95f06438. Both moved to the merged section's blob,
	  d77a1e59fe4951aa2a6f96808725d0506d4ce357, as the hygiene check printed.
	  Both sides' prose merged on its own: #588's POSIX-quoting sentence in
	  the Windows panel beside #587's install step, NOTE and macOS sentence.
	  Re-read: nothing in the seven sections is made false by the other side.
README.md and docs/cli.md merged cleanly with both sides' hunks, and
`render-cli-reference.py --check` passes. README.md is 1449 lines (cap
1449) and AGENTS.md 492 (cap 492, raised by #566 on main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 15, 2026
…eck`'s finding for a link kept above a path the shape writes says the clone holds it as a file, so `scripts -> tools/deep` no longer says where the scripts land and sends the person to change where it points, on to `adopt-symlinks-off` (#531) (#584)

Some disks check a symlink out as a plain file: under `core.symlinks=false`, which is Git for Windows' default and what `git init` writes on a filesystem that cannot hold a symlink. On such a disk, a symlink the plan keeps in the root ABOVE a path the shape writes is a plain file in the assembly's clone, and nothing can be written beneath it.

When `check` reports such a link as `plan-root-symlink`, its finding now says that, in `adopt-symlinks-off`'s words, and gives a fix that works on that disk: send the link to a leg or `drop` it in the plan, and run `check` again. `execute` refuses with the same line, word for word, before any leg exists, as it refused the old line. Only the text of a finding `check` already made changes. A link above that the walk follows cleanly (`scripts -> ci`) stays `execute`'s `adopt-symlinks-off` before any leg, as #504 designed. With symlinks on, the output is byte-identical to `main`'s.

`_a_file_above(walk, probe)` runs only for a walk that is about to be reported, so a plan with no such finding makes no new probe, and no network or gh call is added.

Tests: tests/test_adopt_root_symlinks.py holds 149 tests on `main` and 156 here. `test_with_symlinks_off_a_link_above_a_path_is_named_as_a_file` (6 cases) and `test_with_symlinks_off_the_fix_the_finding_gives_works_there` (`code`, `drop`) are new. Three existing tests of a followed link gain `links_as_links`, so they keep asserting the symlinks-on line wherever the runner's checkout writes a plain file, which a Windows runner's may (Git for Windows' default). Main's test of the old reason is now the D6 row.

Review: the adversarial review found the change PR-READY at 0a178e4, with five text corrections, all in the PR body. The edge cases it found outside #531 are left for #457's checklist.

`main` was merged in five times, and the branch was never rebased.
- **The first merge** was GitHub's update-branch, at 348f8a8 (main 2e6c6fa). 348f8a8 is green on Linux, macOS, Windows and SonarCloud.
- **The second merge was by hand,** at cae5a98 (main 654c476), after #581 (#524) changed `adopt-project.py`. Its one conflict was a comment in `_SymlinkProbe`, about a work directory no repository can be made in. The resolution keeps both #531's sentence and #524's.
- **cae5a98c** passed 71 adopt test files in both modes: 5,796 passed and 5,482 passed, with 0 failed.
- **The third merge** was GitHub's update-branch, at 7f439a5 (main a61cf47), after #589 changed `adopt-project.py`. It was a clean merge.
- **The fourth and fifth merges** were also GitHub's update-branch, both clean, after #566 and #582 changed `adopt-project.py`: 250b070 (main 1d809ea) and c43cde1 (main 526cd2c).
- **Since #597, CI is Linux only.** The head is green on Linux and SonarCloud (ruling 27).

Closes #531

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 15, 2026
…n-source-unread

Merges origin/main 66267f0 so the PR's CI merge ref and the fix that follows
run on current main. The fourteen landings since 2bf8fd4: #597, #578, #593,
#589 (#449), #586, #573 (#537), #575 (#539), #566 (#528), #571, #588, #569
(#538), #582 (#523), #616 and #576 (#441, #442).

Those that touch this PR's files:
- adopt-project.py: #589 (`check` recomputes each pin's digest in the clone
  `plan` read it from: LOCAL_CLONE_RE, _recorded_clone, _why_not_rechecked,
  _recomputed_pin_finding, _pin_digest_findings), #582 (what `plan`,
  `execute` and AGENTS.md say around a run) and #566 (a source's own
  `single-repository.yaml`).
- scaffold-project.py and docs/cli.md: #573, #569 and #576 (the
  `_declaration` docstring: `--pin-source` verifies the first chain link
  alone, which this PR's N3 wording already says).
- tests/test_adopt_pins.py: #589's thirteen cases.
- tests/test_scaffold_leg_path_collisions.py: #573.

This commit holds the textual resolution only. One hunk conflicted, the
`from shape_materialize import (...)` list at adopt-project.py:261. It is
resolved as the union of both sides' changes to the list: main's added
`pin_digest_from_source` (#589), and this PR's added `DIGEST_FROM_CLONE`,
`DIGEST_FROM_GH`, `pin_source_path` and `refuse_an_unread_pin_source` and
its removal of `one_printable_line`, whose last caller this PR deleted and
to which main added none (keeping it is pyflakes' "imported but unused").
pyflakes adopt-project.py is clean on the merge. Every other file merged
without a marker, and `scripts/render-cli-reference.py --check` is ok.

The merge alone does not keep #589 working: this PR's `plan` records a bare
`local-clone`, which names no clone for #589's `check` to read, and `check`
refuses `local-clone (<path>)` as `plan-pin-malformed`. The next commit
reconciles the two.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 15, 2026
…s, so PR #620 merges cleanly (#601)

Main moved through #566, #582, #584, #591, #572 and #570 since this branch's
base, 2989b4c. One hunk in adopt-project.py conflicted, in
`_checked_plan_inputs`: this branch replaced the inline `adopt-bad-id` check
with `_refuse_bad_id`, and #591 (#427, #521) replaced the inline organisation
block just below it with `_planned_org` and `org_given`. The resolution keeps
both: `_refuse_bad_id(args.id, args.project, id_passed)` where the id is
checked, and main's `org_given = bool(args.org)` and `_planned_org(...)`
where the organisation is read. Everything else, the two new test files'
hunks included, merged on its own.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 16, 2026
…y.yaml` record arrives as `leg: drop`, that the adopted Makefile's `CONTRACTS_DIR` follows the spec leg's mount, and that a second setup run is refused at the plan, so the three sentences #566, #572 and #573 left false or incomplete are true of main (#606) (#627)

One README and handbook change for three lines of the v1.1.0 checklist
triage (D7), with the words #566, #572 and #573's bodies held, checked
against the code on main first, and for one sentence #621 (#602) leaves
false.

- L2-50, "Ambiguous is an answer": a source's own `single-repository.yaml`
  record is written `leg: drop`, its rule's default, still
  `review_required: true` with its question beside it, and `execute` takes
  it as written. It drops the record unless a person changes `leg:`; the
  question offers `root`, and `check` passes `spec` too, which the #566
  body's "unless a person writes `root`" left out.
- L2-51, the worked example: the root Makefile exports `CONTRACTS_DIR` at
  the `contracts/` of the spec leg where the plan mounts it,
  `$(CURDIR)/spec/contracts` by default (#572).
- L2-41, the "If it fails part-way" box: a second run is refused at the
  plan, before the `yes`, because the plan is the scaffold's `--dry-run`
  and asks whether each of the three already exists (#573). The #573 body
  said step (5), which is the number `setup-project.py` prints; the README
  numbers the plan (3), so it says (3).
- The Free-plan secrets (#621's rule): the README said a private leg's CI
  credential is kept "as REPOSITORY secrets on the GitHub Free plan",
  which is false of a public root, because the assembly root's `validate`
  reads the secrets and on Free the organisation's secrets reach public
  repositories. README § Starting now says "on the assembly root when it
  is private or internal and the organisation is on the GitHub Free plan",
  in the six lines it had; "Reading private legs in CI" says the root
  decides and that a public root takes org secrets registered with
  `--visibility selected` and the root in `--repos`, or `all`, which
  shows them to every repository, since the commands above it default
  to `private`, which on Free reaches no repository (+3 lines); the
  handbook's `ci` part says the same.

`tests/readme-cap.d/606.txt` raises the cap by the ten lines (1449 to
1459). The handbook's list item and adopt paragraph carry the same
sentences, and its markers move to the new hashes of README § Starting a
project in a new organisation (seven page parts), § The three legs (three)
and § Adopting an existing repository (one). No footer clause: ruling 29
retired them.

Lands after #621: the Free-plan sentences describe the root rule #621 puts
in the tools.

Closes #606

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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

Development

Successfully merging this pull request may close these issues.

adopt-project.py plan: ask about single-repository.yaml with its own drop-or-root rule, not the generic ambiguous-top-level-yaml question

1 participant