Skip to content

shape-doctor.py: a validator's verdict is a printed FINDING or REFUSED and any other non-zero exit is a crash, so a gate killed by a signal, ended by os._exit(1), holding conflict markers or failing argparse no longer "reports a finding", a crash of the standard's own naming script is CANNOT ANSWER instead of INVALID (naming), and a pass that printed WARNING or note lines is a note quoting them while a red row quotes its finding and never a warning (#410, #448) - #557

Merged
brettheap merged 2 commits into
mainfrom
fix/doctor-validator-verdicts
Oct 15, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-4 (openRepoShape-4)
Claim: #410 (comment)
Claim: #448 (comment)
Claim: #447 (comment)

What changed and why

This PR applies one rule to every child the doctor reads. It comes from the v1.1.0 triage on #410 (comment), which Brett Heap approved at 14:31:28Z on 2026-10-14:

A verdict is a printed FINDING or REFUSED. Any other non-zero exit is a crash. A WARNING or note line on a pass makes the row a note.

The change is in shape-doctor.py only.

Measured: #410's case (1) is not gone on main

The brief expected the stock naming script to classify a name that is not UTF-8 without a traceback. On 5ed3e5d here (Python 3.12.3, LC_ALL=C.UTF-8, and the PYTHONIOENCODING=utf-8 the doctor gives every child), the result was:

  • scripts/validate-repository-naming.py --explain -- $'Atl\xffas' exits 1 with UnicodeEncodeError: 'utf-8' codec can't encode character '\udcff'. Its --explain block prints the name to a strict stdout.
  • main's doctor gave naming: FINDING | Atl?as: UnicodeEncodeError: ....

With this PR, that row is the crash row (detail.crashed, detail.environment, "crashed before reaching a verdict"), and the verdict is still NOT A SHAPE ROOT. The script itself is unchanged: the brief rules out editing it, and #441 (lane 5) does not touch its printing. So #410's decision 3 (the script's own crash) is still open and needs an owner. The regression test accepts either true row: the crash row now, or the naming-unclassified FINDING once the script prints such a name. It never accepts the exception quoted as a finding.

Tests

New file tests/test_doctor_validator_verdicts.py, built on #357's harness (tests/test_doctor_validator_rows.py), with its fixtures imported. It has 22 tests:

One existing test changed in place: tests/test_pin_source_env_collisions.py::test_a_project_declaring_a_colliding_pair_takes_a_shape_update. It asserted that the pins row stays ok over a validator that passes and prints the neutral-pin-source-collides note. Before and after the update, that row is now a note with the note in detail.notes and quoted in the reason. The verdict still equals the control's.

Targeted run (Linux, py-bench, pytest 9.1.1, frvenv first on PATH), over 430c13f:

pytest tests/test_doctor_validator_verdicts.py tests/test_doctor_validator_rows.py tests/test_doctor_gitlink_repairs.py tests/test_doctor_next_is_a_command.py tests/test_pin_source_env_collisions.py tests/test_manifest_validator.py tests/test_naming_policy.py tests/test_shape_doctor.py tests/test_repo_hygiene.py tests/test_cli_reference.py tests/test_agents_md_codes.py -q
1072 passed, 3 skipped in 503.58s (0:08:23)

Mutants, each run against its target tests and then restored, all killed:

mutant fails
restore the Traceback requirement in crashed() the 5 own-copy endings, the 2 template endings, the crashed() unit test
drop the WARNING collection the #448 note test, the reported() unit test
quote WARNING before FINDING the decision-4 test, 2 run_validator quoted-line cases
drop environment from the naming crash the CANNOT ANSWER test, the not-a-root test
print the cannot-answer refusal under any verdict the not-a-root test
REPORT_ITEM without note both note/skip orders, the reported() unit test

Shared files

git merge-tree --write-tree of this head with each of these exits 0 (clean):

Lane 4's P1 (fix/doctor-next-detached-commits, #413/#414) owns submodule_update_next, check_legs, members_verdict and detached, and this PR edits none of them. P1 is not on origin yet, so there is no merge-tree result for it.

v1.1.0: lands only after the v1.0.0 tag.

Closes #410
Closes #448
Closes #447

Lane 3, as #460's keeper: this PR closes #460's pins-note line (comment 6136245436); please tick it there when this lands.

🤖 Generated with Claude Code

Summary by Sourcery

Make shape-doctor validator verdict handling reflect explicit child output, distinguishing findings, crashes, and informational notes accurately.

New Features:

  • Classify validator children by printed FINDING or REFUSED verdicts, treating all other non-zero exits as crashes.
  • Surface warnings and notes from successful validators as non-verdict notes with structured details.
  • Treat crashes in the standard's naming script as environment failures that produce CANNOT ANSWER when applicable.

Bug Fixes:

  • Prevent crash output, naming-script exceptions, and warning lines from being misreported as project findings.
  • Ensure red validator rows quote their actual finding or refusal before earlier warnings.
  • Avoid emitting CANNOT ANSWER refusals under NOT A SHAPE ROOT verdicts.

Enhancements:

  • Improve crash diagnostics for signal termination, empty output, syntax errors, and argument parsing failures.
  • Preserve and report validator skip, warning, and note blocks in printed order.

Tests:

  • Add comprehensive coverage for validator crash modes, naming-script failures, warning and note handling, verdict-line selection, and pins-note reporting.

…USED` and any other non-zero exit is a crash, so a gate killed by a signal, ended by `os._exit(1)`, holding conflict markers or failing argparse no longer "reports a finding", a crash of the standard's own naming script is CANNOT ANSWER instead of `INVALID (naming)`, and a pass that printed `WARNING` or ` note ` lines is a `note` quoting them while a red row quotes its finding and never a warning (#410, #448)

One rule for every child the doctor reads, from the v1.1.0 triage of
#410 that Brett Heap approved at 14:31:28Z on 2026-10-14: a verdict is a
printed `FINDING` or `REFUSED`, anything else non-zero is a crash, and a
WARNING or note line on a pass is a `note`.

`crashed()` no longer asks for a `Traceback (most recent call last):`
line. Every finding and refusal the shipped validators and the naming
script choose to report is a line starting `FINDING` or `REFUSED`,
printed before the exit 1 or 2 that goes with it, so a non-zero exit with
neither line is a child that stopped before it said anything. That
brings in #447: a negative exit (SIGKILL -9, SIGTERM -15), `os._exit(1)`
with nothing printed, a `SyntaxError` from merge-conflict markers (no
traceback header), `can't open file` and an argparse `usage:` error, each
of which read "reports a finding" or "refuses". The quoted line is the
output's last, or, with no output, words saying so that name the signal.
A FINDING printed before the child died still stands, as #357 ruled.

Both `naming` rows now ask `crashed()` (#410). The naming script is always
this checkout's, so a crash carries `detail.crashed` and
`detail.environment`, like a crashed template validator: an assembly
root is CANNOT ANSWER, exit 3, and a directory that is not a root stays
NOT A SHAPE ROOT, exit 2, because `verdict_for` decides that first. The
text report prints its `shape-doctor-cannot-answer` refusal only under a
CANNOT ANSWER verdict, so none appears under NOT A SHAPE ROOT.
`check_naming` keeps `--quiet`: the `manifest` row now carries the same
chain WARNING.

On exit 0, `validator_row` reads `  skip `, `WARNING ` and `  note ` blocks
with one reader (`reported`), each into its own `detail` key (`skipped`,
`warnings`, `notes`), quoted in the reason in the order printed. So
validate-manifest.py's `WARNING naming-referent-chain` (#448) and
validate-pins.py's `  note neutral-pin-source-collides` (#460's pins-note
line, comment 6136245436) reach the report as a `note` that moves no
verdict. `REPORT_ITEM` learns `  note `, so a note after a skip is no
longer read as that skip's text. A red row's quoted line is the first
FINDING or REFUSED, else the first WARNING, else the last line, so a
manifest row no longer quotes an earlier leg's WARNING as its reason
(#448's decision 4).

`with_gitlink_repairs` and `with_merged_away_rows` read the whole output
and skip a crashed row, so the wider crash rule only keeps them off rows
that printed no finding. Measured on 5ed3e5d: the stock naming script
still raises `UnicodeEncodeError` printing a name that is not UTF-8
under the doctor's `PYTHONIOENCODING=utf-8`, so #410's case (1) is still
there. The doctor's row now calls it a crash of this checkout's script;
the script itself is unchanged, by the brief's ruling.

Tests: tests/test_doctor_validator_verdicts.py, new, on #357's harness.
tests/test_pin_source_env_collisions.py's colliding-pair test expected
the `pins` row to be `ok` over that note; it now expects a `note`.

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

sourcery-ai Bot commented Oct 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR centralizes child-validator interpretation around printed verdict lines: non-zero runs without FINDING or REFUSED are crashes, naming-script crashes become environment/CANNOT ANSWER results where appropriate, successful warnings and notes become visible non-verdict rows, and failing rows quote the relevant verdict instead of an earlier warning.

Sequence diagram for child validator crash handling

sequenceDiagram
    participant Doctor
    participant Validator
    Doctor->>Validator: run_validator()
    Validator-->>Doctor: exit code and output
    Doctor->>Doctor: crashed(code, output)
    alt FINDING or REFUSED printed
        Doctor->>Doctor: Keep validator verdict
        Doctor-->>Doctor: Create finding or refusal row
    else Non-zero without verdict
        Doctor->>Doctor: Mark detail.crashed
        Doctor->>Doctor: Quote last line or signal_name()
        Doctor-->>Doctor: Create crash row with CRASH_WHY
    end
Loading

Flow diagram for validator verdict classification

flowchart TD
    A["Run child validator"] --> B{"Exit code is 0?"}
    B -->|Yes| C["reported(): read skip, WARNING, and note blocks"]
    C --> D{"Reports found?"}
    D -->|No| E["OK row"]
    D -->|Yes| F["NOTE row with details and quoted reports"]
    B -->|No| G{"Output contains FINDING or REFUSED?"}
    G -->|Yes| H["Verdict row; quote first verdict line"]
    G -->|No| I["Crash row; quote last output line or signal"]
Loading

Flow diagram for naming-script crash classification

flowchart TD
    A["Run naming script"] --> B["crashed(code, output)"]
    B -->|"No crash"| C["Use naming verdict"]
    B -->|"Crash"| D["naming_crashed_row()"]
    D --> E["Set detail.crashed and detail.environment"]
    E --> F{"Assembly root?"}
    F -->|Yes| G["CANNOT ANSWER"]
    F -->|No| H["NOT A SHAPE ROOT"]
    G --> I["Report CANNOT ANSWER refusal"]
    H --> J["Do not print cannot-answer refusal"]
Loading

Flow diagram for successful validator reports

flowchart TD
    A["Validator exits 0"] --> B["reported(output)"]
    B --> C{"skip, WARNING, or note blocks?"}
    C -->|No| D["OK row"]
    C -->|Yes| E["Store skipped, warnings, or notes"]
    E --> F["passed_but() builds quoted reason"]
    F --> G["NOTE row with no NEXT and no verdict movement"]
Loading

Flow diagram for selecting a failing row's quoted line

flowchart TD
    A["run_validator() receives output"] --> B{"Any FINDING or REFUSED line?"}
    B -->|Yes| C["Quote first FINDING or REFUSED"]
    B -->|No| D{"Any WARNING line?"}
    D -->|Yes| E["Quote first WARNING"]
    D -->|No| F["Quote last non-empty line"]
Loading

File-Level Changes

Change Details Files
Defines validator verdicts from emitted output rather than exit codes alone, classifying all other non-zero exits as crashes.
  • Replaces traceback-dependent crash detection with FINDING/REFUSED line detection.
  • Handles signal termination, silent exits, syntax errors, missing files, and argparse failures with crash details and signal-aware messages.
  • Preserves verdicts when a finding or refusal was printed before a later crash.
  • Updates validator row handling and crash explanations.
shape-doctor.py
tests/test_doctor_validator_verdicts.py
Propagates naming-script crashes as environment failures and prevents misleading cannot-answer refusals.
  • Routes both naming checks through crash detection and marks failures with crashed and environment details.
  • Reports CANNOT ANSWER for a naming-script crash at a shape root while preserving NOT A SHAPE ROOT precedence elsewhere.
  • Prints the cannot-answer refusal only under a CANNOT ANSWER verdict.
  • Adds regression coverage for raised scripts and non-UTF-8 names.
shape-doctor.py
tests/test_doctor_validator_verdicts.py
Retains informational output from successful validators as structured note rows.
  • Adds unified parsing for skip, WARNING, and note blocks, preserving multiline content and print order.
  • Treats note as an item boundary so adjacent notes and skips do not absorb one another.
  • Stores each category in its corresponding detail field and formats grouped pass explanations without changing verdicts.
  • Updates pin-source collision expectations from ok to note.
shape-doctor.py
tests/test_doctor_validator_verdicts.py
tests/test_pin_source_env_collisions.py
Makes failing validator rows quote an actual verdict before earlier warnings.
  • Prioritizes the first FINDING or REFUSED, then the first WARNING, then the last output line.
  • Ensures red manifest rows identify the finding that caused failure rather than a preceding warning.
  • Adds unit and integration coverage for quote selection.
shape-doctor.py
tests/test_doctor_validator_verdicts.py

Assessment against linked issues

Issue Objective Addressed Explanation
#410 Treat a non-zero exit from the checkout’s naming script as a crash when it printed neither FINDING nor REFUSED, rather than as a finding about the project. ✅
#410 For an assembly root or holder, mark a crashed naming script as an environment failure so the doctor returns CANNOT ANSWER (exit 3), not INVALID (naming). ✅
#410 For a directory that is not a shape root, keep the NOT A SHAPE ROOT verdict while ensuring a naming-script crash—such as a UnicodeEncodeError for a non-UTF-8 name—is not reported as a finding about the directory’s name. ✅
#447 Treat validators terminated by a signal as crashes rather than as reporting a finding or refusal; identify the signal in the crash reason, including for the standard's template. ❌ Signal-terminated validators are classified as crashes, and template crashes become CANNOT ANSWER. However, the signal name is included only when the child printed nothing. If it printed output before being killed, the reason quotes the last output line instead and does not identify the signal.
#447 Treat nonzero exits without a printed FINDING or REFUSED verdict as crashes, including exit 1 without output, syntax errors, and interpreter or argparse failures. ✅
#447 Apply the same crash detection to the naming rows so a crash in the standard's naming script is not reported as a project finding. ✅
#448 When a validator exits 0 after printing a WARNING or note, report that information in the doctor's output as a note without changing the overall verdict. ✅
#448 When a validator exits non-zero with both an earlier WARNING and a later FINDING, make the row quote the FINDING rather than the WARNING. ✅
#460 Make the doctor reliably report repository compliance and explain findings, refusals, and cases where it cannot answer. ✅
#460 Deliver the issue's v1.0.0 scope: core flows working end to end on Linux, macOS, and Windows, with the listed in-scope blockers resolved before release. ❌ This PR targets #410, #448, and #447 and explicitly says it lands only after the v1.0.0 tag. It does not resolve the listed v1.0.0 issues #237, #277, #291, or #424, so it does not fulfill the tracking issue's release objective.

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

… merge for ruling 27

This is a clean merge of main dd3f23a for Brett Heap's ruling 27, with no hand
resolution: git applied it on its own, and the merge commit's tree equals the tree
`git merge-tree --write-tree 430c13f dd3f23a` printed (03e9650).

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

Copy link
Copy Markdown

@sourcery-ai

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

@brettheap
brettheap merged commit a2243f1 into main Oct 15, 2026
5 checks passed
@brettheap
brettheap deleted the fix/doctor-validator-verdicts branch October 15, 2026 13:29
brettheap pushed a commit that referenced this pull request Oct 15, 2026
…ean merge for ruling 27

This is a clean merge of main a2243f1 (#557, on #560) with no hand
resolution: `git merge` auto-merged shape-doctor.py and
tests/test_doctor_legs_members_next.py and reported no conflict, and its tree,
e0e7f1e, equals what `git merge-tree
--write-tree 66a9353 origin/main` prints (T). T is also the tree the targeted
set passed on, committed in a scratch worktree before this merge existed:
997 passed, 3 skipped (the three PowerShell tests, Windows only).

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 15, 2026
…y-hand test and #581 among them) into #569, so the PR's CI runs on current main

Merges origin/main fb786c1, read from the
shared store without a fetch. Main moved from 7d9bec9 (#569's last merge) by
#556, #557, #558, #559, #560, #562, #567, #568, #579, #580 and #581. #579
(#550) landed tests/test_setup_by_hand_commands.py, which this PR's
`--no-next-steps` makes red; the next commit answers it. The merge itself is
clean, and its tree is the pm569 prediction's #581 step (53b6d0f7)'s, so the
resolution prepared against the predicted main carries over.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 15, 2026
… fix/540-pin-source-unread

This merges origin/main 2bf8fd4 as the shared store held it at
2026-10-15T16:50Z, thirteen landings past the 7d9bec9 this branch last
merged: #555 (shape-doctor's manifest kinds), #565 (scaffold's
`scaffold-remote-exists`), #581 (adopt's `check` of a kept pin path and
an unwritable work directory), #568, #559, #557, #560, #580, #556, #558,
#567, #579 and #562. It is done so that the PR's CI merge ref and this
round's rewording run on current main.

The merge was clean, with no conflict hunk. `adopt-project.py`,
`scaffold-project.py`, `scripts/shape_materialize.py`, `docs/cli.md` and
`tests/test_adopt_root_symlinks.py` took main's changes beside this PR's
by the textual merge, and `scripts/render-cli-reference.py --check`
passes on the result.

Main: 2bf8fd4.

Lane: openreposhape-2 (openRepoShape-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 16, 2026
…ests, 57 of 82 milestone issues closed), with #583, #596, #614, #619, #594, #617 and #618 in their areas, #596's Git Bash facts for its placeholders, release.yml's step 1 whole, and every pull request cited beside its issue (RV630 SHOULD 3, 5, 6, nits 7, 8)

- SHOULD 5: step 1 matches release.yml's header: "always on a
  `release/v<X.Y>` branch, which no push run tests", "A `release/v<X.Y>`
  branch serves while it is still at that commit", and that a tag's
  dispatch running at the tag's commit is GitHub's documented behaviour,
  not yet seen here.
- SHOULD 6: #598 beside #595, #592 beside #593, #460 on #557's line, and
  #589's line says "where that clone is still on the machine and is no
  partial clone" again. The counts and the open-issue list are re-derived
  from `git log --first-parent bfed143..origin/main` (49) and the
  milestone (25 open, 57 closed: #342, #392, #393, #421, #423, #426,
  #432, #433, #434, #443, #444, #454, #455 and #629 joined it). New lines
  under adopt-project.py for #583 (#520, #420), #619 (#530), #618 (#418),
  #614 (#527), #594 (#526) and #617 (#525); #596 (#585) under the one-line
  install, CI, native Windows, the docs and the known limits, where the
  two placeholders for it were.
- SHOULD 3: the known limit about "the installed doctor's temporary
  clone" is gone; the README, the handbook and AGENTS.md no longer say it.
- SHOULD 1: the filter-repo line says install.sh's order, apt and dnf
  included.
- nit 7: the release process's preconditions are said as the process
  this release follows; the workflow checks the environments, and nothing
  checks the rehearsal.
- nit 8: the known limits are wrapped.
- "the first on native Windows, where the one line cannot run" says
  PowerShell cannot run it, since Git Bash does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 16, 2026
….py: a project's own `neutral_product_pins:` or a leg's `naming.referent_chain:` that is not a list is `validate-manifest.py`'s finding for that field in every gate that reads it, so `5` no longer ends `make validate` in a `TypeError` and a scalar `openXdox` is no longer read as its characters (#443) (#631)

Four readers of a project's own declarations iterated whatever was there,
each as `for x in (value or [])`: the naming gate's `_pins_from_project`
and `_chain_from_leg`, `validate-manifest.py`'s `_declared_pins`, and
`validate-pins.py`'s `PinSources`. So `neutral_product_pins: 5` (or `true`,
or `1.5`) ended all three gates in `TypeError: 'int' object is not
iterable`, exit 1. `validate-manifest.py` died one line before its own
`manifest-neutral-product-pins` finding for that value could print. A leg's
`naming.referent_chain: 5` ended the naming gate the same way, though
`validate-manifest.py` already had the finding for it. `make validate` runs
the naming gate first, so its traceback was the first thing a person read.
Since #557 the doctor answered that crash `CANNOT ANSWER`, exit 3, which
outranked the manifest's own finding. A scalar `openXdox` was read as the
pins `X`, `d`, `e`, `n`, `o`, `p`, `x`: the naming gate classified and
exited 0, and `validate-pins.py` refused `neutral-pin-missing` for a pin on
`'o'`. A mapping was read as its keys in either field.

What changed. Each reader now takes a value that is not a list as declaring
nothing, as `validate-manifest.py`'s `_recorded_chain` already read a chain
that is not one: no pins, and no chain. A string is still a chain's one
link, under that function's own string rule. Each gate that reads the field
then says `validate-manifest.py`'s own sentence for it, with the same code:

    FINDING manifest-neutral-product-pins: neutral_product_pins is 5, expected a list of neutral product names
    FINDING naming-referent-chain: leg 'assembly': referent_chain is 5, expected a list of neutral-product names

In detail:

- The naming gate gets `_pins_field_findings`, said before any leg's finding
  in `_targets_from_project`, and `_chain_field_findings`, said first in
  `_leg_target`. That function names the leg by its declared `role:`, as
  `validate-manifest.py` names it. `_chain_from_leg` is now
  `_recorded_chain` rule for rule.
- `validate-manifest.py`'s `_declared_pins` returns the empty set for a
  value that is not a list. Its `_findings` is untouched, so its existing
  finding is now reached.
- `validate-pins.py`'s `_check_neutral_product_pins` asks before
  `PinSources` reads the value, records the finding, and checks no pin.

A finding is exit 1 in each gate, and `make validate` now stops at its first
command on the sentence. The doctor's three rows quote it, and its verdict
is `INVALID (naming, manifest, pins)` for the pins, or `INVALID (naming,
manifest)` for a chain, exit 1.

Decisions the issue left to the author:

- A finding, not a refusal. The issue offers either exit. A non-list field
  is a fault in the manifest a person can fix, the same kind of fault
  `validate-manifest.py` already reports with exit 1 and
  `validate-pins.py`'s `neutral-pin-entry-malformed` reports with exit 1.
- One sentence. The two existing sentences are reused, code and words, so
  every gate that reads the field says the same thing and the doctor quotes
  the same line in each row.
- Guards in each validator, not a new helper in `scripts/repo_shape.py`
  (the issue's option 4). That is the smallest change that makes every case
  right. A shared helper would put a fourth pinned file in the change and
  make three validators import a new name from it, so a project would have
  to take all four copies together, and #444's branch is changing that
  file's chain readers now. The words stay validate-manifest.py's own, and a
  test holds the naming gate's two sentences equal to that file's, so a
  change to one breaks the test.
- `0`, `false` and `''` are findings in every gate. `validate-manifest.py`
  already said they are not a list. The naming gate and `validate-pins.py`
  read them as an empty list (`or []`) and exited 0. Now all three agree:
  absent or null declares nothing, and anything else that is not a list is
  the finding. #338's fix ruled the same for a link's manifest. For a chain,
  `''` is a string, so it stays an empty record with no finding, as
  `_naming_chain_field_finding` has it.

Entries inside a list that are not names (`[5]`, `[[a]]`) were not examined
by #443 and are untouched.

Who this reaches. All three files are pinned copies in every scaffolded
root's `contracts/shape-pin.yaml`. Each has one copy in this repository and
no test records its digest or blob, so nothing else moves with them. A
project takes them through `update-shape.py apply`, as three
`upstream-changed` rows. The doctor's `naming` row runs the standard's copy,
so it says the sentence at once. Its `manifest` and `pins` rows run the
project's own copies, so over a project that has not applied this they
still say the copy "crashed before reaching a verdict" with the
`TypeError`. Even so, the verdict is `INVALID`, exit 1, and no longer
`CANNOT ANSWER`, because the crash that marked this checkout as unable to
ask was the naming gate's.

Tests. New file tests/test_non_list_project_fields.py, 71 cases, on
tests/test_manifest_validator.py's harness (`edit` and the scaffolded
assembly leg's `naming:` block, which it imports) and
tests/test_shape_doctor.py's session standard for the doctor. The cases:

- The pure readers over every value the issue measured. Nothing is read as
  characters or keys, and the naming gate's sentences equal
  `validate-manifest.py`'s.
- Each gate the project runs, from its root, over `5`, `true`, `1.5`,
  `openXdox`, `{a: b}`, `0`, `false` and `''` in the pins, and over the
  first six in a chain: exit 1, the sentence once, no traceback and no
  `neutral-pin-missing`.
- The controls measured on main: absent, null, `[openXdox]` in the pins;
  `[]`, null, `openXdox`, `''` and `[openXdox]` in a chain. Each keeps its
  exit in every gate and says neither sentence.
- A chain on the spec leg: both gates say `leg 'spec'`, as the sentence names
  the leg the chain is recorded on.
- The order: with `neutral_product_pins: openXdox` (a scalar) and a chain that
  begins at `openXdox`, the naming gate says the pins sentence before the
  broken-chain line it explains.
- `make validate` stops at the naming gate on the sentence.
- The doctor's rows and verdict for `5` in each field, and for `openXdox` in
  the pins.

Closes #443

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