Skip to content

[RAPTOR-18075] feat(artifact): add dr artifact code doctor - #866

Open
ajalon1 wants to merge 14 commits into
datarobot-oss:mainfrom
ajalon1:aj/RAPTOR-18075-artifact-doctor
Open

[RAPTOR-18075] feat(artifact): add dr artifact code doctor#866
ajalon1 wants to merge 14 commits into
datarobot-oss:mainfrom
ajalon1:aj/RAPTOR-18075-artifact-doctor

Conversation

@ajalon1

@ajalon1 ajalon1 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

RATIONALE

RAPTOR-18075: when a project's .datarobot/workload/ (legacy .wapi/) sync state breaks, users currently get cryptic failures from init/sync and no guided recovery path. dr artifact code doctor diagnoses the state, reports clear remedies, and offers safe repairs for the cases the CLI can fix locally.

ARCHITECTURE

The feature is split across three layers:

  • cmd/artifact/code/doctor: Cobra command wiring, soft auth probe, output selection, and repair/relink flow control.
  • internal/workload/doctor: workload sync-state checks, remedies, and safe repair/relink operations.
  • internal/doctor: reusable doctor framework with statuses, ordered runner, report aggregation, and text/JSON reporters.

The four remote checks share exactly one GetArtifact snapshot per run through the injected ArtifactGetter seam. SKIP cascades are explicit: missing state skips everything, invalid config skips config-dependent checks, and non-404 remote failures skip remote checks with a connectivity remedy instead of pretending the artifact was deleted. --fix and --relink run through a global held-lock safety gate before making any local changes.

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, stale lock"]
        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
Loading

Adding a new check is intentionally small: implement doctor.Check, add a canonical remedy, register it in the ordered local/remote check list, and test it through the fake seams. Both reporters render whatever the ordered 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 LocalChecks/RemoteChecks"]
    D["4. place it deliberately; order is pinned and user-visible"]
    E["5. test with temp sync state + fake ArtifactGetter"]
    F["text and JSON reporters render it automatically"]

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
Loading

Full design and check-authoring guide: docs/development/doctor.md.

CHANGES

  • Reusable framework: adds internal/doctor with OK/WARN/FAIL/SKIP statuses, ordered runner, report aggregation, text table output, and pure JSON output.
  • Doctor checks: adds internal/workload/doctor with six local checks (wapi.presence, wapi.config, wapi.manifest, wapi.config-manifest-divergence, wapi.rollback, wapi.lock) and four remote checks (remote.artifact-exists, remote.artifact-locked, remote.catalog-mismatch, remote.drift).
  • Command surface: adds dr artifact code doctor under the existing workload feature gate with --dir, --output-format json, --fix, --relink, and --yes. Read-only doctor runs never write local files or server state.
  • Safe repairs: --fix can rebuild a missing manifest, restore an interrupted rollback, and clear a stale lock only when the lock is acquirable. All repairs are skipped while another process holds the sync lock.
  • Relink flow: --relink <artifact-id> repoints an existing local state to a reachable unlocked service artifact, refreshes BASE, appends relink history, leaves the working tree and server untouched, and defaults confirmation to No.
  • Init guidance: dr artifact code init now offers relink-oriented guidance when an existing state points at a missing or mismatched artifact instead of only telling the user to delete state manually.
  • Docs: updates the artifact command reference and adds the doctor architecture/check-authoring guide.

TESTING

  • Added unit and command coverage across internal/doctor, internal/workload/doctor, and cmd/artifact/code/doctor.
  • Verified JSON stdout purity, exit-code aggregation, SKIP cascades, repair summaries, held-lock behavior, relink gates, and init relink-offer paths.
  • Live staging validation used throwaway doctor-test-* fixtures for healthy, deleted-artifact, catalog mismatch, drift, offline, and relink-to-sync scenarios; fixtures were cleaned up.
  • Interactive confirmation paths were exercised through a virtual PTY, including accept, decline, and Ctrl-C.
  • Local/mission validation covered build, lint, unit tests, live staging checks, and user-testing flows.

