dr artifact code doctor — Architecture & Check-Authoring Guide
Audience: CLI contributors
Overview
dr artifact code doctor is a read-only diagnostic for a project's
.datarobot/workload/ sync state. It inspects the local state — linked
artifact, config.json/manifest.json health, config/manifest agreement,
interrupted rollbacks, and the sync lock. If credentials resolve, it also
checks the linked artifact's remote health.
At the end it reports each check as OK, WARN, FAIL, or SKIP with a
concrete remedy for any issues.
Key invariants:
- Read-only diagnosis. Checks perform zero local writes and zero server
writes. Only
--fixand--relinkwrite, and only to local state. - Exit-code contract.
0when no checkFAILs (OK/WARN/SKIPallowed);1when any checkFAILs;1on usage errors. - JSON purity. With
--output-format json, stdout is pure JSON; all warnings, prompts, and logs go to stderr. - Soft auth model. Auth is probed non-fatally inside
RunE(noEnsureAuthenticatedE, no login wizard, nodrconfig.yamlwrite). Local checks always run; remote checks reportSKIPwith a connectivity/dr auth loginremedy when the API is unreachable or unauthenticated.--relinkis the exception — it must fetch the target artifact, so it hard-errors when the API is out of reach.
--fix runs safe local auto-repairs (rebuild the manifest from config,
restore an interrupted rollback) behind a global safety gate that skips every
repair while a live process holds the sync lock and holds the lock itself for
the repairs, then re-runs the full check suite so the report and exit code
reflect the post-fix state. Windows has no sync lock (sync itself runs
unlocked there), so local.lock reports SKIP and --fix and --relink run
with nothing stopping a concurrent sync: the gate protects Linux and macOS
only. --relink <new-artifact-id> repoints the project at a different
artifact with a fresh sync baseline (empty BASE reset). --fix and --relink
are mutually exclusive.
Architecture
The feature is layered in three packages. The command layer wires Cobra
flags and the soft auth probe; the workload layer owns the wapi-specific
checks and repairs; the generic framework layer owns the ordered runner, the
reporters, and exit-code aggregation. The generic layer imports nothing
about workload state, so a future top-level dr doctor can reuse it.
flowchart TD
subgraph CMD["cmd/artifact/code/doctor (cobra wiring)"]
F["flags: --dir, --output-format, --yes, --fix, --relink"]
P["soft auth probe (non-fatal, no login wizard)"]
RUN["pageDoctor"]
end
subgraph WL["internal/workload/doctor (wapi checks + repairs)"]
L["6 local checks: presence, config, manifest, divergence, rollback, lock"]
R["4 remote checks: artifact-exists, artifact-locked, catalog-mismatch, drift"]
S["one GetArtifact snapshot (ArtifactGetter seam)"]
end
subgraph CORE["internal/doctor (generic framework)"]
RN["Runner (ordered execution)"]
REP["Report + exit-code: 1 if any FAIL"]
T["text reporter (lipgloss table)"]
J["JSON reporter (pure stdout)"]
end
API["DataRobot API (read-only GET)"]
F --> P
P --> RUN
RUN --> L
RUN --> R
R --> S
S --> API
L --> RN
R --> RN
RN --> REP
REP --> T
REP --> J
P -.->|"offline: remote checks SKIP"| R
L -.->|"presence FAIL: skip all"| SK1["remaining checks SKIP"]
L -.->|"config FAIL: skip divergence + remote"| SK2["divergence + remote SKIP"]
subgraph REPAIR["repair phase (side branch)"]
G["global held-lock safety gate"]
FIX["--fix: manifest, rollback"]
REL["--relink: repoint + fresh BASE"]
end
RUN -.-> G
G -.->|"live holder: skip all repairs"| SK3["all repairs skipped"]
G -.-> FIX
G -.-> REL
REL -.->|"hard-requires API"| API
The four remote checks share exactly one GetArtifact per run through the
injected ArtifactGetter seam (remoteSnapshot memoizes the fetch with
sync.Once), so a mid-run disappearance collapses to a single read. SKIP
cascades are honest per-run observations, not construction-time snapshots:
each check re-reads local state at Run time, so local.presence failing
makes every later check report SKIP ("no linked state"), and local.config
failing makes the divergence and all remote checks SKIP (no artifact id).
A 404 is owned solely by remote.artifact-exists (the only check allowed
to FAIL on one); the dependent remote checks SKIP rather than piling on.
Any non-404 remote failure maps to SKIP with the connectivity remedy —
never a misleading OK and never FAIL-as-deleted.
Adding your own check
The check suite is an ordered list of doctor.Check implementations built by
Checks/LocalChecks/RemoteChecks in internal/workload/doctor. A new
check is five small steps; both reporters pick it up automatically because
they render whatever the Runner returns.
flowchart TD
A["1. implement doctor.Check: ID, Name, Run(ctx) -> Result"]
B["2. add a canonical remedy constant in remedies.go"]
C["3. register the constructor in the check-list composition (LocalChecks/RemoteChecks)"]
D["4. place it at a deliberate position — order is pinned and user-visible"]
E["5. write tests with the fake seams: initProject temp state, ArtifactGetterFunc"]
F["both reporters render it automatically"]
A --> B
B --> C
C --> D
D --> E
E --> F
Notes for check authors:
Resultshape:{CheckID, Status, Summary, Remedy, Details, Fixable}.CheckIDis stamped by theRunnerfromCheck.ID(), so do not set it yourself.Detailsis an optionalmap[string]stringsurfaced in JSON (e.g.{"path": "/abs/file"}for corrupt-file checks); omit it when nil.- Remedies are canonical. Add exactly one remedy string per check
condition in
remedies.goand reuse it verbatim in both reporters — the command layer renders remedy strings as-is, never rewording them. - Check IDs and their order are a stable public contract. Scripts match
on the
idvalues in--output-format jsonfrom the first release, so renaming an ID (local.*,remote.*) or reordering checks is a breaking change. The six local checks run before the four remote checks, in the table order; a new check gets a new ID and a deliberate position, and existing ones keep theirs. - Pure diagnostics. A check
Runmust not mutate local state or make server writes. Repairs live behind--fix/--relinkininternal/workload/doctor(fix.go,relink.go); the command layer only wires the flags. - Reuse the SKIP cascades. Call
skipIfUnlinked(and thelinkedConfig/fetchedArtifacthelpers for remote checks) so a missing precondition reports an honestSKIPinstead of a misleadingFAIL. - Test seams. Use the existing
initProject-style temp state and theArtifactGetterFuncfake (no network). Inject aGOOSseam for any platform-specific behavior so it is unit-testable on any host.