/** * Doctor command — Provider-aware diagnostics (P4-04, DESIGN.md §14). * * The command is presentation-only: it receives a report builder through * injected dependencies and wraps the resulting {@link DiagnosticsReport} * as base data with a computed exit code. Provider resolution, capability * probing, settled collection, and failure redaction live in * {@link buildDiagnosticsReport} so the command never imports a Provider * transport (ZaiMcpClient, monitor client, or environment credential * reads) directly. * * Exit semantics (DESIGN.md §14): * - Missing effective Provider credentials -> exit 1. * - Any configured probe error -> exit 1 (successful entries preserved). * - All configured probes succeed, or only tools-disabled skips -> exit 0. * * Under `--no-tools` the command returns after metadata + configured-state * evaluation and constructs no Adapter and no transport (FR-034). */ import type { CommandResult } from "../command-invocation.js"; import type { DiagnosticsReport, ProviderDiagnostic, ProviderVerificationSummary } from "../capabilities/diagnostics.js"; import type { ProviderDescriptor, ProviderId } from "../providers/types.js"; import type { VerificationPromotionStore } from "../lib/config-store.js"; import { type ProviderAvailability } from "../lib/availability.js"; import { type QuotaState } from "../lib/quota-store.js"; export interface DoctorDiagnosticsDependencies { readonly noTools: boolean; readonly healthProbe?: boolean; readonly effectiveProvider: ProviderId; readonly descriptors: readonly ProviderDescriptor[]; readonly env: NodeJS.ProcessEnv; /** * Effective (post-validation) per-capability routing table from * config.json; absent when unconfigured. Embedded additively into * the report. */ readonly routing?: Readonly>; readonly sleep: (ms: number) => Promise; readonly random: () => number; /** * Pre-formatted one-line cache summary (Cache Module Unification * Ticket 03). The dispatcher formats this from `cacheStats()` output * before invoking the report builder; the report builder only embeds * it. Optional for backward compatibility with existing tests that * don't cover the cache surface. When omitted, the returned report * simply leaves out the `cache` field. */ readonly cacheSummary?: string; /** * Optional verification promoter (T3b — review item 10). When * supplied, Doctor flips `verification.status: unverified → verified` * for each Provider whose probe SUCCEEDS. The promotion is * best-effort: a write failure is reported through * {@link DoctorDiagnosticsDependencies.onPromotionError} (when * supplied) and never turns a successful probe into a Doctor failure. * Skipped, failed, no-tools, and network-deferred records are NOT * promoted (the probe result is authoritative). When omitted, Doctor * runs without promotion (backward-compatible with existing tests). */ readonly verificationPromoter?: VerificationPromotionStore; /** * Optional clock used for the promoted `checkedAt` timestamp. * Defaults to `Date.now`; tests inject a fixed clock for * deterministic timestamps. */ readonly now?: () => number; /** * Optional sink for promotion-write failures. Doctor isolates write * failures (a successful probe never becomes a Doctor failure on the * back of a write error); when this sink is supplied, the failure is * reported there so the dispatcher can surface it as a stderr notice * without affecting the exit code. When omitted, write failures are * silently swallowed. */ readonly onPromotionError?: (providerId: ProviderId, error: unknown) => void; /** * Optional quota snapshot for the per-Provider `quota` summary * (PB-T5 — Plan B). When supplied, each Provider entry embeds a * `{ source, observedAt?, authoritative }` summary derived from its * snapshot entry. When omitted, the `quota` field is omitted on * every entry (backward-compatible with pre-PB-T5 callers). Doctor * NEVER live-probes quota — it only reads the snapshot; the live * probe belongs to the `quota` command. Under `--no-tools` the * field still appears when a snapshot is available (a snapshot * read is a local state read, not transport). */ readonly quotaSnapshot?: QuotaState; /** * Optional verification records (Plan A — surfaced in Doctor by * PB-T5). When supplied, each Provider entry embeds a * `verification` summary mirroring the Provider's * `config.providers[id].verification` record. The dispatcher maps * the config-store `ProviderVerification` shape to the capability * contract's `ProviderVerificationSummary` (structural twin — kept * separate so the capability contract does not import * `lib/config-store.ts`). When omitted, the field is omitted * (backward-compatible). */ readonly verificationRecords?: Partial>; /** * Optional staleness threshold for the per-Provider `quota` * summary's `authoritative` flag. Defaults to * {@link DEFAULT_QUOTA_STALE_THRESHOLD_MS} (10 min — Tavily's * 10/10min key limit is the floor). Only consulted when * `quotaSnapshot` is supplied. */ readonly thresholdMs?: number; /** * When `true` (`--available`), the report's `providers` array is * filtered to rows whose `availability` is `"ok"`. The * `availableProviders` short list is computed BEFORE the filter and * is unchanged by it, so the full diagnostic picture (who is * exhausted, who errored) survives via the classification summary * even in filtered mode. Availability classification itself always * runs on every row, filtered or not. */ readonly availableOnly?: boolean; } export interface ProviderHealthCheck { readonly healthy: boolean; readonly latencyMs: number; readonly status: "ok" | "error" | "auth_error"; readonly error?: string; } /** * A {@link ProviderDiagnostic} row carrying the additive `availability` * classification (#94). Additive under DiagnosticsReport schema * version 2 (the documented PB-T5 additive-without-bump policy): the * field is always present on rows built by * {@link buildDiagnosticsReport}; older consumers that read the plain * {@link ProviderDiagnostic} shape ignore it. */ export interface ProviderDiagnosticWithAvailability extends ProviderDiagnostic { readonly availability: ProviderAvailability; readonly health?: ProviderHealthCheck; } /** * The {@link DiagnosticsReport} shape built by * {@link buildDiagnosticsReport}: every provider row carries * `availability` and the report carries `availableProviders` — the ids * whose row availability is `"ok"`, in registry order (#94). Additive * under schema version 2; older consumers reading the plain * {@link DiagnosticsReport} shape are unaffected. */ export interface DiagnosticsReportWithAvailability extends DiagnosticsReport { readonly providers: readonly ProviderDiagnosticWithAvailability[]; readonly availableProviders: readonly ProviderId[]; } /** * Build a schema-version-2 {@link DiagnosticsReport}. Inventory * (`capabilityMatrix`) is derived purely from `deps.descriptors` — no * descriptor.create(), no transport, no production registry import. * Under `--no-tools` the command returns after metadata + * configured-state evaluation. Otherwise each configured Provider is * probed through shared execution with settled collection, preserving * registry order and normalized redacted failures. * * PB-T5: when `deps.quotaSnapshot` is supplied, each entry carries a * `quota` summary derived from the snapshot (source/freshness). When * `deps.verificationRecords` is supplied, each entry carries a * `verification` summary mirroring Plan A's config record. Both fields * are omitted when their dependency is absent (backward-compatible). * Doctor never live-probes quota — it only reads the snapshot. The * `quota` field appears even under `--no-tools` (snapshot reads are * local state, not transport). * * #94: every entry carries an `availability` classification * (`ok` | `exhausted` | `error` | `unconfigured`), the rows are * ordered healthy-first (class rank, then registry order — stable), * and the report carries `availableProviders` (the ok rows in registry * order). `deps.availableOnly` (`--available`) filters the rows array * to the ok rows; `availableProviders` is unchanged by the filter. * Additive under schema version 2 — see * {@link DiagnosticsReportWithAvailability}. */ export declare function buildDiagnosticsReport(deps: DoctorDiagnosticsDependencies): Promise; /** * Compute the doctor exit code from a finalized report. Exit 1 when the * effective Provider is unconfigured or any configured probe errored; * otherwise exit 0. A tools-disabled or not-configured skip on a * non-effective Provider never fails the report. */ export declare function doctorExitCode(report: DiagnosticsReport): number; export interface DoctorOptions { noTools?: boolean; } /** * Injectable dependencies for the doctor command. `buildReport` resolves * the availability-carrying diagnostics report; the command only wraps * it for presentation and exit-code selection. */ export interface DoctorCommandDependencies { readonly buildReport: () => Promise; /** * Optional clock for the TTY presentation's snapshot-age labels * (T5 — #95). Defaults to `Date.now` — the same clock pattern the * report builder's `now` uses; tests inject a fixed clock for * deterministic age labels. */ readonly now?: () => number; } /** * Run the doctor command. Returns the diagnostics report as base data * with a computed exit code (1 when the effective Provider is * unconfigured or any configured probe failed; otherwise 0). T5 (#95): * the result also carries a TTY presentation override — * {@link formatDiagnosticsReport} rendered against the injected clock — * so terminal (`tty` output mode) runs get the human-friendly * healthy-first summary while the data payload itself stays * byte-identical (the tty string is presentation-only and never enters * `data`). */ export declare function doctor(deps: DoctorCommandDependencies): Promise>; export declare const DOCTOR_HELP: string; //# sourceMappingURL=doctor.d.ts.map