/** * The concept-layer enrichment pass. * * Walks the index's files most-central-first and asks a model, once per file, * what the file is for. The answer is a short plain-English summary plus a * **crux**: the line span that actually carries the file's meaning. A summary * can drift from the truth; a pointer into the source cannot, so the two are * always stored together. * * ## Why this is a separate pass * * Model calls take seconds. `runIndexerAtomic` is a SQLite write transaction — * holding one open across thousands of network round trips would block every * other writer for the duration and roll the whole thing back on the first * failure. So enrichment runs on its own, outside the indexer, writing each * result as it arrives. * * ## Why it is affordable * * `files.content_hash` already exists and is exactly the right cache key: a * file whose bytes have not changed is never re-sent. A first pass over this * repository is thousands of calls; every pass after it is only the files that * actually changed. That is the difference between a one-off cost and a * recurring one. * * ## Why it never takes the caller down * * Enrichment is an optional layer over a working index. A model that refuses, * times out, or returns something unparseable degrades one file's summary — it * is recorded in `errors` and the walk continues. Cancellation is honoured * between files, and everything already written stays written. */ import { type IndexStore } from './writer.js'; /** What the summariser is given about one file. */ export interface SummarizeFileInput { /** Project-relative path, for the model's benefit. */ file: string; /** Absolute path, if the summariser wants to read more itself. */ absolutePath: string; language: string; /** File source, already truncated to {@link MAX_SOURCE_CHARS}. */ source: string; /** Whether `source` was cut short. */ truncated: boolean; /** Declarations the index recorded, as orientation. */ declarations: ReadonlyArray<{ name: string; kind: string; line: number; }>; /** A previous, now-outdated summary, when one exists. */ staleSummary?: string | undefined; signal?: AbortSignal | undefined; } export interface SummarizeFileResult { /** One or two sentences on what the file is for. */ summary: string; /** 1-based inclusive line span of the load-bearing lines. */ cruxStart?: number | undefined; cruxEnd?: number | undefined; /** Model identifier, recorded so a later pass can tell what produced this. */ model?: string | undefined; } export interface SummarizeSubsystemInput { /** Package or directory label. */ name: string; files: ReadonlyArray<{ file: string; summary: string; rank: number; }>; signal?: AbortSignal | undefined; } export interface SummarizeSubsystemResult { summary: string; /** * Other subsystem names this one relates to, with a relation from the closed * vocabulary. Unknown relations and unknown targets are dropped by the caller. */ relations?: ReadonlyArray<{ to: string; relation: string; }> | undefined; model?: string | undefined; } /** * The host-supplied model transport. * * Injected rather than imported: producing a summary needs a configured * provider and the model-tier policy, both of which live in the host. This * mirrors how SAGE takes `getLlmCall` — `packages/tools` stays free of * provider wiring, and a host that supplies no port simply gets no concepts. */ export interface SummarizerPort { describeFile(input: SummarizeFileInput): Promise; describeSubsystem?(input: SummarizeSubsystemInput): Promise; } /** Source sent per file. Enough for a summary; short enough to stay cheap. */ export declare const MAX_SOURCE_CHARS = 12000; /** Crux span ceiling. Graft uses twelve lines; longer stops being a pointer. */ export declare const MAX_CRUX_LINES = 12; export declare const DEFAULT_CONCURRENCY = 5; /** Summary length ceiling, so one verbose model cannot bloat the layer. */ export declare const MAX_SUMMARY_CHARS = 400; export interface EnrichOptions { /** Stop after this many files. The natural way to sample the cost first. */ maxFiles?: number | undefined; /** Files summarised in parallel. */ concurrency?: number | undefined; /** Re-summarise files whose summary is already current. */ force?: boolean | undefined; /** Also derive the subsystem layer, when the port supports it. */ subsystems?: boolean | undefined; signal?: AbortSignal | undefined; onProgress?: ((done: number, total: number) => void) | undefined; } export interface EnrichResult { /** Files sent to the model. */ summarised: number; /** Files skipped because their stored summary already matched. */ cached: number; /** Files the model declined or failed on. */ failed: number; /** Concepts marked stale before the walk started. */ markedStale: number; /** Concepts dropped because their file left the index. */ pruned: number; subsystems: number; durationMs: number; errors: string[]; } /** * Run one enrichment pass. * * `relativeOf` is injected for the same reason the retrieval walk takes it — * the caller owns what "project-relative" means, and this module stays free of * path policy. */ export declare function enrichConcepts(store: IndexStore, port: SummarizerPort, relativeOf: (file: string) => string, options?: EnrichOptions): Promise; /** Reported when a project has no index to enrich. */ export type ConceptIndexMissing = { indexed: false; }; /** * Enrich a project's concept layer, owning the store lifetime so callers * outside this package never touch `indexStorePool`. * * Refuses to run without an existing index: opening a store CREATES the * database, and spending money summarising an empty index helps nobody. */ export declare function enrichProjectConcepts(projectRoot: string, port: SummarizerPort, options?: EnrichOptions & { indexDir?: string | undefined; }): Promise; //# sourceMappingURL=concept-enrichment.d.ts.map