Skip to content

shape-doctor.py: the placement row asks a leg's top-level single-repository.yaml the staying-single record's question only where kind: makes it the record, so one declaring release-config is asked the general question as plan asks it, and an answered entry names that rule (#691) - #734

Merged
brettheap merged 3 commits into
mainfrom
l3-691-placement-record
Oct 16, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-3 (openRepoShape-3)
Claim: #691 (comment)

Closes #691.

What changed and why

The doctor's placement row walks each leg with adopt.walk, whose policy rule for the staying-single record (ambiguous-single-repository-record) matches the name single-repository.yaml alone. A leg's copy declaring kind: release-config was told it records that the repository stays single, and offered drop as the default, on every run. adopt-project.py plan asks that file the general top-level-YAML question, because ask_about_a_record_only reads its kind:. #617 narrows the difference (execute records the answer under answered_paths:), but an answered entry still named the record's rule, and every project adopted before #617 is asked the record's question on every run.

  • shape-doctor.py:6507: audit_leg calls adopt.ask_about_a_record_only(policy, adopt.walk(policy, kept), adopt.Source(mount, None, "", "")), plan's own function (adopt-project.py:1869). The doctor carries no copy of it, so the doctor's statements that it reimplements nothing (shape-doctor.py:9, :152, the IT REIMPLEMENTS NOTHING paragraph, README.md:1211-1213, docs/handbook.html:1632-1634) stay true and are unchanged. The function reads only source.path and source.commit, so adopt-project.py is not changed.
  • THE LEG'S INDEX IS READ, NOT ITS DISK (the open point of the first round, decided). A commit of "" is git's :<path>, the index leg_tracked_files lists. So a tracked link is read as its blob and never followed (not into the leg, not outside the root), and a record deleted or edited on disk and not staged is asked as the index holds it, as plan reads its commit. shape-doctor.py:6462-6467 says so in audit_leg's docstring.
  • An answered entry for a file that is no record names ambiguous-top-level-yaml. The verdict's leg stays null, so the verdict and exit code do not move.
  • contracts/path-classification.yaml:441-452: the comment says the doctor asks the record's question only of a leg's copy that declares the record's kind in the leg's index, and drops "by name".

Tests

  • tests/test_adopt_single_repository_record.py, beside test_the_doctor_reads_a_legs_copy_as_a_question_and_not_misplaced, through audit_leg on a real git leg (leg_audited, :373, whose tree holds widgets/a.c and widgets/b.c so that a reclassification of every entry adds a second question):
    • a leg's single-repository.yaml that declares release-config, is not a mapping or does not parse gets the general question and rule, with its files == 1 and its bytes (:400);
    • a record, full or kind: alone, keeps the record's question and its stated default, with its files and bytes (:420);
    • a record deleted on disk, a record edited to another kind and not staged, and a non-record edited to a record and not staged are read as the index holds them (:447);
    • a tracked link to a record beside the leg is asked the general question; skipped on Windows and wherever a link cannot be made (:458).
  • tests/test_doctor_recorded_answers.py:225: through the doctor's --json run, an answered single-repository.yaml that is not a record names ambiguous-top-level-yaml; a record names the record's rule.
  • Mutants: reading the disk (Source(mount, None, "", None)) fails the three index cases and the link case; a reclassification of every entry fails the three non-record cases and the edited-to-a-record case; dropping files/bytes fails the three non-record cases. The first round's four behaviour cases fail on main and pass here.
  • The bytes assertion compares with the leg file's own read_bytes(), not len(text.encode()), because make_source_repo writes with write_text and a Windows checkout would count CRLF.
/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt -o tmp_path_retention_policy=failed tests/test_adopt_single_repository_record.py tests/test_doctor_recorded_answers.py tests/test_doctor_plan_overrides.py tests/test_scaffold_leg_path_collisions.py tests/test_repo_hygiene.py tests/test_cli_reference.py
293 passed in 77.68s (0:01:17)

tests/test_shape_doctor.py, run once, same flags: 428 passed, 3 skipped, 1 xfailed in 336.95s (0:05:36).

Pinned copies

None touched. shape-doctor.py and contracts/path-classification.yaml are in no COPIED_VERBATIM, FAMILY_COPIED_VERBATIM, COPIED_FROM_SHAPE or assembly-root template list (grep in scripts/shape_materialize.py and templates/), so no project sees an upstream-changed row. No test pins the comment's text. No doc states the by-name behaviour, so README.md, AGENTS.md, docs/cli.md and docs/handbook.html are unchanged.

Lands after

#655 (PR #721) has landed; main is merged into this branch (66b230e), cleanly.

🤖 Generated with Claude Code

Summary by Sourcery

Align placement auditing with planning by classifying a leg’s staying-single file from its indexed content and preserving the corresponding answer rule.

Bug Fixes:

  • Ensure placement audits ask a leg’s single-repository.yaml the staying-single record question only when its indexed content declares the record kind.
  • Record the rule that actually asked each answered top-level YAML entry, including general handling for non-record files.

Enhancements:

  • Read leg file contents from the index, matching planning behavior for unstaged edits, deletions, and tracked links.
  • Add coverage for record classification, index-versus-disk behavior, symlinks, answer tracking, and file-size accounting.

Tests:

  • Expand adoption and doctor tests to verify record-specific versus general questions and indexed leg behavior.

…epository.yaml` the staying-single record's question only where `kind:` makes it the record, so one declaring `release-config` is asked the general question as `plan` asks it, and an answered entry names that rule (#691)

`audit_leg` walked a leg with `adopt.walk`, whose rule for the record is
by name alone, so a leg's `single-repository.yaml` declaring another kind
was told it records that the repository stays single, and was answered
under `ambiguous-single-repository-record`, on every run of a project
adopted before #617 recorded answers under `answered_paths:`.

`ask_about_a_record_only` (shape-doctor.py) reads the leg's file with
`shape_advisory.read_record` and, unless it silences the advisory,
reclassifies that entry with `policy.classify_file(path,
skip=(adopt.RECORD_RULE,))`, as `adopt-project.py`'s function of that name
does for `plan`. `adopt-project.py` is not changed. The comment at
contracts/path-classification.yaml:443-446 now says the doctor asks the
record's question of a leg's copy only where it declares the record's kind.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Placement auditing now shares adoption planning's kind-based classification for single-repository.yaml, reading each leg's Git index and recording answers under the matching question rule, with comprehensive regression coverage for record types, index-versus-disk behavior, links, and row metadata.

Sequence diagram for index-based placement auditing

sequenceDiagram
    participant Doctor as shape-doctor.py
    participant Index as Git index
    participant Adopt as adopt.ask_about_a_record_only
    participant Policy as path-classification.yaml

    Doctor->>Index: list leg_tracked_files
    Doctor->>Adopt: ask_about_a_record_only(policy, walked, Source(mount, None, "", ""))
    Adopt->>Index: read :single-repository.yaml
    Adopt->>Policy: classify indexed kind
    alt kind is single-repository-record
        Policy-->>Adopt: ambiguous-single-repository-record
    else any other kind or invalid YAML
        Policy-->>Adopt: ambiguous-top-level-yaml
    end
    Adopt-->>Doctor: walked findings and matching answer rule
Loading

File-Level Changes

Change Details Files
Align placement auditing with adoption planning when classifying each leg's single-repository.yaml.
  • Reuse adopt.ask_about_a_record_only rather than duplicating plan logic.
  • Classify the file from the leg's Git index, preserving behavior for unstaged edits, deletions, and tracked links.
  • Ask the record-specific question only when indexed kind: is single-repository-record; otherwise use the general top-level YAML question.
  • Preserve file counts and byte sizes in generated review rows.
shape-doctor.py
contracts/path-classification.yaml
Record answered placement paths under the rule that actually asked the question.
  • Use ambiguous-single-repository-record for files whose kind identifies the staying-single record.
  • Use ambiguous-top-level-yaml for same-named files declaring another kind or invalid content.
  • Keep verdict status and leg assignment unchanged.
tests/test_doctor_recorded_answers.py
shape-doctor.py
Add regression coverage for classification, index semantics, symlinks, and metadata.
  • Test record and non-record contents, malformed files, and kind-only records.
  • Verify index contents take precedence over unstaged disk changes.
  • Verify tracked links are read as blobs and not followed.
  • Assert question text, rules, file counts, bytes, and recorded answer names.
tests/test_adopt_single_repository_record.py
tests/test_doctor_recorded_answers.py

Assessment against linked issues

Issue Objective Addressed Explanation
#691 In the doctor’s placement audit, ask the staying-single record question for a leg’s top-level single-repository.yaml only when its contents declare kind: single-repository-record; otherwise classify and ask the general top-level-YAML question. ✅
#691 Read the leg’s indexed file contents when deciding which question applies, matching adoption planning behavior for unstaged edits, deletions, and tracked links. ✅
#691 Ensure recorded answers for a non-record file name the general top-level-YAML rule, and update the path-classification documentation to describe the corrected behavior. ✅

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-3, 2026-10-16T23:09:05Z) Lane 3's independent review of #734 at 109b77b0 (RV-e, ruling 8): NOT READY. There is one MUST, three SHOULDs and three NITs. The behaviour is right for every ordinary file, and nothing else in the output moves. The writer takes one round, and the MUST's fix also clears SHOULD-1 and SHOULD-2.

MUST-1: the doctor now carries its own copy of plan's logic.

  • shape-doctor.py:5902-5937 is a near line-for-line copy of adopt-project.py:1869 ask_about_a_record_only, under the same name.
  • That makes four sentences false:
    • shape-doctor.py:9 ("IT REIMPLEMENTS NOTHING");
    • :151-153 ("rather than carrying a second copy of the classification");
    • :6785-6791;
    • README.md:1211-1213 and docs/handbook.html:1632-1634 ("no second copy of that classification to drift").
  • adopt's function reads only source.path and source.commit, so the doctor can call it with no change to adopt-project.py.
  • Fix: delete the copy, and in audit_leg read the leg's INDEX ("" is git's :<path>), so a link is read as its blob and never followed:
    walked = adopt.ask_about_a_record_only(policy, adopt.walk(policy, kept), adopt.Source(mount, None, "", ""))
    Then reword contracts/path-classification.yaml:450 to "declares it in the leg's index".
  • Measured on main 66b230ed: 192 passed, and the doctor matches plan in all 28 edge cases.

SHOULD

  • SHOULD-1: a tracked symlink is followed, even outside the root. read_record(mount) → is_file() follows links.

  • SHOULD-2: the working tree against the index. The writer's open point is decided for the index. The row lists paths from the index, and its docstring keeps a path's classification when nothing is on disk.

    index disk head plan
    record deleted general record
    record edited to release-config, not staged general record
    record replaced by a directory general record
    release-config edited to a record, not staged record general

    Reading the index makes all four match plan. Pin it with two tests beside leg_audited (tests/test_adopt_single_repository_record.py:368):

    • a committed record deleted on disk keeps the record's question;
    • a tracked link to a record gets the general question, skipped where symlinks cannot be made.
  • SHOULD-3: a mutant that reclassifies EVERY entry of such a leg passes all 64 tests. On a leg with widgets/a.c, widgets/b.c and widgets/c.h, it adds code/widgets/ as a new question. Add widgets/a.c and widgets/b.c to leg_audited's tree.

NIT

  • NIT-1: the record entry's files and bytes are never asserted. Assert files == 1 and bytes == len(text.encode()).
  • NIT-2: the commit message gives :443-446 and the body :443-448. The comment runs :441-452.
  • NIT-3: the commit trailer names Sonnet. That is right: the writer ran on sonnet-xhigh.

What is right as written

  • A leg's single-repository.yaml that is not a record now gets the general question and rule: in the row, in --json, in the --placement-plan file and on an answered_paths: entry.
    • This covers release-config, a list, unparsable YAML, an empty file, a file that is not UTF-8, and CRLF.
  • A record keeps its question: a full record, kind: alone, CRLF, a BOM, ---, or a quoted kind.
  • Across 28 cases, only that one entry's rule, question and reason change. The verdict and exit do not move. adopt-project.py is not in the diff.

Other checks

Lane: openreposhape-3 (openRepoShape-3)

…ask_about_a_record_only` over each leg's index, so a leg's `single-repository.yaml` is asked the record's question where the index holds a record, and a link or a file changed or deleted on disk is read as `plan` reads it (#691)

RV-e's round on #734. The first commit gave the doctor a copy of `plan`'s
`ask_about_a_record_only` under the same name, which made the doctor's own
statements that it carries no second copy of the classification false, and
read the leg's disk where the row lists its index: a tracked link was
followed, including outside the root, and a record deleted or edited on
disk and not staged was asked the other question than `plan` asks.

`audit_leg` now calls `adopt.ask_about_a_record_only(policy, adopt.walk(
policy, kept), adopt.Source(mount, None, "", ""))`. That function reads only
`source.path` and `source.commit`, and a commit of `""` is git's `:<path>`,
the index `leg_tracked_files` lists, so a link is read as its blob and never
followed. The copy in shape-doctor.py is deleted and `adopt-project.py` is
not changed. The comment in contracts/path-classification.yaml says the
doctor reads the leg's index.

tests/test_adopt_single_repository_record.py: the leg's tree holds `widgets/`
beside the source file, so a reclassification of every entry adds a second
question; the reclassified and the kept entry assert their `files` and
`bytes`; a record deleted on disk, a record edited to another kind and
a non-record edited to a record, none staged, are read as the index holds
them; and a tracked link to a record beside the leg is asked the general
question, skipped where a link cannot be made.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-16T23:22:11Z) Lane 3's independent re-verification of #734 at 79e862ca (RV-e, ruling 8): READY. Every MUST, SHOULD and NIT from round 1 is fixed.

