/** * Pre-flight blast-radius guard (change: add-preflight-blast-radius-guard). * * "Before I commit this diff, what does it actually touch?" The expensive * mistakes — changing a hub 58 callers depend on, orphaning a decision anchored * to a symbol you deleted, making a spec stale — are all knowable *before* the * edit, deterministically, from analyses OpenLore already computes. They just * are not surfaced at the moment they would prevent the mistake. * * This handler is PURE ORCHESTRATION: it composes existing deterministic * analyses — `analyze_impact` (callers / layers / hubs), `select_tests` (the * tests to run), and `check_spec_drift` (which already folds in anchored-memory * and ADR drift) over the diff returned by `getChangedFiles`. It adds no new * structural computation and runs no LLM (north star `c6d1ad07`). The result is * a single conclusion-shaped briefing — counts and named risks — never a graph. * * It is advisory by definition: the briefing informs, the agent acts. The * non-blocking git hook and opt-in blocking live in `cli/commands/blast-radius.ts`. */ import { type DiffGuardResult, type GeneratedFileFact } from '../generated-file-guard.js'; import { type PhantomReferenceFact, type PhantomReferenceResult } from '../phantom-references.js'; import { type InjectedMemory, type StaleTriggeredMemory } from './memory-triggers.js'; import { type ChangeGranularityReceipt } from '../symbol-changed-set.js'; import type { ConfidenceBoundary } from './confidence-boundary.js'; import { type ServedContentProvenance } from '../served-content.js'; export interface BlastRadiusInput { directory: string; /** Git ref to diff the working tree against. Default `HEAD` (uncommitted changes). */ baseRef?: string; /** Impact-analysis traversal depth (forwarded to analyze_impact). Default 2. */ depth?: number; /** Cap on the number of changed symbols analyzed for impact. Default 12. */ maxSymbols?: number; /** * Opt into cross-repo (federation) scope: also evaluate consumer repos that reach a * call site of a changed published symbol. Forwarded verbatim to the composed * `select_tests`. (change: add-multi-repo-federation) */ federation?: boolean; /** Restrict the federation scope to these registry repo names (default: all). */ federationRepos?: string[]; } type RiskLevel = 'low' | 'medium' | 'high' | 'critical'; /** Per-symbol slice of the briefing (the riskiest changed symbols). */ interface SymbolRisk { symbol: string; file: string; riskLevel: RiskLevel; affectedCallers: number; /** * Edges in this symbol's impact closure recovered by a synthesis rule rather than direct * resolution, by rule — a message or event hop counted in `affectedCallers` is not a call * (change: label-synthesized-hops). Absent when the closure is all directly resolved. */ synthesizedEdges?: Record; fanIn: number; isHub: boolean; } export interface BlastRadiusBriefing { /** The base ref the caller requested (default `HEAD`). */ baseRef: string; /** The ref git actually diffed against. Differs from `baseRef` when the * requested ref did not resolve and resolveBaseRef fell back (main → master → * HEAD~1); a caveat is emitted when they differ. */ resolvedBaseRef: string; changed: { files: number; /** Symbols that CHANGED. Symbols seeded for another reason are counted in `alsoSeeded`. */ symbols: number; symbolNames: string[]; /** Seeded because they name a changed symbol, or hold a dynamic-dispatch site — not changed. */ alsoSeeded?: number; }; /** * How precisely the changed symbols were identified (change: add-symbol-content-hashes): files * whose changed symbols are exact, and files kept whole with the reason. Absent when the diff * named no file with production symbols. */ changeGranularity?: ChangeGranularityReceipt; impact: { highestRiskLevel: RiskLevel | 'none'; maxAffectedCallers: number; hubsTouched: Array<{ symbol: string; fanIn: number; }>; layersCrossed: string[]; governingDecisions: string[]; /** Additive provenance companion for the legacy title array above. */ governingDecisionProvenance: Array<{ title: string; provenance: ServedContentProvenance; }>; topSymbols: SymbolRisk[]; analyzedSymbolCount: number; truncated?: { omitted: number; reason: string; }; }; tests: { count: number; toRun: Array<{ test: string; file: string; confidence: string; }>; soundness: unknown; /** Unmodified reachability-cap receipt from select_tests. */ truncatedAtDepth?: number; /** * Present when test selection THREW. `count: 0` alongside this means "not * computed", not "no tests are impacted" — the two are different claims and a * pre-commit reader acts differently on each. */ unavailable?: string; }; memory: { drifted: number; orphaned: number; willDrift: Array<{ kind: string; message: string; filePath: string; provenance: ServedContentProvenance; }>; /** Uncapped stable identities used by the enforcement ratchet, never the display-capped list. */ orphanFindings: Array<{ id: string; filePath: string; message: string; }>; }; specs: { willGoStale: number; items: Array<{ kind: string; message: string; domain: string | null; specPath: string | null; provenance: ServedContentProvenance; }>; }; decisions: { affected: number; /** Uncapped count of `adr-orphaned` issues. The hook's block gate reads this, * never `items` — `items` is display-capped and could omit a triggering issue. */ orphaned: number; items: Array<{ kind: string; message: string; domain: string | null; provenance: ServedContentProvenance; }>; /** Uncapped stable identities used by the enforcement ratchet, never the display-capped list. */ orphanFindings: Array<{ id: string; filePath: string; message: string; }>; }; /** Whether the composed drift pass was complete enough to initialize or shrink a baseline. */ driftAssessment: { complete: boolean; filesOmitted: number; detailsTruncated: boolean; unavailable?: string; }; /** * Cross-repo (federation) impact. `evaluated: false` carries a truthful note about * why nothing ran — never a claim that the shipped federation capability does not * exist. `evaluated: true` carries the forwarded `select_tests` federation block * (cross-repo tests + repos-consulted/-skipped coverage). (change: * fix-git-derived-signal-honesty; BriefingCapabilityClaimsAreCurrent) */ federation: { evaluated: false; note: string; } | ({ evaluated: true; } & Record); /** * Notes whose trigger matches this diff, pushed with their receipts; orphaned ones are listed * apart, never as content (change: add-memory-trigger-predicates). Absent when none fired. */ injectedMemories?: InjectedMemory[]; staleTriggeredMemories?: StaleTriggeredMemory[]; injectedMemoriesNote?: string; /** * Changed files that are generated or vendored, with the rule that matched, their literal source, * and regenerate command (change: add-generated-file-guard). Absent when none are. */ generated?: GeneratedFileFact[]; /** * References the diff adds that resolve to nothing — a missing module, a name the target does not * export, an undeclared or unpinned package (change: add-phantom-reference-findings). Present * whenever the check RAN, with `referencesChecked` so that `count: 0` reads as "checked, all * resolved" and not as "not checked"; absent only when the check could not run (then a caveat * says why). */ phantomReferences?: { count: number; unresolved: PhantomReferenceFact[]; notAssessed: number; filesChecked: number; referencesChecked: number; }; /** OpenAPI operations served by routes in the changed files (change: add-contract-graph-projection). */ contracts?: import('../contract/contract-briefing.js').ContractBriefing; /** * The inverse of `contracts`: the diff changed a SPECIFICATION, so this names the elements it * declares and the code bound to each (change: add-contract-graph-projection). */ changedContracts?: import('../contract/contract-briefing.js').ChangedContractBriefing; /** * Env vars the diff stops reading anywhere, each with the infrastructure sites that still set the * old name (change: add-iac-code-binding). Absent when the diff removes no env-var read. */ envDeclarations?: { removedReads: string[]; stillSet: Array<{ name: string; sites: Array<{ ecosystem: string; resource: string; file: string; line: number; }>; }>; }; /** Present only when the requested base did not resolve; names both refs (fix-cli-conclusion-honesty). */ baseRefFallback?: { requested: string; resolved: string; }; /** Index-staleness disclosure: a risk headline computed over a graph that predates the * working tree must say so (fix-cli-conclusion-honesty). Absent when the index is current. */ confidenceBoundary?: ConfidenceBoundary; headline: string; posture: 'advisory'; caveats: string[]; } /** * Compute the pre-flight blast-radius briefing for a diff. Read-only, * deterministic, offline. Exported for reuse by the CLI hook; the MCP dispatch * entry is {@link handleBlastRadius}. */ export declare function computeBlastRadius(input: BlastRadiusInput, /** Internal: receives the composed diff checks' results, so verify_change reuses them. */ hooks?: { guard?: (guard: DiffGuardResult) => void; references?: (refs: PhantomReferenceResult) => void; }): Promise; /** MCP dispatch entry. Returns the briefing object directly (additive-by-cast). */ export declare function handleBlastRadius(input: BlastRadiusInput): Promise; export {}; //# sourceMappingURL=blast-radius.d.ts.map