/** * The `check` orchestrator (M10) - THE SEAM every entrypoint adapts over. * * One function wires the whole pipeline so no orchestration logic ever lives in * `cli/` or `mcp/` (hard rule #1): discover the brand (M4), resolve the buyer * prompts (M5), estimate + confirm the main spend, ask the engines (M6), score * the answers (M7), fold in the audit findings (M3), assemble the M8 envelope, * and write a snapshot. It renders nothing - callers render via M9. * * Everything that spends or touches the network is injected (fetcher, engine * adapters, judge, cost guard, fs, clock), so tests drive the entire flow with * mocks and no network (hard rule #3). The cost guard's two-phase budget is * honored end to end (discovery/query-gen on `setup`, asking/judging on `main`) * and the run's honesty - `costCapped`, `skippedEngines`, `degraded` - is * assembled from all three independent signals and surfaced upward, never * hidden (hard rule #6). */ import { CostGuard, type CostEstimate } from '../costs.js'; import type { ProfileFs } from '../discovery/index.js'; import { type AskMode, type EngineAdapter } from '../engines/index.js'; import type { Fetcher } from '../fetcher/index.js'; import type { QueryFs } from '../queries/index.js'; import { type SnapshotFs, type VisibilityEnvelope } from '../output/index.js'; import type { EngineId, JudgeClient, RunSpend } from '../types.js'; /** * Progress events emitted through a run's phases. Structured data only - the * orchestrator never renders (hard rule #1); a caller (the CLI) turns these * into a spinner and per-prompt lines. */ export type ProgressEvent = { kind: 'discovery-start'; } | { kind: 'discovery-done'; brand: string; } | { kind: 'queries-start'; } /** * `note` is whatever the pack's resolution needs to say out loud: why * generation produced nothing (judge error, setup cap, no judge configured), * OR that a reused pack was truncated by `--quick`. The second case ships a * FULL pack and is routine, so a truthy `note` never implies zero prompts - * read `prompts.length` for that, and render the note as information rather * than as an error. * * NO current consumer renders it, and that is deliberate rather than an * oversight: both of today's progress sinks are attached to a surface that * prints the same reason again moments later - the CLI's abort block * (`cli/progress.ts` says so at its `queries-done` case) and the MCP tool's * error text, which carries `result.notes`. Kept on the event because it is * the only channel a progress consumer has for WHY a phase came back empty, * and a future MCP progress renderer (one without an abort block after it) * needs it. Do not delete it as dead weight; it is a published field of a * structured event, not an internal. */ | { kind: 'queries-done'; prompts: string[]; note?: string; } | { kind: 'ask-start'; total: number; } | { kind: 'ask-answered'; done: number; total: number; } | { kind: 'ask-done'; answered: number; total: number; } | { kind: 'scoring-start'; } | { kind: 'scoring-done'; }; /** Context passed to the confirmation gate before the main ASK spend. */ export interface ConfirmContext { /** Pre-run cost estimate, or undefined if it could not be priced. */ estimate?: CostEstimate; /** Number of buyer prompts that will be asked to each engine. */ nPrompts: number; /** Engines that will be asked (available ones). */ engines: EngineId[]; /** * Products this run covers, for a `shopping` run. Absent on a brand check - * the gate then describes prompts and engines only, as it always has. */ nProducts?: number; } /** Injected collaborators. Anything that spends or hits the network lives here. */ export interface RunCheckDeps { /** Fetcher (M2); defaults to a real-network fetcher. */ fetcher?: Fetcher; /** Engine adapters (M6); their availability drives `skippedEngines`. Defaults from env. */ adapters?: EngineAdapter[]; /** Judge for discovery/query-gen/scoring pass 2. */ judge?: JudgeClient; /** Two-phase cost guard; defaults to uncapped. */ guard?: CostGuard; /** Profile persistence (M4). */ profileFs?: ProfileFs; /** Query pack persistence (M5). */ queryFs?: QueryFs; /** Snapshot persistence (M8). */ snapshotFs?: SnapshotFs; /** Clock, injected for deterministic `generatedAt`. */ now?: () => string; /** * Confirmation gate before the main spend. Return false to abort without * asking any engine. Skipped entirely when `opts.yes` is set (hard rule #8). */ confirm?: (ctx: ConfirmContext) => Promise; /** Env for the default adapter set, when `adapters` is not injected. */ env?: Record; /** Progress sink for interactive rendering; defaults to a no-op. */ onProgress?: (event: ProgressEvent) => void; } /** Options mirroring the `check` command's flags. */ export interface RunCheckOptions { /** State dir for profile/queries/snapshots (from `resolveStateDir`). */ stateDir: string; refresh?: boolean; brand?: string; category?: string; samplePages?: number; regenerate?: boolean; queriesFile?: string; count?: number; mode?: AskMode; concurrency?: number; judgeRateCap?: number; /** Skip the confirmation gate (agents / CI). */ yes?: boolean; /** Persist profile, queries, and the snapshot (default true). */ persist?: boolean; } /** * Why a run stopped before asking any engine. `declined` is the user's own * choice; `no-prompts` (generation produced nothing to ask) and `unconfirmed` * (no confirm handler and no `yes`) are failures to measure. */ export type AbortReason = 'declined' | 'no-prompts' | 'unconfirmed'; /** * Whether an abort was a FAILURE rather than the user's own choice. The single * source of truth for any consumer that has to tell the two apart, so none of * them hand-rolls its own subset of the taxonomy and drifts when a reason is * added (the M8 lesson that `isPartialRun` in `core/output` was extracted for). * * Today that is exactly one caller: the CLI's exit code (`cli/check.ts`), the * only surface a human can decline at. The MCP tool does NOT call this and is * right not to - it passes `yes: true` (hard rule #8), so `declined` is * unreachable there and every abort it can see is a failure it reports as an * error. A second interactive surface, or an MCP tool that ever grows a * confirmation, asks here rather than re-deriving the rule. * * Only `declined` is evidence that a human chose to stop; everything else * measured nothing. `undefined` therefore reads as a FAILURE, not as a decline: * an abort that carries no reason is an abort nobody explained, and calling it * a decline would claim a user consented to a stop they never saw. Every * `runCheck` abort path tags a reason today, so this case can only arise from a * path that forgot to - which is exactly when silence must not read as consent * (hard rule #6). * * Call it for a run whose `aborted` is true; a completed run has no abort to * classify. */ export declare function isAbortFailure(reason: AbortReason | undefined): boolean; /** Outcome of {@link runCheck}. */ export interface RunCheckResult { /** The check envelope; absent on every abort, whatever the reason. */ envelope?: VisibilityEnvelope; /** True when the run stopped before querying any engine. {@link abortReason} says why. */ aborted: boolean; /** * Why the run aborted, when it did. A caller that maps aborts to an exit code * or an error message asks {@link isAbortFailure} rather than comparing the * value itself - a decline is not a failure, everything else is. */ abortReason?: AbortReason; /** Snapshot path written, if persisted this run. */ snapshotPath?: string; /** Human-readable notes (competitor skip, query-gen skip, confirmation abort). */ notes: string[]; /** * What the run spent, from the cost guard. * * Present even on an ABORTED run: discovery and query generation happen * before the confirmation gate, so declining still billed for the setup * phase. Reporting only on success would tell a user who declined in order * to avoid spending that the run was free. */ spend?: RunSpend; } /** * Run the full `check` pipeline for `domain`. Never throws for a partial run - * a cost cap, a skipped engine, or a degraded profile come back on the envelope. */ export declare function runCheck(domain: string, deps: RunCheckDeps | undefined, opts: RunCheckOptions): Promise; /** * Best-effort cost estimate; undefined when no judge or an unpriced model. * * Exported so the `shopping` orchestrator quotes its confirm gate exactly the * way `check` does - a second copy would drift, and the gate is the only place * a user sees a number before money is spent. */ export declare function priceRun(nPrompts: number, availableEngines: EngineAdapter[], judge?: JudgeClient, grounded?: boolean): CostEstimate | undefined;