/** * Deterministic spec link index. * * Replaces the LLM-pipeline-owned requirement mapping with a pure function of * (existing specs, current analysis graph). Every link comes from an anchor a * human or agent wrote explicitly in the spec — never from name similarity, * embeddings, or an LLM. That makes coverage an OBSERVATION rather than an * inference, and makes Repair usable without a prior probabilistic Generate run. * * Resolution is intentionally conservative (change `harden-spec-workflow-lifecycle`, * decision 671084e7): * * - exact anchor resolving to exactly one graph identity → `linked` * - anchor resolving to several exact identities → `ambiguous` * - anchor naming an exported TYPE (not behaviour) → `type-only` * - anchor whose identity is absent from the graph → `stale` * - absent identity in a file the analysis cannot vouch for → `not-assessed` * - requirement with no exact symbol anchor at all → `unmapped` * * A file-only anchor contributes to the domain FOOTPRINT and never to function * coverage: citing `src/auth/session.ts` does not make every function in that * file covered. * * This module is pure and does no I/O so it can be exercised directly by unit * fixtures; callers supply already-read spec content and the cached graph. */ import type { DependencyGraphResult } from '../analyzer/dependency-graph.js'; /** * Schema version of the persisted deterministic link index. * * BUMP THIS whenever the MEANING of the artifact changes — including a change to * how anchors are parsed or resolved, not only to the field layout. The cache's * provenance binds the analysis generation and the spec CONTENT, so a code change * that reinterprets unchanged specs is invisible to it; the version is the only * thing that invalidates such a cache. * * History: * 3 — deterministic anchors replace the LLM/semantic/heuristic mapping * 4 — legacy `> Implementation: \`name\` in \`file\`` hints are read too * 5 — a dotted `Class.method` token resolves as a member identity when the * graph holds it, instead of always being read as a file path * 6 — an anchor naming an exported type resolves to `type-only` instead of * `stale`: the type exists, it is merely outside what coverage measures * 7 — an absent identity in a file whose exports the analysis cannot vouch for is * `not-assessed` instead of `stale` (change: ground-generated-specs-in-the-graph) */ export declare const SPEC_LINK_INDEX_VERSION = 7; /** Default bound on disclosed candidates for one ambiguous or stale anchor. */ export declare const SPEC_LINK_MAX_CANDIDATES = 5; export type SpecLinkState = 'linked' | 'ambiguous' | 'unmapped' | 'stale' | 'not-assessed'; /** Why one anchor did not resolve to a single identity. */ export type SpecAnchorState = 'linked' | 'ambiguous' | 'stale' /** A file-only anchor: domain footprint, never function coverage. */ | 'footprint' /** * The anchored name EXISTS but is a type declaration, so it is outside what * coverage measures. Distinct from `stale`, which asserts the cited symbol is * gone — saying that about a type that is right there is simply false. */ | 'type-only' /** * The cited identity is absent from the export inventory, but the cited FILE is one whose exports * the analysis cannot vouch for — a language with no export extraction, or a file the analysis did * not cover. Absence there is not evidence the symbol is gone, so * the anchor is not called `stale` (change: ground-generated-specs-in-the-graph). */ | 'not-assessed'; export interface SpecSymbolRef { name: string; /** Normalized repo-relative POSIX path. */ file: string; line: number; kind: string; } export interface SpecLinkAnchor { /** The anchor exactly as written in the spec. */ raw: string; /** Normalized repo-relative path, or `null` when the anchor names only a symbol. */ file: string | null; /** Exact symbol name, or `null` for a file-only anchor. */ symbol: string | null; state: SpecAnchorState; /** Bounded exact-name candidates disclosed for `ambiguous` / `stale`. */ candidates: SpecSymbolRef[]; /** Total exact matches before the candidate bound was applied. */ candidateTotal: number; /** Why a `not-assessed` anchor could not be assessed (`language-not-extracted`, …). */ boundary?: string; } export interface SpecRequirementLink { requirement: string; domain: string; /** Repo-relative path of the spec that declares the requirement. */ specFile: string; state: SpecLinkState; anchors: SpecLinkAnchor[]; /** Symbols established as covered — only uniquely-resolved exact anchors. */ functions: SpecSymbolRef[]; /** Files contributed to the domain footprint by file-only anchors. Never coverage. */ footprintFiles: string[]; } export interface SpecLinkIndexProvenance { /** Identity of the committed analysis generation the anchors were resolved against. */ analysisGeneration: string; /** Digest over every parsed spec's content, so a spec edit invalidates the cache. */ specDigest: string; /** * Legacy export-inventory fingerprint, retained so a v2 consumer can still tell * whether the artifact describes its analysis. */ sourceAnalysisFingerprint?: string; } export interface SpecLinkIndexStats { totalRequirements: number; linked: number; ambiguous: number; unmapped: number; stale: number; /** Requirements whose anchors could not be assessed (change: ground-generated-specs-in-the-graph). */ notAssessed: number; /** Exported, non-type symbols in the analyzed graph. */ totalExportedFunctions: number; /** Distinct symbols covered by at least one uniquely-resolved anchor. */ coveredFunctions: number; orphanCount: number; /** Distinct files reached by file-only anchors. */ footprintFileCount: number; } export interface SpecLinkIndex { version: typeof SPEC_LINK_INDEX_VERSION; generatedAt: string; provenance: SpecLinkIndexProvenance; links: SpecRequirementLink[]; /** Exported symbols no requirement anchors. */ orphanFunctions: SpecSymbolRef[]; stats: SpecLinkIndexStats; } export interface SpecLinkIndexSpecInput { domain: string; /** Repo-relative path of the spec file. */ specFile: string; content: string; } export interface SpecLinkIndexInput { specs: SpecLinkIndexSpecInput[]; graph: DependencyGraphResult; /** Identity of the analysis generation being resolved against. */ analysisGeneration: string; /** Legacy export-inventory fingerprint, recorded for compatibility reporting. */ sourceAnalysisFingerprint?: string; maxCandidates?: number; /** * The boundary that makes a cited file unassessable, or `undefined` when its exports are fully * inventoried. Supplied by the I/O shell, which can read the filesystem; absent, * every file is treated as assessable (change: ground-generated-specs-in-the-graph). */ assessFile?: (file: string) => string | undefined; /** * The real repository spelling of a cited file (through symlinks, and letter case on a * case-insensitive volume), or `undefined` when it is no file. Anchors match exports by it, so an * existing symbol cited under another spelling of its file is not called `stale` * (change: ground-generated-specs-in-the-graph). */ canonicalFile?: (file: string) => string | undefined; /** Injected only by tests that need a stable `generatedAt`. */ now?: () => Date; } /** One raw anchor token split into the parts a resolver can act on. */ export interface ParsedSpecAnchor { /** Normalized repo-relative path, or `null` when the token names only a symbol. */ file: string | null; /** Exact symbol name, or `null` for a file-only anchor. */ symbol: string | null; /** * Set only for a slash-free dotted token whose shape fits BOTH a file and a * member identity. The parser records both readings; the resolver picks the * symbol one only when the graph actually holds that exact name. */ memberCandidate?: string; } /** * Normalize an anchor path to a repo-relative POSIX path, or `null` when it * escapes the repository (absolute, drive-qualified, or `..`-relative). A path * that cannot be confined is dropped rather than resolved — an anchor is only * ever evidence about this repository. */ export declare function normalizeAnchorPath(value: string): string | null; /** * Parse one raw anchor token into its path and symbol parts. * * Accepted forms, in resolution order: * - `name::path/to/file.ts` — the house `name::path` convention used by the * navigation tools, so a spec anchor reads the same as a tool argument * - `path/to/file.ts#name` — the familiar document-fragment form * - `path/to/file.ts` — file-only, footprint evidence only * - `name` — a bare exact symbol name, resolved graph-wide * - `Class.method` — a dotted token that is BOTH readings until the * graph settles it (see {@link memberCandidateOf}) * * An unparseable or repo-escaping token yields `null` and is ignored. */ export declare function parseSpecAnchor(raw: string): ParsedSpecAnchor | null; /** * Digest over the parsed specs. Bound into provenance so any spec edit makes a * persisted index recognizably stale without re-deriving it. */ export declare function specCorpusDigest(specs: SpecLinkIndexSpecInput[]): string; /** * A resolver over the graph's exported symbols that answers only with certainty. * * Returns the symbol when the name (optionally constrained to a file) identifies * exactly ONE exported identity, and `null` when it identifies none or several. * Used by standalone generation to verify an LLM-proposed function name before * writing it into a spec as an anchor: an unverifiable proposal is written as no * anchor at all rather than as a probabilistic one. */ export declare function buildSymbolResolver(graph: DependencyGraphResult): (name: string, file?: string | null) => SpecSymbolRef | null; /** Render a symbol as the canonical `name::path` spec anchor. */ export declare function formatSpecAnchor(ref: SpecSymbolRef): string; /** * Exact anchors a generator verified against the graph, keyed by requirement. * * Only verified entries exist: a proposal that did not resolve to exactly one * symbol is absent from the map, so the spec is written with no anchor for that * requirement rather than with a guessed one. */ export type VerifiedRequirementAnchors = ReadonlyMap; /** Key requirements the same way on both the write and the read side. */ export declare function requirementAnchorKey(domain: string, requirement: string): string; /** * Build the deterministic link index. Pure: same specs plus same graph always * produce the same index, modulo `generatedAt`. */ export declare function buildSpecLinkIndex(input: SpecLinkIndexInput): SpecLinkIndex; export type MappingArtifactRead = { kind: 'link-index'; index: SpecLinkIndex; } | { kind: 'legacy'; version?: number; generatedAt?: string; sourceAnalysisFingerprint?: string; reason: 'incompatible-provenance'; } | { kind: 'invalid'; reason: 'invalid-json'; }; /** * Read a persisted `mapping.json` without trusting it. * * A v3 artifact is returned as a link index. A v1/v2 artifact is reported as * legacy provenance — never converted, because its links were produced by LLM, * semantic, and name-similarity matching that this schema deliberately refuses * to treat as coverage. Unparseable content is `invalid-json`. */ export declare function readMappingArtifact(raw: string): MappingArtifactRead; /** * Is a persisted index still describing the current inputs? Both the analysis * generation and the spec digest must match — a spec edit alone invalidates it. */ export declare function isLinkIndexCurrent(index: SpecLinkIndex, analysisGeneration: string, specDigest: string): boolean; //# sourceMappingURL=spec-link-index.d.ts.map