/** * MCP handler: change_impact_certificate (change: add-change-impact-certificate). * * Third of three in SPEC-STORE-INTEGRATION.md. For a proposed change (the working * tree vs a base ref), emit ONE deterministic, conclusion-shaped impact * certificate: the change's blast radius (reused from `blast_radius`), the paths it * NEWLY OPENS into each declared covering surface, the specs it drifts, and the * tests to run. The certificate is anchored to the touched symbols via the existing * code-anchored freshness lease, so it decays: when the change grows or an anchored * symbol moves, a re-check reports it stale and it is never presented as current. * * The novel piece is newly-opened-path detection, computed DIFFERENTIALLY without * any full-repo rebuild and without the (still-unbuilt) incremental dependency * graph: a new call edge can only originate from a changed file, so we re-parse * ONLY the changed files at the base ref and at the working tree (the exact bounded * primitive `structural_diff` uses), take each changed caller's added/removed callee * names, resolve them to canonical node ids by unique-name match, and adjust the * canonical adjacency both ways: post = canonical + added − removed, pre = canonical * − added + removed. That normalization is correct regardless of index staleness, * because unchanged-file edges are invariant and the changed-file deltas are * authoritative. A node that can reach a surface in `post` but not in `pre` is * newly able to reach it — the path the change opened. * * Deterministic, no LLM (north star `c6d1ad07`). Read-only and conclusion-shaped: * the result is a briefing an owner acts on — named surfaces, named shortest paths, * counts — never a raw graph. It never throws for an infrastructure problem; every * problem degrades to a finding or a caveat, and the certificate is advisory. */ import { type BlastRadiusBriefing } from './blast-radius.js'; import type { SerializedCallGraph, FunctionNode } from '../../analyzer/call-graph.js'; import type { StructuralAnchor, CoveringSurfaceConfig, CoveringSurfaceSeverity, ImpactCertificateConfig } from '../../../types/index.js'; /** Stable finding/diagnostic codes — part of the agent-facing `--json` contract. */ export type ImpactCertificateCode = 'surface-unresolved-member' | 'surface-empty' | 'surface-newly-reached' | 'surface-critical' | 'spec-drift' | 'no-surfaces-declared' | 'unresolved-added-call' | 'certificate-stale'; export type ImpactCertificateSeverity = 'info' | 'warn' | 'error'; export interface ImpactCertificateFinding { code: ImpactCertificateCode; severity: ImpactCertificateSeverity; /** The surface/symbol/spec the finding concerns. */ subject: string; message: string; remediation: string; /** For surface findings: the surface's declared severity. */ surfaceSeverity?: CoveringSurfaceSeverity; } /** One path the change opens into a declared surface. */ export interface NewlyOpenedPath { surface: string; surfaceSeverity: CoveringSurfaceSeverity; /** The added edge that opened the path (caller → callee by name). */ openingEdge: { from: string; to: string; }; /** The shortest opening path, as named symbols `A → B → surfaceMember`. */ path: string[]; /** The surface member the path lands on. */ reaches: string; } export interface ResolvedSurfaceView { name: string; severity: CoveringSurfaceSeverity; /** Count of symbols the surface resolved to (the unit assessed). */ resolvedSymbols: number; /** Declared members that did not resolve to exactly one symbol. */ unresolvedMembers: string[]; } export interface ImpactCertificate { /** Schema marker so a persisted certificate is identifiable + versioned. */ kind: 'impact-certificate'; version: 1; /** The base ref the diff was computed against (post-fallback). */ baseRef: string; resolvedBaseRef: string; /** Present only when the requested base did not resolve and --allow-base-fallback accepted the fallback. */ baseRefFallback?: { requested: string; resolved: string; }; /** The change id, when assessed in a spec-store context; else 'working-tree'. */ change: string; changed: { files: number; symbols: number; }; /** The declared surfaces assessed against. */ surfaces: ResolvedSurfaceView[]; /** Paths the change opens into a declared surface (the differential core). */ newlyOpenedPaths: NewlyOpenedPath[]; /** Blast radius (callers/layers/hubs), reused verbatim from `blast_radius`. */ impact: BlastRadiusBriefing['impact'] | { unavailable: string; }; /** Tests to run, reused from `blast_radius`. */ tests: BlastRadiusBriefing['tests'] | { unavailable: string; }; /** Specs the change drifts, reused from `blast_radius`. */ specs: BlastRadiusBriefing['specs'] | { unavailable: string; }; /** The freshness lease: anchors to the touched symbols (drives decay). */ lease: { anchors: StructuralAnchor[]; }; findings: ImpactCertificateFinding[]; /** Highest surface severity with a newly-opened path (the block signal). */ highestSurfaceSeverity: CoveringSurfaceSeverity | 'none'; posture: 'advisory'; caveats: string[]; headline: string; } export interface ImpactCertificateInput { directory: string; /** Git ref to diff the working tree against. Default `HEAD`. */ baseRef?: string; /** Change id (spec-store context) — recorded on the certificate. Default working-tree. */ change?: string; /** Persist the certificate under `.openlore/impact-certificates/` for later decay re-checks. */ persist?: boolean; /** * Certification is fatal on an unresolvable base by default (fix-cli-conclusion-honesty). * Set this to accept the disclosed main → master → HEAD~1 fallback instead. */ allowBaseFallback?: boolean; } /** * Resolve declared covering surfaces to concrete symbol-id sets over the indexed * graph. A `symbol` member resolves only when it matches exactly one internal node * (no guessing); a `file` member contributes all internal nodes in that file. An * unresolved member degrades to a finding — it never throws (mcp-handlers contract). * * `postNodes` (the post-change snapshot's internal nodes) is merged in so a surface * member that was ADDED in this same diff — and so is absent from the canonical * graph — still resolves, instead of being silently missed (a false "no new reach"). */ export declare function resolveSurfaces(surfaces: readonly CoveringSurfaceConfig[], cg: SerializedCallGraph, postNodes?: readonly FunctionNode[]): { resolved: Array<{ name: string; severity: CoveringSurfaceSeverity; ids: Set; }>; views: ResolvedSurfaceView[]; findings: ImpactCertificateFinding[]; }; /** * The changed files for the diff vs `baseRef`, each carrying its git status and (for * a rename) the base-ref `oldPath`. Folds in UNTRACKED files — `git diff` excludes * them, but a brand-new file's functions are all genuine additions and may open a * path into a surface, so they must be assessed (mirrors `structural_diff`). The * differential and lease both consume this; missing either class is a silent * false-"no new reach", which is the exact mistake this tool exists to prevent. */ export declare function collectChangedFiles(rootPath: string, baseRef: string): Promise; interface EdgeDelta { /** Edges present after the change but not before, in canonical ids (caller → callee). */ added: Array<{ from: string; to: string; }>; /** Edges present before but not after. */ removed: Array<{ from: string; to: string; }>; /** Added call names that did not resolve to exactly one symbol (honest limit). */ unresolved: Array<{ caller: string; name: string; }>; /** Internal nodes of the post-change snapshot — lets a surface member that was * ADDED in this same diff (absent from the canonical graph) still resolve. * Optional: the pure detection core does not require it (it takes post nodes * as a separate argument); `computeEdgeDelta` always populates it. */ postNodes?: FunctionNode[]; } /** A changed file with its git status + (for renames) the path it lived at in the base ref. */ export interface ChangedFileEntry { path: string; status: 'added' | 'modified' | 'deleted' | 'renamed'; /** For a rename, the file's path at the base ref (where its old content lives). */ oldPath?: string; } /** * Compute the added/removed call edges introduced by the changed files, resolved * to canonical node ids. Re-parses ONLY the changed files (bounded). A callee name * resolves only when it maps to exactly one internal symbol in the post-change * universe (canonical ∪ new snapshot) — ambiguous names are reported, never guessed. * * The old snapshot is read from each file's BASE-REF path (`oldPath ?? path`), so a * renamed file's pre-existing calls pair correctly across versions instead of * looking like a flood of additions; a new/untracked file has no old content (every * edge is genuinely added) and a deleted file has no new content (every edge removed). */ export declare function computeEdgeDelta(absDir: string, resolvedBaseRef: string, changedFiles: readonly ChangedFileEntry[], cg: SerializedCallGraph): Promise; /** * Detect the paths the change opens into each declared surface, differentially. * Pure over the graph + delta — the testable core. A surface symbol that a node * can reach in `post` but not in `pre` is newly reachable; we attribute it to the * added edge that opened it and name the shortest opening path. */ export declare function detectNewlyOpenedPaths(cg: SerializedCallGraph, surfaces: ReadonlyArray<{ name: string; severity: CoveringSurfaceSeverity; ids: Set; }>, delta: EdgeDelta, postNodes?: readonly FunctionNode[]): NewlyOpenedPath[]; /** * Read declared covering surfaces from a repo's config, fully defensive against * wrong-typed JSON (config arrives via raw JSON.parse, no schema): * - a non-string / empty / whitespace `name` is dropped (an empty name would emit * blank-subject findings and a "into 1 surface(s): " headline); * - a `severity` that is not exactly info/warn/critical is coerced to `warn` — an * unrecognized value would otherwise flow into SEVERITY_RANK and make the block * signal `highestSurfaceSeverity` come out `undefined` (NaN index); * - duplicate names are collapsed to the first (later they key `reachedBySurface`, * so a duplicate would fold two surfaces' findings into one and drop a severity). */ export declare function surfacesFromConfig(cfg: ImpactCertificateConfig | undefined): CoveringSurfaceConfig[]; /** Persist a certificate under `.openlore/impact-certificates/` for later decay re-checks. */ export declare function persistCertificate(absDir: string, cert: ImpactCertificate): void; export interface CertificateLeaseStatus { change: string; status: 'fresh' | 'stale'; /** Anchors whose verdict is no longer `fresh` (drifted/orphaned), by symbol/file. */ movedAnchors: Array<{ subject: string; verdict: 'drifted' | 'orphaned'; }>; } /** * Re-check a certificate's freshness lease against the repo's CURRENT graph. The * certificate is `stale` when any anchored touched symbol moved/changed/died — * exactly the existing memory freshness verdict. An expired certificate must never * be treated as silently still-true (mcp-handlers: ImpactCertificateDecaysWithLease). */ export declare function recheckCertificate(absDir: string, cert: ImpactCertificate): CertificateLeaseStatus; /** A persisted certificate the health check found stale, for re-firing. */ export interface StaleCertificate { change: string; movedAnchors: CertificateLeaseStatus['movedAnchors']; } /** * Re-check every persisted certificate in a repo and return the stale ones. Cheap * gate: returns immediately when the repo has no certificates directory, so it adds * nothing for repos that never opted in. Used by the spec-store health check to * surface a stale certificate as a finding (the lease re-fires it). */ export declare function recheckPersistedCertificates(absDir: string): StaleCertificate[]; /** * Compute the change-impact certificate for a proposed change. Read-only, * deterministic, advisory. Exported for reuse by the CLI; the MCP dispatch entry is * {@link handleChangeImpactCertificate}. */ export declare function computeImpactCertificate(input: ImpactCertificateInput): Promise; /** MCP dispatch entry. Returns the certificate object directly (additive-by-cast). */ export declare function handleChangeImpactCertificate(input: ImpactCertificateInput): Promise; export {}; //# sourceMappingURL=impact-certificate.d.ts.map