Current CI note: the PR currently has one failing Windows test job. The failures are in doctor lock-check expectations on Windows, where wapi.lock returns SKIP ("lock not enforced on this platform") while several tests still expect OK. Linux tests, lint, build, CodeQL, and other checks are passing.

NOTES

  • A separate stacked PR will carry the informational checks that were intentionally kept out of RAPTOR-18075 scope.
  • --fix and --relink are mutually exclusive via Cobra's generic mutually-exclusive flag handling.
  • Remote checks use one fetched artifact snapshot per run to avoid inconsistent per-check observations.

RELATED


Note

Medium Risk
New CLI paths mutate local sync state (--fix, --relink) and change init already-linked behavior; impact is bounded by the workload feature gate and extensive tests, but mis-repair or relink could confuse sync baselines.

Overview
Adds dr artifact code doctor under the existing workload feature gate to diagnose .datarobot/workload/ sync state, print text or JSON reports, and exit 1 when any check FAILs.

The command wires a reusable internal/doctor runner/reporters to internal/workload/doctor checks (six local, four remote with one shared artifact fetch). Optional --fix applies safe local repairs (manifest rebuild, rollback restore, stale lock clear) behind a held sync.lock gate; --relink repoints local binding with confirm/--yes, history logging, and no working-tree or server writes. Diagnosis skips normal auth PreRun and uses a soft credential probe so remote checks SKIP when offline.

dr artifact code init when the project is already linked now branches on config health and remote artifact state: corrupt config → doctor --fix; deleted artifact or catalog mismatch → doctor --relink (interactive relink offer or non-interactive guidance); healthy → doctor only. Removes prior “delete state to re-init” messaging and adds pinned JSON abort shapes for scripting.

Docs and command trees are updated; broad unit/command tests cover registration, JSON purity, repair/relink gates, and init relink flows.

Reviewed by Cursor Bugbot for commit 211b745. Configure here.

@datarobot-pr-review-router

Copy link
Copy Markdown

🎫 Jira: RAPTOR-18075 — dr artifact code doctor: diagnose & repair .wapi state

@ajalon1 ajalon1 changed the title [RAPTOR-18075] feat(artifact): add dr artifact code doctor [WIP][RAPTOR-18075] feat(artifact): add dr artifact code doctor Aug 28, 2026
Comment thread internal/doctor/doctor.go Outdated
Comment on lines +20 to +21
// live in their own packages and plug into the Runner. This keeps the layer
// reusable for a future top-level "dr doctor".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example package checks I was thinking of:
dr plugin doctor
dr template doctor
dr dotenv doctor
dr auth doctor

So many doctors.

ajalon1 and others added 14 commits August 31, 2026 12:52
Add internal/doctor: a state-agnostic check-and-report framework that a
future top-level `dr doctor` can reuse. No wapi/sync/workload imports.

- Status enum (OK/WARN/FAIL/SKIP), Result{CheckID, Status, Summary,
  Remedy, Details, Fixable}, Check interface (ID, Name, Run(ctx)).
- Runner executes checks in caller order, stamps CheckIDs, and Report
  derives counts, the lowercase ok|warn|fail verdict, and the exit code
  (1 iff any FAIL).
- Text reporter: header (project dir + artifact or "not linked"),
  CHECK/STATUS/DETAIL lipgloss table with tui.TableBorderStyle, remedies
  for non-OK rows, summary line with counts + verdict.
- JSON reporter: single pure-JSON object with the pinned schema —
  absolute projectDir, artifactId null when unlinked (empty string
  normalized), uppercase per-check status, checks in runner order,
  summary counts matching the tally, and an optional actions[] array
  (omitted entirely for read-only runs, present for repair runs).

