doctor has no answer to "what do I fix first". Findings appear in check order, a root cause and its echoes carry equal weight, and the same fix string prints once per finding that shares it. The report's first line is whichever check happens to be first, not the thing that matters.
Why this exists
Scope
Pure, exported, tested functions in src/commands/doctor/report.ts. No output change in this ticket — the envelope ticket wires fixPlan/headline into --json, the text ticket renders them.
export const computeFixPlan = (checks: readonly DoctorCheck[]): string[];
export const deriveHeadline = (args: {
checks: readonly DoctorCheck[];
fixPlan: readonly string[];
status: 'ok' | 'degraded' | 'failed'; // computed by the caller; derivation lands with the envelope
}): string;
Per PRD §4:
- Membership: ids whose status is
fail or warn and whose blockedBy is unset. (Optionality does not filter the plan — §4.1 filters on status and blockedBy only; optional gates status and exit code, §1.4.)
- Order:
fail entries before warn entries; registry order within each tier; stable across runs.
- No cap — "A cap would hide findings, which is the one thing this command exists not to do." The wall-of-text control is collapse plus render-time dedup, and dedup is the renderer's job, not the plan's.
- Headline:
Next action [<id>]: <detail> — <fix> from fixPlan[0]. Empty plan: status === "ok" → healthy wording; status === "degraded" with no actionable entry → "usable; some checks could not be verified". Never healthy wording while status is non-ok.
Acceptance criteria
Tests that must exist
| case |
what it catches |
dead hook runtime: the plan contains hook-runtime and not commit-msg-hook |
the second half of PRD §11's "collapse" row — a plan that lists both (noise) or filters the root instead of the blocked entry (loses the cause) |
an independent defect stays in the plan — the dependent failing for its own reason, blockedBy unset, appears |
PRD §11 "independent failure survives collapse"; ADR falsification 2 — the plan burying a real finding because an unrelated dependency also failed. This is the fails-if-wrong case |
fail sorts before warn, registry order breaks ties — interleaved fixture |
severity-blind ordering, or an unstable sort that reorders equal-tier entries between runs |
twenty synthetic findings produce twenty entries |
a helpful cap sneaking in |
empty plan + degraded: headline says usable-not-verified and empty plan + ok: healthy wording are distinct strings |
PRD §4.4's exact warning: a merely-unverified run reading as a clean one — the #402 shape ("a ready that is not ready") in miniature |
headline of a plan whose first entry has fix: null renders without a dangling separator |
the crash-row case (fix: null from a contained throw) producing — null |
Open questions the PRD leaves — flagged, not decided here
- The exact healthy and usable-not-verified headline strings are unfixed — the PRD fixes their properties (distinct; never healthy while non-ok), not their words. Pick once, snapshot, and treat as contract.
Next action [<id>]: <detail> — <fix> with fix: null (a contained crash row is fail with no fix) is unspecified. Minimal reading: omit the — <fix> segment. State the choice in the PR.
Depends on
Out of scope
- Rendering and the
seenRemediations-style fix dedup (text ticket, PRD §4.3), the status derivation and envelope wiring (envelope ticket), any change to which checks warn or fail.
Traceability: PRD §4, §11; ADR-0032 §5.
doctor has no answer to "what do I fix first". Findings appear in check order, a root cause and its echoes carry equal weight, and the same fix string prints once per finding that shares it. The report's first line is whichever check happens to be first, not the thing that matters.
Why this exists
pending lsexists."Scope
Pure, exported, tested functions in
src/commands/doctor/report.ts. No output change in this ticket — the envelope ticket wiresfixPlan/headlineinto--json, the text ticket renders them.Per PRD §4:
failorwarnand whoseblockedByis unset. (Optionality does not filter the plan — §4.1 filters on status andblockedByonly;optionalgates status and exit code, §1.4.)failentries beforewarnentries; registry order within each tier; stable across runs.Next action [<id>]: <detail> — <fix>fromfixPlan[0]. Empty plan:status === "ok"→ healthy wording;status === "degraded"with no actionable entry → "usable; some checks could not be verified". Never healthy wording while status is non-ok.Acceptance criteria
computeFixPlanimplements membership and order exactly as above, is pure, and is deterministic across runs on identical input.deriveHeadlineimplements §4.4 including the never-healthy-while-non-ok property.--jsonbyte-identical, snapshots green.Tests that must exist
dead hook runtime: the plan contains hook-runtime and not commit-msg-hookan independent defect stays in the plan— the dependent failing for its own reason,blockedByunset, appearsfail sorts before warn, registry order breaks ties— interleaved fixturetwenty synthetic findings produce twenty entriesempty plan + degraded: headline says usable-not-verifiedandempty plan + ok: healthy wordingare distinct stringsreadythat is not ready") in miniatureheadline of a plan whose first entry has fix: null renders without a dangling separatorfix: nullfrom a contained throw) producing— nullOpen questions the PRD leaves — flagged, not decided here
Next action [<id>]: <detail> — <fix>withfix: null(a contained crash row isfailwith no fix) is unspecified. Minimal reading: omit the— <fix>segment. State the choice in the PR.Depends on
blockedBy— membership filters on it), doctor is one 1,159-line file: model, registry, runner, thirteen checks and renderer with no seam between them #467 (report.tsexists).Out of scope
seenRemediations-style fix dedup (text ticket, PRD §4.3), thestatusderivation and envelope wiring (envelope ticket), any change to which checks warn or fail.Traceability: PRD §4, §11; ADR-0032 §5.