ADR-024: over doctor aggregates existing diagnostics and repairs only its own corruption¶
Status: accepted
Context¶
#142's own "Status and diagnostics" section floats an over doctor as a
possible follow-up, with one explicit constraint: "it must not silently
rewrite user-owned Git configuration". #149 narrows that into a concrete,
backlog-scoped ask: a doctor/repair command specifically for malformed
.git/info/exclude blocks — over status (#148) already detects and
reports a malformed block (a stray, unmatched begin/end marker line for
over's own exclude:<overlay> marker), but never touches it; every write
path (reconcile/unreconcile, #146/#147) also leaves it strictly alone,
by design (see those issues' own acceptance criteria).
Two designs were on the table:
- Minimal:
doctordoes nothing but repair malformed exclude blocks — the literal #149 ask, no environment or config checks. - Aggregator:
doctoralso surfaces overlay configuration issues (over lint's domain) and environment/plumbing sanity (git onPATH, XDG state directory writable) alongside the exclude repair, in one report — closer to chezmoi'sdoctor, which is explicitly the shape a prior internal comparison against chezmoi/stow flagged as missing.
The minimal design is easy to reason about but produces a command whose
only job most users will never need (malformed blocks are rare — they
require external interference with .git/info/exclude, not normal over
usage). The aggregator design risks scope creep: reinventing diagnostic
taxonomies lint/status already own well.
Decision¶
over doctor aggregates rather than reinvents. Each source keeps owning
its own diagnostic shape and severity:
- Overlay configuration — delegates entirely to
lint::lint_repository; doctor never re-derives overlay validation. - Git-exclude drift — delegates to
git_exclude::diagnoseper overlay, reusingExcludeDiagnosis's ownDisplay. - Environment and XDG state are the two checks doctor actually
owns:
gitonPATHand the XDG state directory's writability (nothing else currently checked this), and stalesync.toml/virtual_checkout.tomlrecords (a target that no longer exists, or an overlay that no longer resolves) — both non-authoritative bookkeeping per their own module docs, so a stale record is only ever aWarning.
doctor::Finding stores each source's already rendered display line
rather than a new structured format — lint::Diagnostic and
ExcludeDiagnosis each already have an established, tested human-facing
shape; doctor's job is aggregation, severity-based exit-code counting, and
--verbose gating, not reformatting.
Repair stays exactly as narrow as #149 asked: --fix calls
git_exclude::repair, which touches a (repository, overlay) group's
.git/info/exclude block only when it is currently
BlockState::Malformed for that overlay's own marker. Every other status
(Ok/Missing/Modified/Orphaned) is left completely untouched — repair
fixes corruption of over's own marker lines, it never reconciles drift or
fills in a missing block (that stays reconcile's job, run by over
apply). This is how #142's "must not silently rewrite user-owned Git
configuration" constraint is honored: the only bytes --fix ever writes
are over's own stray marker lines and a freshly generated block using the
same compute_expectation the write path already uses — never a
user-authored line, and only on explicit --fix, never by default.
A malformed exclude block is promoted to Error severity (every other
exclude finding is a Warning): Missing/Modified/Orphaned all
self-heal on the very next over apply (reconcile recomputes the block
from scratch every run), but Malformed never does — it stays stuck until
someone runs over doctor --fix. That asymmetry is why plain over doctor
(no --fix) is the one case where exclude drift alone can fail the
process, matching over lint's existing "errors fail, warnings don't"
exit-code contract.
Consequences¶
doctor adds no new diagnostic vocabulary to learn — every finding traces
back to over lint or over status's own already-documented output,
except the two checks doctor owns outright (environment, XDG state), which
follow the same Ok/Warning/Error shape.
The XDG-state check is detection-only in this first version: no --fix
support for pruning a stale record. That's an intentional, low-risk
omission (these records are explicitly non-authoritative — losing one only
loses "last synced N days ago"-style bookkeeping) rather than a limitation
that blocks anything; it can be added later without revisiting this
decision.
Because doctor calls into lint/git_exclude rather than owning their
logic, any future change to either module's diagnostics automatically
flows into doctor's report — but it also means doctor inherits their
scope limits. It cannot detect anything lint/status themselves can't
(e.g. it does not walk Plan/DesiredTree reconciliation status the way
over status's own entries do — only the git-exclude drift layered on top
of that).