TDD with testify; 19 tests pass under -race; task lint clean on
linux/darwin/windows.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…state

Add internal/workload/doctor: the six read-only local checks for
`dr artifact code doctor`, built on the generic internal/doctor framework
and the existing wapi/sync primitives. Checks perform zero local writes
and zero network calls; repairs remain behind --fix/--relink (later
milestone).

Checks (pinned fixed order):
- wapi.presence: state dir exists at current or legacy location
- wapi.config: LoadConfig; missing/corrupt/semantic FAIL carries the
  absolute path in details.path
- wapi.manifest: LoadManifest; same FAIL semantics, independent of config
- wapi.config-manifest-divergence: config lastSyncedVersionId vs manifest
  syncedVersionId incl. nil-ness and empty->nil normalization
- wapi.rollback: stale .rollback/ tree at current or legacy location,
  empty dir included
- wapi.lock: NON-CREATING probe (open without O_CREATE + non-blocking
  exclusive flock): absent -> OK, acquirable -> OK (released within Run),
  held -> FAIL, permission/IO open error -> WARN "cannot inspect" (never
  misreported as held), Windows -> SKIP via an injected goos seam

SKIP cascades: presence FAIL skips everything; config FAIL skips
divergence (remote checks will skip too once they exist) while
manifest/rollback/lock still run; manifest FAIL skips divergence only.

