/** * Corpus indexer — read files, chunk, embed in batches, persist. * * Idempotent by file hash: if a path's sha256 matches what's already in * the corpus under the same model_version, chunks for that path are * reused verbatim (no re-embedding). Files no longer in the input set * are dropped. New or changed files are embedded fresh. * * This means `index` can be called repeatedly in daily use without * burning the embed tier on unchanged content. * * Phase 7 / FT-001 event-emission policy: this module does NOT call * the logger directly — the report return value is the operator-facing * surface. Tool handlers (src/tools/corpusIndex.ts) build their own * `kind: "call"` event from the report; if a future call site needs to * emit a corpus-step event, use `buildCorpusIndexStepEvent` below * (op: 'pack_step', rule: 'corpus_index_step') so the closed * CorrelationOp enum stays tight. */ import type { OllamaClient } from "../ollama.js"; /** * Mint a globally-unique chunk id. Folds a PATH digest into the id so two * DIFFERENT files with IDENTICAL content (duplicate LICENSE / template / stub * docs — common in doc trees) don't collide into one id and shadow each other * in the searcher's chunkById map / RRF fusion (M8, 2026-07 health pass). * Deterministic in (name, path, fileHash, index): re-indexing is stable AND * heals any corpus minted under the old path-less `name-contentHash-index` * scheme, since reused/carried chunks are re-minted through this same helper. */ export declare function mintChunkId(corpusName: string, path: string, fileHash: string, chunkIndex: number): string; export interface IndexParams { name: string; paths: string[]; model: string; chunk_chars?: number; chunk_overlap?: number; client: OllamaClient; /** * Optional progress callback invoked after each input file is processed * (success OR failure). `done` counts files that have been handled, * `total` is params.paths.length, `currentPath` is the path just * processed. Safe to ignore — purely observability. A second callback * fires after each embed batch with done=total+batchIdx, so callers can * see embed progress too; MCP tool layer can filter on `currentPath` * starting with "embed:" to distinguish. */ onProgress?: (done: number, total: number, currentPath: string) => void; } /** One entry per path that could not be read/hashed during indexing. */ export interface IndexFailedPath { path: string; reason: string; } export interface IndexReport { name: string; model_version: string; documents: number; chunks: number; total_chars: number; reused_chunks: number; newly_embedded_chunks: number; dropped_files: string[]; elapsed_ms: number; /** * The tag Ollama resolved the embed model to during this index run (e.g. * "nomic-embed-text:latest"), captured from EmbedResponse.model on the * FIRST embed response. Null if no embed happened this run (pure reuse). * Refresh uses this to detect silent :latest drift across runs. */ embed_model_resolved: string | null; /** * Additional resolved tags observed within this single refresh — populated * only when Ollama silently bumped `:latest` partway through a multi-batch * index. Rare but possible if a daemon restarts or the tag is rotated * mid-run. When present, chunks from the first half are embedded by the * original model and chunks from the second half by the new one, which * quietly corrupts the vector space. The index report surfaces this as a * warning and the manifest records it too. * * Empty or absent on the happy path (all batches returned the same tag). */ embed_model_resolved_drift_within_refresh?: string[]; /** * Paths that could not be read (size cap, symlink, permission denied, * TOCTOU, etc.) during this index run. Indexing continues past these so * one bad file in a batch of 1000 no longer halts the whole pass. Empty * array on the happy path. */ failed_paths: IndexFailedPath[]; } /** * Read a file and hash it with TOCTOU protection. * * Invariant: a successful return means "the file was in exactly this state * (size + mtime) when we hashed it". We stat BEFORE read (to enforce size * cap and symlink rejection without reading bytes first), then stat AGAIN * after read and fail if size or mtime drifted — that means the file * mutated mid-read and the hash doesn't match the returned content. */ export declare function sha256File(path: string): Promise<{ hash: string; mtime: string; content: string; }>; export declare function indexCorpus(params: IndexParams): Promise; /** * Internal: indexCorpus body without the per-corpus lock. Refresh uses * this because it already holds the lock — calling indexCorpus from * inside a held lock would self-deadlock. */ export declare function indexCorpusUnlocked(params: IndexParams): Promise; /** * Detail payload for an operator-facing structured event a tool handler * can emit when it wraps a corpus index in a pack-step pipeline. * * Phase 7 / FT-001: tagged `op: 'pack_step'` so it slots into the * existing closed CorrelationOp enum (corpus indexing is a structured * multi-step operation; a separate corpus_step op would be redundant). * The NDJSON logger auto-merges `run_id` from ALS at write time. * * Carries the headline counters from the index report so a log-only * consumer can build a "indexing throughput" view without capturing * envelopes. */ export interface CorpusIndexStepEventDetail { /** Closed-enum op tag from observability.CorrelationOp. */ op: "pack_step"; /** Stable rule identifier — greppable. */ rule: "corpus_index_step"; /** Corpus name. */ name: string; /** Documents (unique paths) in the resulting corpus. */ documents: number; /** Total chunks in the resulting corpus. */ chunks: number; /** Chunks reused verbatim from the existing corpus. */ reused_chunks: number; /** Chunks newly embedded this run. */ newly_embedded_chunks: number; /** Files that were in the prior corpus but dropped from the input. */ dropped_file_count: number; /** True when Ollama bumped `:latest` mid-index (within a single run). */ embed_model_drift_within_refresh: boolean; /** Number of paths that failed this run (read errors, size cap, symlink, etc.). */ failed_path_count: number; } /** * Build the pack-step event detail for a completed index run. Pure * shaping — does NOT call the logger itself. */ export declare function buildCorpusIndexStepEvent(report: IndexReport): CorpusIndexStepEventDetail; //# sourceMappingURL=indexer.d.ts.map