Round-1 items

  • MUST-1: the doctor's copy is gone, and shape-doctor.py:6507 calls plan's own ask_about_a_record_only over the leg's index. Comparing the two tools' ^def names now leaves only main. Nothing changed in adopt-project.py, scripts/, README, AGENTS.md or docs/, so the four "reimplements nothing" sentences are true again. contracts/path-classification.yaml:449-450 reads "declares it in the leg's index".
  • SHOULD-1: a link to a record, inside the leg or outside the root, gets the general question, as plan gives it. The test is at tests/test_adopt_single_repository_record.py:458.
  • SHOULD-2: the doctor matches plan in 28 of 28 edge cases, including the four disk-only states. The tests are at :442-447.
  • SHOULD-3 and both NITs are fixed.
  • No other output moves: against main, only the record entry's rule, question and reason change. Through the CLI, --json and --placement-plan agree, and each case gives exit 0, COMPLIANT, note.

The writer's open point, an unmerged single-repository.yaml, is ruled right. The index has no stage 0, so the file gets the general question. A conflicted file is not one file until it is resolved, and the row is a note that moves no verdict. A new, optional NIT: that case is untested, and a mutant that only changes it survives.

Mutants: RV-e killed six of seven:

  • the doctor reading the working tree;
  • main's call;
  • reading the assembly root;
  • files and bytes forced;
  • reclassifying every entry;
  • dropping the reclassification.

The seventh survives and touches only the unmerged case above.

Tests (stub gh): 293 passed at the head. Merge with main 66b230ed: clean, and the head's tree is the merge's tree.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

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