Canonical remedy strings live in remedies.go and are shared by both
reporters. Two small exported helpers added to existing packages:
wapi.ManifestPath and sync.LockFileName.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Register the doctor command under `artifact code` (inheriting the artifact
tree's DATAROBOT_CLI_FEATURE_WORKLOAD gate) and run the six local sync-state
checks through the generic framework Runner.

- Flags: --dir (default ".", resolved via filepath.Abs with final-component
  symlink resolution, never prompts), --output-format (text|json), and
  --yes/-y read from cobra with the DATAROBOT_CLI_NON_INTERACTIVE env var
  bound via viperx.BindEnv only.
- Soft auth probe inside RunE: remote credentials are resolved from the env
  pair or the stored drconfig profile without prompting, without calling
  auth.EnsureAuthenticatedE (no login wizard), and without ever writing
  drconfig.yaml. Local checks run regardless.
- Exit 1 iff any check FAILs, via cli.ErrSilent with runtime-set
  SilenceErrors so the rendered report is not followed by a cobra error
  echo; usage errors keep their explanatory message.
- Text and JSON reporters per the pinned output contract; read-only runs
  write nothing (verified: state byte-identical, sync.lock never created).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ive artifact

Add the four remote checks to internal/workload/doctor and wire them into
`dr artifact code doctor`, keeping the pinned 6-local-then-4-remote order:

- remote.artifact-exists: 404 -> FAIL "linked artifact not found (deleted?)"
  with the `doctor --relink` remedy; any other fetch failure -> SKIP.
- remote.artifact-locked: locked -> WARN (sync execute refused, preview
  allowed), fixable=false, never FAIL.
- remote.catalog-mismatch: config.CatalogID vs artifact codeRef.CatalogID,
  FAIL on mismatch (either side absent counts as divergent), both-absent OK.
- remote.drift: codeRef.CatalogVersionID vs config.LastSyncedVersionID,
  WARN on drift with a `sync --dry-run` remedy, no baseline -> OK.

All four share ONE artifact snapshot per run through a lazy remoteSnapshot
backed by an injected ArtifactGetter seam (production: workload.GetArtifact;
tests: fake store with call-count assertion), so a run performs exactly one
GetArtifact and a vanished artifact collapses the dependent checks to SKIP.
Remote error mapping is pinned: 404 -> FAIL-as-deleted on artifact-exists
only; ANY other failure (401/403, 5xx, timeout, unreachable, unauthenticated)
-> SKIP with a `dr auth login`/connectivity remedy that never mentions
--relink. Empty-string codeRef fields normalize to nil on both sides. The
checks stay pure diagnostics: zero local writes, zero server writes.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Implement `dr artifact code doctor --fix`: safe local-only auto-repairs
with a global safety gate.

Global safety gate: the sync lock is probed first (non-creating probe,
same logic as the wapi.lock check). When a live process holds the lock
(or it cannot be inspected), ALL repairs are skipped with reason "sync
in progress" — a sync writes manifest.json in its final phase, so no
repair is safe underneath it.

Three repairs, each reported as an actions[] entry
(performed|skipped-with-reason|not-needed) in both text and JSON:

1. Rebuild manifest from config — Manifest{Version:1, SyncedAt
   nil-iff-version-nil, SyncedVersionID: cfg.LastSyncedVersionID,
   Files:{}}. Requires a valid config; corrupt config skips with a
   re-init remedy.

2. Clear interrupted rollback via sync.RestoreStaleIfPresent — restores
   backed-up files to the working tree, removes .rollback/.

3. Clear lock only if acquirable — AcquireSyncLock then release; absent
   lock file reports not-needed (never created).

A repair failing mid-write is reported skipped with the error as
reason; remaining repairs still attempt. --fix re-runs the full check
suite and reports post-fix state; exit code reflects POST-fix state.
No-op on a healthy project with explicit "nothing to fix" output.
--fix never touches the server. --fix and --relink are mutually
exclusive (usage error, exit 1, no checks run).

New files:
- internal/workload/doctor/fix.go: RunFix repair suite + three repair ops
- internal/workload/doctor/fix_test.go: 20 unit tests covering all
  VAL-FIX scenarios (healthy no-op, missing/corrupt/divergent manifest,
  corrupt config, rollback restore, lock safety matrix, held lock gate,
  partial failure, working-tree preservation, windows gate, multiple
  problems in one run)
- cmd/artifact/code/doctor/fix_cmd_test.go: command-level tests for
  fix flows (healthy no-op, missing manifest, corrupt config, held
  lock, rollback restore, idempotent second run, mutual exclusion,
  deleted artifact + missing manifest composition)
- cmd/artifact/code/doctor/heldlock_{unix,windows}_test.go: platform-
  specific held-lock helpers for command-level tests

Modified files:
- cmd/artifact/code/doctor/cmd.go: --fix flag, mutual-exclusion check,
  fix-then-rerun wiring, actions in report
- internal/doctor/text.go: writeActions section in text reporter
- internal/workload/doctor/lock.go: extracted newLockCheckWithGoos for
  fix's gate probe reuse

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Implement `dr artifact code doctor --relink <new-artifact-id>`: repoints
the project at a new artifact with a fresh sync baseline reset, without
deleting the state directory.

Safety gates (every abort leaves state byte-identical):
- Lock held by live process → abort "sync in progress"
- Not-linked project → error pointing to init (short-circuits before fetch)
- API unreachable/unauthenticated → error abort (relink hard-requires API)
- Target 404 → abort
- Target locked → abort (cannot sync to a locked artifact)
- Target Artifact.Type != "service" → abort (cross-type lineage refused)
- Same-id relink → allowed, warned, BASE reset

Confirm prompt defaults to No (bespoke [y/N] where empty Enter declines;
NOT reader.AskYesNo). Non-interactive (--yes or non-TTY) prints the warning
to stderr and proceeds. Decline/Ctrl-C/EOF aborts with state untouched.

On confirm: config rewritten (artifactId=new, catalogId=new codeRef.CatalogID
normalized empty→nil, lastSyncedVersionId=null), manifest reset to empty
BASE, history.log appended {op:relink, from, to, ts}, working tree untouched,
zero server writes. Post-relink checks re-run and report; actions[] included.
--fix and --relink are mutually exclusive (cobra MarkFlagsMutuallyExclusive
plus belt-and-suspenders guard).

17 unit tests in internal/workload/doctor/relink_test.go cover all gates and
the happy path. 16 command-level tests in cmd/artifact/code/doctor/relink_cmd_test.go
cover the CLI surface (JSON purity, actions array, exit codes, flag shapes).

Manually verified end-to-end on staging: create A → init → delete A → doctor
FAILs with relink remedy → --relink B → doctor healthy (all 10 OK) → sync
targets B (acceptance criterion #1) → second sync no-op → clean up both
fixtures (sweep confirms 0 doctor-test-* remaining).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…-authoring guide

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Remove the "Delete %s to re-init." advice from all init branches. The
already-linked check now fetches the linked artifact and branches:

- Corrupt config (unreadable linked state): report unreadable, remedy
  names `doctor --fix`, never deletion. No fetch attempted.
- Gone (404) or catalog mismatch: interactive → offer to relink in place
  (prompt for new artifact id, then run the doctor --relink path incl.
  warn/confirm and safety gates); non-interactive → print guidance naming
  `doctor --relink <new-id>`.
- Healthy (or non-404 error): keep abort behavior, point to `doctor` for
  diagnosis. No delete advice anywhere.

JSON mode: the already-linked abort emits a single JSON object on stdout
{status:error, error:already-linked, artifactId:<id|null>, remedy:<guidance>}
with human text on stderr, exit 1. HTML escaping is disabled so remedy
strings with <new-artifact-id> survive verbatim (matching the doctor's
JSON reporter).

The interactive offer and confirm prompts are testable via package-level
seams (offerRelinkFn, makeRelinkConfirmFn, isInteractiveFn). The relink
reuses internal/workload/doctor.RunRelink with the same safety gates
(lock probe, 404/locked/type checks, warn+confirm default-No).

Fresh-init path is byte-identical (unchanged output, state files, history
entry). Legacy .wapi/-only projects follow the same branches (init already
calls EnsureMigrated first).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Three non-blocking issues from the M1 scrutiny validator:

1. internal/doctor/text.go: make error handling consistent across all
   fmt.Fprint* calls in WriteText and its helpers. The header Fprintf,
   writeRemedies, writeActions, and writeSummary now all check and
   propagate errors, matching the existing writeChecksTable posture.

2. internal/doctor/reporters_test.go: add a raw-bytes assertion (before
   json.Unmarshal) that <, >, and & survive verbatim in the marshaled
   output, pinning the SetEscapeHTML(false) behavior documented in
   json.go's doc comment. The previous post-Unmarshal assertion was
   escaping-invariant and proved nothing.

3. cmd/artifact/code/doctor/cmd.go resolveProjectDir: replace the
   basename-only symlink heuristic (filepath.Base(resolved) ==
   filepath.Base(abs)) with os.Lstat-based detection on the final
   component, so a symlink whose target directory shares the link's
   basename is still detected. Intermediate symlinks (e.g. macOS
   /tmp → /private/tmp) continue to stay as written.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ndings

Bundle 12 non-blocking code-quality findings from the M3 scrutiny and
user-testing rounds onto the ticket branch before the M4 stacked branch is
created.

Scrutiny findings (10):
1. doctor-fix fixLock benign TOCTOU: documented why the stat-then-acquire
   ordering is safe (flock is per-open-file-description; post-fix check
   suite reports honestly regardless).
2. fixLock bare 'not-needed' reason: now reports "verified acquirable
   (acquired and released); no holder detected" for the acquire+release probe.
3. doctor-relink RunRelink mid-write non-atomicity: documented the invariant
   in the relinkWrite doc comment (state untouched until first write; on
   mid-write failure run doctor --fix; each individual write is atomic via
   write-temp-then-rename, but the sequence is not transactional).
4. --relink '' (explicit empty value) is now a usage error (exit 1) instead
   of silently behaving as read-only. The repair phase gates on
   Flags().Changed so an empty value is rejected before any work begins.
5. RunRelink dereferences opts.Confirm without nil guard: added nil guard
   (nil Confirm = decline). Fixed gate-numbering comment drift in the doc
   comment (same-id relink is not a numbered gate; it's a post-gate note).
6. init runRelinkFromInit passes context.Background(): now threads
   cmd.Context() through to RunRelink so cobra cancellation propagates.
7. init corrupt-config branch: now wraps the underlying LoadConfig error
   with %w and includes the config path in both text and JSON-mode stderr
   (previously JSON-mode stderr omitted the path that text mode included).
8. init empty entry at new-artifact-ID prompt: documented that empty entry
   = decline is the intended default-No UX (consistent with dirprompt.Ask
   contract and the bespoke [y/N] confirm prompt).
9. isNotFound/isCatalogMismatch predicate duplication: exported
   IsNotFound and IsCatalogMismatch from internal/workload/doctor and
   updated cmd/artifact/code/init to use the shared implementations,
   removing the duplicated local copies.
10. reader.go:80 bare newline to stdout on read error: added a code comment
    noting the cosmetic JSON-purity edge (Ctrl-C at an interactive prompt
    in JSON mode can emit a stray newline on stdout; abort paths emit no
    JSON anyway). Behavior intentionally unchanged.

User-testing findings (2):
11. Corrupt-config wapi.manifest remedy: WONTFIX — the remedy string is
    contract-pinned as canonical per check ID (one exact string owned by
    internal/workload/doctor, reused in text and JSON). The wapi.manifest
    check always shows RemedyManifest regardless of config state; the
    --fix action's skip reason (which points to re-init) is a separate
    output in the actions array, not the check remedy.
12. Fresh-init TEXT mode 'Error: Command not found' on stderr: WONTFIX —
    pre-existing, not introduced by the init relink-offer change (the
    relink-offer commit only touched the already-linked path, not the
    fresh-init path). Out of mission scope.

Tests added/updated for behavior changes (items 4, 6, 7, 9):
- TestRunE_RelinkEmptyValue_UsageError: --relink '' exits 1 with usage error
- TestRunE_RelinkUnchanged_ReadOnlyRun: plain read-only run unaffected
- TestRunE_AlreadyLinked_CorruptConfig: error wraps LoadConfig + includes path
- TestRunE_AlreadyLinked_CorruptConfig_JSON: JSON stderr includes config path
- TestRunE_AlreadyLinked_RelinkAccept_PropagatesContext: context propagation
- TestIsNotFound: shared 404 predicate (nil, 404, 500, wrapped 404, plain)
- TestIsCatalogMismatch: shared mismatch predicate (anchor-on-local rule)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…y nits

Four minimal, scoped edits from the misc-cleanup scrutiny synthesis:

1. TestRunE_AlreadyLinked_RelinkAccept_PropagatesContext (init/cmd_test.go)
   was vacuous — its assertion held regardless of cmd.Context() vs
   context.Background(). Added a runRelinkFn package seam (matching the
   existing offerRelinkFn/makeRelinkConfirmFn pattern) and rewrote the
   test to capture the context via the seam and assert it equals
   cmd.Context() using a sentinel value — genuinely falsifiable now.

2. Pinned the fixLock probe-path reason string: added
   assert.Contains(..., "verified acquirable") on the lock action's
   Reason in TestRunFix_LockAcquirable_VerifiedNotNeeded.

3. RunRelink's nil-Confirm decline now reports reason 'no confirm
   function provided; relink declined as a safety default' instead of
   the misleading 'declined by user' (no prompt happened).

4. Fixed curly-quote typo in TestRunE_RelinkEmptyValue_UsageError doc
   comment (right double-quote → straight quote).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Add `dr artifact code doctor` to the user-facing command references that
enumerate `artifact code` subcommands (docs/commands/artifact.md and
docs/commands/README.md). The repo-root README stays high-level and does
not enumerate subcommands, so the update belongs in docs/commands/.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Strip mission validation criteria IDs (VAL-*) from doc comments across
  internal/doctor, internal/workload/doctor, and cmd/artifact/code; the IDs
  are meaningless outside the mission that produced them. Replaced with
  short plain-language behavior notes where the code doesn't speak for
  itself, and deleted outright where it does.
- Rename runDoctor to pageDoctor (docs/development/doctor.md diagram
  updated to match).
- Remove the hand-rolled --fix/--relink mutual-exclusion reimplementation
  from validateRepairFlags; cobra's MarkFlagsMutuallyExclusive error now
  stands alone. validateRepairFlags retains only the empty --relink value
  check, and its test asserts cobra's generic message.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@ajalon1
ajalon1 force-pushed the aj/RAPTOR-18075-artifact-doctor branch from e180006 to 211b745 Compare August 31, 2026 19:52
@ajalon1 ajalon1 changed the title [WIP][RAPTOR-18075] feat(artifact): add dr artifact code doctor [RAPTOR-18075] feat(artifact): add dr artifact code doctor Sep 3, 2026
@ajalon1
ajalon1 marked this pull request as ready for review September 3, 2026 19:38
@ajalon1
ajalon1 requested review from a team as code owners September 3, 2026 19:38
@ajalon1

ajalon1 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@BugBot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 211b745. Configure here.

renderAlreadyLinkedJSON(cmd.OutOrStdout(), nil, remedy)

fmt.Fprintf(stderr, "Project is already linked but the config at %s is unreadable: %v\n", configPath, err)
fmt.Fprintln(stderr, "Run 'dr artifact code doctor --fix' to repair the config.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Init advertises unusable config fix

Medium Severity

A corrupt config.json is steered to dr artifact code doctor --fix, which cannot rebuild config. RemedyConfig and fixManifest both send the user to re-init, and the config check is marked not Fixable, so following this remedy leaves the project stuck.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: Bugbot Rules for DataRobot CLI

Reviewed by Cursor Bugbot for commit 211b745. Configure here.

_, err := os.Stat(lockPath(t, dir))

assert.ErrorIs(t, err, fs.ErrNotExist, "probe must not create sync.lock")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows lock tests expect OK

Medium Severity

On Windows, wapi.lock returns SKIP ("lock not enforced on this platform"), but several tests still require OK and a 10-ok summary. The GOOS seam is unused here, so the Windows CI job fails on healthy-project and cascade cases.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: Bugbot Rules for DataRobot CLI

Reviewed by Cursor Bugbot for commit 211b745. Configure here.

// not transactional.
func relinkWrite(opts RelinkOptions, oldCfg wapi.Config, art *workload.Artifact) ([]core.Action, error) {
newCfg := wapi.Config{
ArtifactID: opts.NewArtifactID,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey AJ, chas's claude poking at your droid's config build. newCfg carries ArtifactID/CatalogID/LastSyncedVersionID/CreatedAt/CLIVersion but not LastBuiltVersionID, so relink quietly nils it. its own doc says a deploy inheriting an image needs it to know if the code moved.

intended for the fresh BASE? if so worth making it explicit like the // fresh BASE on LastSyncedVersionID. the explicit field list also means the next Config field added drops here the same silent way.

fmt.Fprintln(w, tui.ErrorStyle.Render(
fmt.Sprintf("Project is already linked but the config at %s is unreadable.", configPath),
))
fmt.Fprintln(w, tui.DimStyle.Render("Run 'dr artifact code doctor --fix' to repair the config."))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to cursor. printCorruptConfig sends the user to doctor --fix, but nothing actually repairs config.json: fixManifest skips a corrupt config telling them to re-init, configCheck is Fixable:false, and RemedyConfig is init itself. so --fix runs and lands them right back at re-init. point at init <artifact-id> directly?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants