/** * Symbol-level changed-sets between a base revision and the working tree * (change: add-symbol-content-hashes). * * `blast_radius`, `select_tests` and `briefing_since` used to seed from every production symbol in * every changed file. This module narrows that to the symbols that actually changed. Each changed * file is extracted twice with normalized content hashes, once at the base revision and once from the * working tree, and the two hash sets are compared: a symbol is `changed` when its hash differs, * `appeared` or `disappeared` when it exists on one side only. Only the files the diff names are ever * read, so the cost is bounded by the diff and never by the repository. * * Narrowing must never drop a symbol that file-level seeding would have caught for a reason that * still holds. A file therefore stays FILE-granular, with a named reason, whenever the evidence is * incomplete: * * - either side could not be read, parsed cleanly, or hashed (`unreadable`, `parse-errors`, * `language-not-hashed`, `span-not-contiguous`, `invalid-span`); * - anything outside every symbol changed: an import, a module-level constant, a class field, or * the order of the symbols (`module-level-change`); * - module-level code NAMES a changed symbol, so it may bind it (`module-level-reference`); * - the index does not match what the working tree extracts to (`index-mismatch`); * - a bound is spent: files, per-file bytes, total bytes, or wall clock (`file-cap`, `size-cap`, * `time-cap`) — every one of which keeps all of that file's symbols seeded; * - the changed-set could not assess the file at all (`not-assessed`). * * Inside a symbol-granular file, two more groups stay seeded because a same-file caller can reach a * changed symbol without a resolved edge: symbols whose text names a changed symbol (`referencing`), * and symbols that hold a dynamic-dispatch site (`dynamicDispatch`). Cross-file effects are no worse * than file-level seeding, which never seeded other files either. * * A disappeared/appeared pair that symbol-identity continuity (`analyzer/continuity.ts`) matches is * also reported as a carried rename or move. Both ids stay in the seed set — which seeds nothing at * all when the index holds neither path yet, and the receipt says so through `changedSymbolsNotIndexed` * rather than letting the caller read the silence as "unchanged". */ import type { ChangedFile } from '../../types/index.js'; import type { FunctionNode, SerializedCallGraph } from '../analyzer/call-graph.js'; import { type ContinuityPair } from '../analyzer/continuity.js'; /** * Most changed files hashed per call. Each costs two reads and two parses. A diff that names more * code files than this keeps the rest file-granular (`file-cap`), which is today's behavior and is * disclosed. A cost bound, not a change-detection threshold: detection is hash equality only. */ export declare const MAX_SYMBOL_HASHED_FILES = 200; /** * Cumulative source bytes (both revisions) hashed per call. The file bound alone says nothing about * cost — 200 large files parse far longer than 200 small ones — so the byte bound is what keeps the * worst case bounded. Deterministic (files are read in path order), and disclosed as `size-cap`. */ export declare const MAX_SYMBOL_HASHED_BYTES: number; /** * Largest single file hashed. A file this big is parsed twice, and its parse dominates the call's * cost; a diff that touches one is better served whole (`size-cap`) than by a briefing that takes * a minute. Deterministic and disclosed, like every other bound here. */ export declare const MAX_SYMBOL_HASHED_FILE_BYTES: number; /** * Wall-clock the hashing pass may spend before the remaining files keep file granularity * (`time-cap`). Bytes are a poor proxy for parse cost — a file of 900 tiny functions parses far * slower than one function of the same size — and these tools run in a pre-commit hook and an agent * turn, where minutes are not available. Like the analyzer's per-file parse budget, this trades * PRECISION for a bounded answer: the degraded direction is always the conservative one (the whole * file counts as changed), so a slow machine can only ever seed MORE, never fewer, and the receipt * names every file it skipped. */ export declare const SYMBOL_HASHING_BUDGET_MS = 8000; /** Why a changed file keeps file-level granularity. A closed vocabulary. */ export type FileGranularityReason = 'language-not-hashed' | 'parse-errors' | 'module-level-change' | 'module-level-reference' | 'span-not-contiguous' | 'invalid-span' | 'unreadable' | 'index-mismatch' | 'file-cap' | 'size-cap' | 'file-too-large' | 'time-cap' | 'not-assessed'; export declare const FILE_GRANULARITY_REASONS: Record; export interface SymbolGranularChange { granularity: 'symbol'; /** Present on both sides with a different normalized hash. */ changed: string[]; /** Present only in the working tree. */ appeared: string[]; /** Present only at the base revision. */ disappeared: string[]; /** Unchanged symbols whose text names a changed, appeared, or disappeared symbol. */ referencing: string[]; /** Unchanged symbols that hold a dynamic-dispatch site the resolver cannot follow. */ dynamicDispatch: string[]; /** * Module-level code outside the imports is identical, and the imports gained bindings the file did * not have. Present so a consumer can disclose the one thing this narrowing does not attribute to * the file's other symbols: the load-time side effects of the newly imported module. */ importsAdded?: true; } export interface FileGranularChange { granularity: 'file'; reason: FileGranularityReason; } export type FileSymbolChange = SymbolGranularChange | FileGranularChange; export interface CarriedSymbol { from: string; to: string; reason: ContinuityPair['reason']; basis: ContinuityPair['basis']; } export interface SymbolChangedSet { /** The base revision this set was computed against, so a reusing caller can check it matches. */ baseRef?: string; /** Keyed by the analyzed-root-relative path the index uses. */ byFile: Map; /** Renames and moves continuity matched, sorted by `from`. */ carried: CarriedSymbol[]; } /** Entry shape the consumers already hold: `getChangedFiles` output. */ export type DiffEntry = Pick; /** * Compute the symbol-level changed-set for the code files a diff names. `baseRef` is resolved the * way `getChangedFiles` resolves it, and old content is read at the merge base it diffs from. * Never throws: any failure keeps the affected file file-granular. */ export declare function computeSymbolChangedSet(input: { absDir: string; baseRef: string; diff: readonly DiffEntry[]; callGraph: SerializedCallGraph; /** Overrides {@link MAX_SYMBOL_HASHED_FILES} (tests). */ maxFiles?: number; /** Overrides {@link MAX_SYMBOL_HASHED_BYTES} (tests). */ maxBytes?: number; /** Overrides {@link MAX_SYMBOL_HASHED_FILE_BYTES} (tests). */ maxFileBytes?: number; /** Overrides {@link SYMBOL_HASHING_BUDGET_MS} (tests). */ budgetMs?: number; }): Promise; /** * Narrow file-level seeds to the symbols the changed-set implicates. A seed in a file the set did * not cover, or covered at file granularity, is kept: narrowing only ever removes a seed on evidence. */ export declare function narrowSeedsToChangedSymbols(seeds: FunctionNode[], set: SymbolChangedSet): FunctionNode[]; /** * Record every seed file the changed-set did not cover as file-granular `not-assessed`, so the * receipt accounts for each file that contributed seeds. Returns a new set; the input is unchanged. */ export declare function coverSeedFiles(set: SymbolChangedSet, seeds: readonly FunctionNode[]): SymbolChangedSet; /** The ids that genuinely changed in a symbol-granular file (for a "what changed" briefing). */ export declare function changedSymbolIds(change: SymbolGranularChange): Set; /** Bounded, consumer-facing receipt of how precise the changed-set was. */ export interface ChangeGranularityReceipt { symbolExactFiles: number; fileGranularFiles: number; /** Symbol-exact files whose module level gained imports (see {@link SymbolGranularChange.importsAdded}). */ importsAddedFiles: number; /** Symbols the hashes found changed, appeared, or disappeared — whatever the index knows. */ changedSymbolsFound: number; /** Of those, the ones no indexed symbol matches: the index predates the edit. */ changedSymbolsNotIndexed: number; /** How many file-granular files each reason accounts for (all of them, not the sample). */ reasons: Partial>; /** Which files stayed file-granular and why, bounded to {@link GRANULARITY_FALLBACK_SAMPLE}. */ fallbacks: Array<{ file: string; reason: FileGranularityReason; }>; fallbacksOmitted?: number; } export declare const GRANULARITY_FALLBACK_SAMPLE = 20; export declare function granularityReceipt(set: SymbolChangedSet, /** Whether an id exists in the index. Absent → nothing is counted as not-indexed. */ isIndexed?: (id: string) => boolean): ChangeGranularityReceipt; /** What a consumer may say when nothing was seeded: one sentence, and a headline-length form. */ export interface NoChangeClaim { kind: 'unchanged' | 'not-indexed' | 'not-seeded' | 'not-assessed'; /** The full sentence, for a caveat. */ text: string; /** The same claim in headline length. Derived here so a headline cannot drift from the caveat. */ headline: string; } /** * The claim a consumer may make when nothing was seeded, or `undefined` when it may make none. * "Nothing differs" is only ever true when every changed code file was hashed AND the hashes found * no changed symbol. A symbol that changed but is absent from the index is "not indexed", never * "unchanged" — that is the stale-index case, and it is the most common one. */ export declare function noChangeClaim(receipt: ChangeGranularityReceipt): NoChangeClaim; /** One caveat line for a consumer, or undefined when every changed file was symbol-exact. */ /** * The phrase each of this module's caveats is built around. A renderer decides whether a caveat * qualifies the changed-set by asking {@link isChangedSetCaveat}, never by matching prose of its * own: three renderers carrying three regexes over wording defined here is a silent-drop waiting to * happen the next time a sentence is reworded. `changed-set-caveats` in the tests pins the pairing. */ export declare const CHANGED_SET_CAVEAT_MARKERS: readonly ["stayed at FILE granularity", "bind new names", "did not themselves change", "renamed or moved with an unchanged body", "absent from the index", "none of them is in scope here", "not assessed at symbol level", "No changed code file could be assessed", "formatting or comments only", "No symbol's behavior differs", "No changed code file"]; /** True when a caveat came from this module and qualifies WHAT the changed-set covered. */ export declare function isChangedSetCaveat(caveat: string): boolean; /** Seeds kept for a reason other than their own change: the caller must not call them "changed". */ export declare function seededUnchangedCaveat(count: number): string | undefined; /** Renames and moves whose body is unchanged: their callers change even though their behavior does not. */ export declare function carriedCaveat(carried: readonly CarriedSymbol[]): string | undefined; export declare function importsAddedCaveat(receipt: ChangeGranularityReceipt): string | undefined; export declare function granularityCaveat(receipt: ChangeGranularityReceipt): string | undefined; //# sourceMappingURL=symbol-changed-set.d.ts.map