/** * Web search command. * * P2-05: the command receives an injected SearchCapability and shared * execution dependencies instead of constructing a Provider client. * The command owns query splitting, parallel scheduling, normalized * merge, rank, occurrence, truncation, projection, notices, and * presentations; the Adapter owns credentials, transport, and Provider * field mapping. Count is applied AFTER normalization by shared * execution and never enters an Adapter request or cache identity. */ import type { CommandContext, CommandResult, DataCommandResult } from "../command-invocation.js"; import type { SearchCapability, SearchTopic, SearchType } from "../capabilities/search.js"; import type { ResponseCache } from "../lib/cache.js"; import type { FusionMode } from "../lib/config-store.js"; import type { RetryPolicy } from "../lib/execution.js"; import { type LadderRule } from "../lib/output-budget.js"; import type { ProviderDescriptor, ProviderId } from "../providers/types.js"; import type { ConsumptionSink } from "../lib/consumption.js"; type RecencyFilter = "oneDay" | "oneWeek" | "oneMonth" | "oneYear" | "noLimit"; export interface SearchOptions { count?: number; domain?: string; recency?: RecencyFilter; contentSize?: "medium" | "high"; location?: "cn" | "us"; topic?: SearchTopic; type?: SearchType; maxSummary?: number; fields?: string[]; noCache?: boolean; merge?: boolean; } /** * Shared execution dependencies the search command consumes. The * Capability and cache/sleep/random are injected so tests run fully * offline; production wires the real Adapter from the selected Provider * and the default on-disk cache. */ export interface SearchExecutionDependencies { readonly capability: SearchCapability; readonly cache: ResponseCache; readonly sleep: (ms: number) => Promise; readonly random: () => number; readonly retryPolicy?: RetryPolicy; /** * Optional consumption sink (usage-ledger DESIGN D7 — PB-T2 parity * with the fan-out executor and the other billable handlers). When * present, every sub-query's `executeSearch` emits one consumption * event per billable invoke attempt through it. When absent (the * default), no event is emitted and behavior is byte-for-byte * identical to before. */ readonly consume?: ConsumptionSink; /** Timestamp source for consumption events; defaults to `Date.now`. */ readonly now?: () => number; /** * Fusion ranking mode (seed-24 T3). Resolved ONCE at the handler seam * via `resolveFusionMode` (env > config > default "rrf") and threaded * here so the command never parses a flag or a config file itself. * Omitted (tests and other direct callers) → "rrf", the standing * default; the merge seam still requires an explicit value, so this * default lives at the caller and never inside `mergeResults`. */ readonly fusionMode?: FusionMode; } export interface FormattedResult { rank: number; title: string; url: string; summary: string; source?: string; date?: string; /** Set when merging multiple queries: how many sub-queries surfaced this URL. */ occurrences?: number; /** * Provenance (fan-out only, DESIGN D3): distinct providers that * surfaced this URL, in first-encounter order. Omitted on the * single-provider path — SCHEMA.md. */ mergedFrom?: ProviderId[]; /** * Near-duplicate clustering (DESIGN D4, fan-out lane T5): on a cluster * representative, every OTHER member's emitted url, verbatim, in * first-encounter order. Absent on unclustered rows — the key is never * emitted at all rather than emitted empty. */ clusterUrls?: string[]; } /** The search Output Budget ladder (ordered; see ADR-0007 T3). */ export declare const SEARCH_LADDER: readonly [LadderRule, LadderRule, LadderRule]; /** * One arm of a merge grid: a Provider's results split into sub-queries. * `provider` is absent on the single-provider `--merge` path (no * provenance is tracked there); the fan-out path always sets it. */ export interface MergeGridArm { provider?: ProviderId; results: FormattedResult[][]; } export interface MergeResultsOptions { /** * Ranking algorithm. REQUIRED — the caller resolves it (production: * `deps.fusionMode`, defaulting to "rrf" at the handler seam), so the * seam cannot silently drift to a default inside the merge. A value * that is neither mode throws a plain Error (internal invariant, not * a user-facing ValidationError). */ mode: FusionMode; /** Emit mergedFrom provenance on every result (fan-out active). */ emitMergedFrom?: boolean; /** Post-merge --count cap. Each arm was already asked for this count. */ count?: number; } /** Jaccard similarity over two shingle sets: |∩| / |∪|. */ export declare function jaccard(a: Set, b: Set): number; /** * Merge results from an (arm × sub-query) grid (DESIGN D3, D2). Generalizes * the pre-fan-out sub-query merge with two new keys: dedupe by * `canonicalUrl(url)` (DESIGN D4) instead of the raw string, and a * caller-supplied ranking `mode`. * * First-writer-wins: the earlier arm's title/summary/url win a collision * (arm order is the tiebreak priority). Every URL accumulates * `mergedFrom` (distinct providers, first-encounter order) when * `emitMergedFrom` is set, and the merged list is sliced to `count` * post-merge. The single-provider `--merge` path and the fan-out path * share this one implementation. * * Ranking: * - `"occurrence"` (legacy): occurrence count desc → best position asc, * over a stable sort. Byte-identical to the pre-fusion merge. * - `"rrf"`: raw reciprocal-rank score desc (`Σ 1/(RRF_K + rank)` over * every grid occurrence) → occurrences desc → bestPos asc → * first-encounter (the stable sort's insertion order). The sort runs * on the RAW double, never on the rounded display value: two rows * that round to the same `fusionScore` still order by their true * scores. Under this mode every row emits `fusionScore` (exactly * three decimals, locale-independent); under "occurrence" the key is * absent entirely. * * `bestPos` is internal bookkeeping in both modes and never emitted. */ export declare function mergeResults(grid: MergeGridArm[], options: MergeResultsOptions): FormattedResult[]; /** * Output Budget T3: rebuild every text presentation from a budgeted * projection so -O compact/markdown/refs/tty reflect the shrunken * envelope (urls/titles/ranks stay visible by the ladder's never-cut * invariant). Exported for the handler seam (index.ts); a thin passthrough * over `buildPresentations` — no separate render logic to drift. */ export declare function rebuildBudgetedPresentations(budgetedResults: readonly unknown[]): NonNullable; export declare function search(query: string, options: SearchOptions | undefined, deps: SearchExecutionDependencies, context?: CommandContext): Promise; /** * Resolved activation plan. `mode` is the dispatcher hinge; `arms` is * the ordered Provider list to execute (already expanded and * filtered — never the `"all"` sentinel; the resolver owns the * expansion against its injected descriptors). `suppress` is the * human-readable reason the resolver ignored fan-out (e.g. "explicit * pin: fan-out ignored"); emitted on the single-pin path so the user * understands why fan-out did not engage. */ export interface FanoutPlan { readonly mode: "single" | "fanout"; readonly arms: ProviderId[]; readonly suppress?: string; } /** * Options for {@link resolveFanoutPlan}. The resolver is pure: every * input is an injected value, no I/O, no environment reads. `configFanout` * is the lone scalar read from the user config (Ticket 4 owns the * registry row); `routing` is the typed routing table already in * `HandlerDependencies`. `descriptors` is the live provider registry — * the resolver uses it for `isConfigured(env)` + `capabilities()` * filtering (the same gate `resolveEffectiveProvider` uses) so the * arm list the executor sees is the same list the executor would * otherwise walk, giving the dispatcher a single source of truth. */ export interface ResolveFanoutPlanOptions { /** Raw `--provider` value (or undefined). Empty string is treated as absent. */ readonly explicitProviderRaw: string | undefined; /** The resolved env the handler runs under (env + file-configured keys). */ readonly env: NodeJS.ProcessEnv; /** Whether `fanout` is enabled in the active config. */ readonly configFanout: boolean; /** Resolved per-capability routing table (routing-table plan). */ readonly routing?: Readonly>; /** Live provider registry; the resolver filters by capability "search". */ readonly descriptors: readonly ProviderDescriptor[]; } /** * Resolve the activation plan (D1). Tiers in precedence order: * * 1. **Explicit raw** (D1.1): a comma list or `"all"` → fanout * (`"all"` expands to configured ∩ advertising in registry * order); a single id → single (with suppress when * `configFanout=true`). * 2. **SCOUTLINE_PROVIDER env** (D1.2): a pin → single (suppress * when `configFanout=true`). * 3. **`configFanout=true` with no pin** (D1.3): fanout; arms = * `routing.search` filtered/deduped (if set), else configured ∩ * advertising in registry order. * 4. **Default** (D1.4): single; arm = first configured ∩ * advertising in registry order (informational — the dispatcher * still consults `resolveEffectiveProvider` for quota ranking). * * The resolver exposes its decision only through the returned * `FanoutPlan`; the dispatcher hinges on `mode`. The truth table is * pinned by Ticket 3 tests. */ export declare function resolveFanoutPlan(options: ResolveFanoutPlanOptions): FanoutPlan; /** * Options for {@link executeFanoutPlan}. The executor owns the * per-arm Lifecycle (descriptor.create → adapter → executeSearch → * close) and the merged error envelope. `query` is the resolved raw * query; when `searchOptions.merge` is set the executor splits it on * unescaped `|` and runs EVERY sub-query on EVERY arm — DESIGN D3's * (arm × sub-query) merge grid. * * `dependencies` is a subset of {@link SearchExecutionDependencies}: * the per-arm capability is constructed internally by the executor * (one per arm), so the caller does not supply a single shared * capability. Only the shared cache/policy seams cross arms. */ export interface FanoutExecutionDependencies { readonly cache: ResponseCache; readonly sleep: (ms: number) => Promise; readonly random: () => number; readonly retryPolicy?: RetryPolicy; /** * Optional consumption sink (PB-T2 parity with the other billable * seams). When present, every arm's `executeSearch` emits one * consumption event per billable invoke attempt through it — fan-out * arms must not silently skip local quota accounting (review fix, * PR #36). */ readonly consume?: ConsumptionSink; /** Timestamp source for consumption events; defaults to `Date.now`. */ readonly now?: () => number; } export interface FanoutExecutionOptions { readonly descriptors: readonly ProviderDescriptor[]; readonly env: NodeJS.ProcessEnv; readonly query: string; readonly searchOptions: SearchOptions; /** * Fusion ranking mode for the (arm × sub-query) merge (seed-24 T3), * threaded from the handler's `deps.fusionMode`. Required: the fan-out * merge always names its ranking algorithm explicitly — there is no * defaulting inside the executor. */ readonly fusionMode: FusionMode; readonly dependencies: FanoutExecutionDependencies; readonly secrets?: string[]; } /** * Execute the fan-out plan: parallel per-arm search, settled wrapper, * per-arm notices, D5/D6 exit policy, combined summary notice. The * single-pin path is NOT routed through this executor (Tier 2 / Tier 4 * stay verbatim through `executeWithFallback`); the executor asserts * `mode === "fanout"` on entry so a misuse throws loudly. */ export declare function executeFanoutPlan(plan: FanoutPlan, options: FanoutExecutionOptions, context?: CommandContext): Promise; export declare const SEARCH_HELP: string; export {}; //# sourceMappingURL=search.d.ts.map