Skip to content

doctor --json is an unversioned struct dump: no schema, no version, and the state between healthy and broken has no name #469

Description

@MongLong0214

doctor --json dumps { checks, exitCode } verbatim: no schema identifier, no version, no overall status. A CI consumer cannot pin what it parses, and the state #458 needed a word for — usable, but something this repository relies on has stopped or cannot be verified — has no representation at all. A repository is either "no check failed" or "a check failed", and #458's repository was the first kind while capture had been dead for eight days.

Why this exists

Scope

In src/commands/doctor/report.ts and the command action; docs/cli.md gains the envelope documentation (ADR-0032 consequences).

export interface DoctorReport {
  schema: 'commitlore_doctor.v2';
  version: string;                                   // packageVersion()
  status: 'ok' | 'degraded' | 'failed';
  installSource: 'plugin' | 'npm' | 'npx' | 'source' | 'unknown';
  headline: string;
  summary: { total: number; ok: number; warn: number; fail: number; skipped: number; durationMs: number };
  fixPlan: string[];
  selection?: string[];                              // filters ticket; omitted here, never null
  checks: DoctorCheck[];                             // registry order
  exitCode: number;                                  // unchanged semantics
}
  • deriveStatus(checks) at one chokepoint, from non-optional checks only: any failfailed; else any warn or non-optional skippeddegraded; else ok (PRD §5.1). This function owns the §8.3 invariant the way the reference doctor owns its honesty clamp — one tested function, not a property left emergent on each check: no report may say ok while any non-optional check is non-ok, including rows the runner synthesized from a crash.
  • summary counts satisfy ok + warn + fail + skipped === total === checks.length; summary.durationMs === Σ checks[].durationMs (PRD §5.2, §10.2).
  • exitCode: 1 iff some non-optional check is fail, else 0 — SPEC §10's meanings, unchanged; degraded exits 0 (PRD §7.1: this preserves RELEASE-GATE §4's fresh-clone row and init's doctor --fix step, and keeps 3 from acquiring a doctor-private meaning).
  • installSource per PRD §5.1, detected without spawning anything: CLAUDE_PLUGIN_ROOT set or entry under the plugin root → plugin; entry under a global node_modules prefix → npm; an _npx cache segment → npx; entry beside a .git with this package's name → source; otherwise unknown. Until a surface has a test, its detection returns unknown rather than a guess. The resolved entry path is already in cli-runtime's evidence, so the classification is itself inspectable.
  • The v1 compatibility contract (PRD §5.3, ADR-0032 §6): every v1 key keeps its exact name, type, and meaning; new optional fields are omitted when absent, never null; removal or repurposing requires a new schema id and a superseding ADR. New keys are camelCase, matching the surface they extend.
  • src/commands/init.ts consumes report.checks[].needsAttention and formatReport — it keeps compiling and behaving unchanged (v2 is a superset).

Acceptance criteria

  • doctor --json | jq -r .schema prints commitlore_doctor.v2 (PRD §11 "schema pinned").
  • v1 superset regression test: a v1-shaped reader (checks[].{id, title, status, needsAttention, detail, fix, fixed}, exitCode, and report.checks.some(c => c.status === 'fail')) decodes a v2 report, with every v1 key present at the same type and — over the same fixture — the same values as the pre-envelope output. PRD §5.3: this test "must pass forever".
  • Serialized JSON contains no null-valued optional: no "blockedBy": null, "skipReason": null, "selection": null.
  • Status table exercised: all-ok → ok; one warn → degraded; one non-optional skip → degraded; one optional skip (synthetic registry entry) → ok; one fail → failed.
  • Crash-synthesized rows flow through the chokepoint: a throwing check yields status: "failed", never ok (PRD §8.3).
  • Exit codes: ok and degraded → 0; failed → 1 (PRD §11 "exit codes", first three columns).
  • Fresh-clone-shaped fixture (no notes refspec, nothing pushed): warns present, status: "degraded", exit 0 — the RELEASE-GATE §4 row unbroken.
  • Summary invariants asserted as a property over all fixtures (PRD §11 "summary invariant").
  • installSource has one test per detected surface; an undetectable surface yields unknown.
  • docs/cli.md documents the envelope and the additive rule.
  • Text output still byte-identical — the envelope is a --json-only change until the text ticket.

Tests that must exist

case what it catches
a v1 reader survives v2 (the superset regression) ADR falsification 3 exactly: a removed key, a renamed key, or null-instead-of-omitted — the three ways a pinned consumer breaks. This is the fails-if-wrong case
degraded exits 0 the reference doctor's 0/1/2/3 exit mapping being carried over despite the ADR rejecting it — degraded → 3 would give a SPEC §10 code a doctor-private meaning (ADR falsification 4)
a crashed check cannot leave status ok the #458 property restated: a subsystem verifiably stopped while the report says ok (ADR falsification 1) — through the crash path, where it is easiest to forget
summary counts and durationMs sum a summary computed independently of checks[] drifting from it
installSource: plugin / npm / npx / source / unknown (five synthesized entry-path fixtures) a heuristic guessing on an untested surface — PRD §5.1 requires unknown over a guess
init still reports needsAttention findings identically the one in-repo consumer of the report shape breaking silently

Depends on

Out of scope

  • Text rendering (next ticket), selection emission (filters ticket), snake_case keys (rejected, ADR-0032), any change to a v1 key, any network probe for installSource (PRD §5.1: "detected without spawning anything").

Traceability: PRD §5, §5.1, §7.1, §8.3, §10.2, §11; ADR-0032 §6, §7; SPEC §10; docs/RELEASE-GATE.md §4.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions