/** * SeoWorkflowRunner — the end-to-end SEO workflow pipeline * (spec-20260715-ultimate-seo-suite, Sprint 11). Mirrors * `runStandaloneSecurityAudit` (`src/cli/commands/security-audit.ts:141-180`): * resolve playbook context -> select a data source -> gather data -> analyze * -> never-encode filter -> citation gate -> persist -> best-effort hub emit * -> exit code. * * Clock discipline: `now` is injected by the caller (`SeoCommand`) and NEVER * re-stamped here — this file never constructs a `Date` for wall-clock * purposes (the `.tmp` uniqueness suffix inside `SeoReportStore.save` is a * uniqueness token, not a report timestamp, and lives in a different file). * * Fail-closed: EVERY step that can throw (context resolution, source * selection, data gathering, analysis, persistence) is wrapped in a single * top-level try/catch -> `exitCode: 2` (arch line 395). A transport error * from `analyzer.analyze` (which does NOT catch `llm.chat` errors itself, * `analyzer.ts:16-18`) is caught here. A `parsed: false` analysis result is * ALSO fail-closed -> `exitCode: 2` with ZERO hub emits (sc-11-5) — checked * BEFORE the never-encode filter and citation gate run. * * Never-encode belt (spec-20260717-seo-improver-builder, Sprint 2; ADR-3): * `NeverEncodeFilter` runs between the `parsed` check and the citation gate, * dropping any LLM-synthesized banned tactic — even one carrying a * well-formed `citationUrl` that would otherwise pass the gate. Only its * `kept` findings ever reach `SeoCitationGate.apply`. * * Hub emission is best-effort and happens strictly AFTER the report has * been persisted; a hub failure never changes the exit code (mirrors * `emitFindingsToHub`, `security-audit.ts:196-239`). Only `gate.cited` * findings are ever passed to the emitter — an uncited finding is dropped * TWICE (the gate, then `SeoHubEmitter.mapToFindings`'s own belt-and- * suspenders check). */ import type { BoberConfig } from "../config/schema.js"; import type { SeoWorkflow, SeoReport } from "./types.js"; import type { AiVisibilityDeps } from "./ai-visibility-provider.js"; import type { SeoDataSource } from "./data-source.js"; import { SeoAnalyzer } from "./analyzer.js"; import type { SeoFindingSink } from "./hub-emitter.js"; import type { SeoVerifier } from "./verifier.js"; export type SeoRunInput = { projectRoot: string; /** May omit `seo` entirely — the explicit CLI call IS the opt-in. */ config: BoberConfig; workflow: SeoWorkflow; /** Defaults to `config.seo?.defaultTarget`, then `DEFAULT_TARGET_FALLBACK`. */ target?: string; /** ISO timestamp, stamped ONCE by `SeoCommand`. */ now: string; /** TEST injection — default = `selectSource(config, projectRoot)`. */ dataSource?: SeoDataSource; /** TEST injection — default binds `ingestFinding` to a real `FactStore`. */ findingSink?: SeoFindingSink; /** * TEST injection so `runner.test.ts` never builds a real LLM client * (the built `SeoAnalyzer` is LLM-only — `analyzer.ts:280-288` always * calls `llm.chat`). Default = a real `SeoAnalyzer` via `createClient`. */ analyzer?: SeoAnalyzer; /** * TEST injection for the opt-in adversarial verifier stage (Sprint 12). * Default = a real `SeoRecommendationVerifier`, constructed and invoked * ONLY when `config.seo?.verifier?.enabled === true` (byte-identical to * the no-verifier run otherwise — sc-12-2). */ verifier?: SeoVerifier; }; export type SeoRunOutcome = { report?: SeoReport; exitCode: 0 | 2; }; /** * Build the `SeoDataSource` for a run from the FOUR independent egress axes * (widened from two, spec-20260717-seo-improver-builder Sprint 9). ALL FOUR * axes off (default) -> `LocalExportSource` (zero egress, no credentials * touched, no governor/ledger constructed, `import('damcrawler')` never * evaluated — sc-9-2). This `return` is the FIRST statement after the * predicate, strictly BEFORE `SeoQuotaGovernor.load` (sprint briefing * Pattern B / Pitfall 2) — that ordering is what makes the all-off path * provably zero-construction. * * Otherwise, assemble a `CapabilitySeoRouter` per the deterministic * ADR-8/ADR-10 route table (sc-9-3): * - `url-inspection`: `GscAdapter` when `search-console` is on (GSC always * wins, ADR-8); else `CrawlSource` when `site-crawl` is on. * - `link-graph`: `CrawlSource` when `site-crawl` is on. * - `serp`: the config-selected `SerpProvider` (`resolveSerpProvider`, * ADR-10), wrapped in `SerpProviderSource`, whenever `serp-provider` OR * `site-crawl` is on (whichever axis the selected provider itself * requires; each provider re-asserts its own axis on call). * - `keywords`/`backlinks`: `DataForSeoAdapter` when `serp-provider` is on. * - `ai-visibility`: `AiVisibilityAdapter` wrapping the composed * `resolveAiVisibilityProvider(...)` result when `ai-visibility` is on * AND at least one configured engine is keyed (Sprint 3); the offline * `LocalExportSource` arm otherwise (no-key-safe fallback). */ export declare function selectSource(config: BoberConfig, projectRoot: string, deps?: AiVisibilityDeps): Promise; export declare class SeoWorkflowRunner { /** * Run one SEO workflow end-to-end. NEVER throws — every failure mode * (context/source/gather/analyze/persist errors, a `parsed: false` * analysis, or a blocked citation gate) resolves to `exitCode: 2`; * only a fully successful, non-blocked run resolves to `exitCode: 0`. */ run(input: SeoRunInput): Promise; } //# sourceMappingURL=runner.d.ts.map