{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/indexer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAK5E,OAAO,EAAE,KAAK,yBAAyB,EAAkB,MAAM,eAAe,CAAC;AAE/E,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAEjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG7E,MAAM,WAAW,YAAY;IAC5B,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACrE,YAAY,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CACjB;AA0BD,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,MAAM,CAAuB;IAErC,YAAY,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,yBAAyB,EAAE,MAAM,CAAC,EAAE,oBAAoB,EAI9F;IAED,IAAI,gBAAgB,IAAI,oBAAoB,CAE3C;IAED,6CAA6C;IACvC,KAAK,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CA2GzD;IAED,gEAAgE;IAC1D,OAAO,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CA6H3D;IAED,OAAO,CAAC,oBAAoB;IAiB5B,OAAO,CAAC,SAAS;YAoDH,sBAAsB;YAoBtB,cAAc;CA6G5B","sourcesContent":["/**\n * Workspace indexer: full builds and incremental refresh.\n *\n * A build creates a new index generation atomically. Building generations are\n * never query authority; the most recent validated ready generation is served\n * during a build. Interrupted builds are discardable. Source files are never\n * written by the indexer.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { type ChunkOutput, chunkFile } from \"./chunk.js\";\nimport { resolveIndexConfig, type WorkspaceIndexConfig } from \"./config.js\";\nimport { discoverWorkspaceFiles } from \"./discovery.js\";\nimport { resolveEmbeddingBackend, truncateForEmbedding } from \"./embedding.js\";\nimport { collectGitMetadata, computeWorktreeFingerprint, type GitFileMeta } from \"./git.js\";\nimport { sha256 } from \"./guard.js\";\nimport { type ResolvedWorkspaceIdentity, toIdentityMeta } from \"./identity.js\";\nimport { deleteLexical, writeLexical } from \"./lexical.js\";\nimport { newGenerationId, type WorkspaceDb } from \"./storage.js\";\nimport { extractSymbolsHeuristic, markdownSections } from \"./symbols.js\";\nimport type { EmbeddingBackend, WorkspaceIndexGeneration } from \"./types.js\";\nimport { VectorIndex } from \"./vectors.js\";\n\nexport interface BuildOptions {\n\tconfig?: WorkspaceIndexConfig;\n\tsignal?: AbortSignal;\n\tonEvent?: (event: string, payload?: Record<string, unknown>) => void;\n\tembedBackend?: EmbeddingBackend;\n}\n\nexport interface BuildReport {\n\tgenerationId: string;\n\tstatus: WorkspaceIndexGeneration[\"status\"];\n\tfilesIndexed: number;\n\tchunksIndexed: number;\n\tsymbolsIndexed: number;\n\tembeddingsIndexed: number;\n\tdiscovered: number;\n\tskippedIgnored: number;\n\tsensitiveSkipped: number;\n\tchanged: number;\n\tadded: number;\n\tremoved: number;\n\trebuild: boolean;\n}\n\ninterface FileModel {\n\tfileId: string;\n\trelPath: string;\n\tcontent: string;\n\tcontentSha256: string;\n\tclassification: string;\n\tlanguageId?: string;\n\tsizeBytes: number;\n\tlineCount: number;\n\tisSensitive: boolean;\n\tchunks: ChunkOutput[];\n\tsymbols: Array<{\n\t\tsymbolId: string;\n\t\tname: string;\n\t\tqualifiedName?: string;\n\t\tkind: string;\n\t\tlanguageId: string;\n\t\tstartLine: number;\n\t\tstartCharacter: number;\n\t\tendLine: number;\n\t\tendCharacter: number;\n\t}>;\n}\n\nexport class WorkspaceIndexer {\n\tprivate db: WorkspaceDb;\n\tprivate identity: ResolvedWorkspaceIdentity;\n\tprivate config: WorkspaceIndexConfig;\n\n\tconstructor(db: WorkspaceDb, identity: ResolvedWorkspaceIdentity, config?: WorkspaceIndexConfig) {\n\t\tthis.db = db;\n\t\tthis.identity = identity;\n\t\tthis.config = config ?? resolveIndexConfig();\n\t}\n\n\tget generationConfig(): WorkspaceIndexConfig {\n\t\treturn this.config;\n\t}\n\n\t/** Full (re)build of the workspace index. */\n\tasync build(opts: BuildOptions = {}): Promise<BuildReport> {\n\t\tconst { signal } = opts;\n\t\tconst onEvent = opts.onEvent ?? (() => {});\n\t\tconst identity = this.identity;\n\t\tconst genId = newGenerationId();\n\t\tconst gen: WorkspaceIndexGeneration = {\n\t\t\tgenerationId: genId,\n\t\t\tworkspaceId: identity.workspaceId,\n\t\t\tschemaVersion: toIdentityMeta(identity).indexVersion,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t\tsourceSnapshot: {\n\t\t\t\tworktreeFingerprint: \"\",\n\t\t\t\tfileManifestHash: \"\",\n\t\t\t},\n\t\t\tstatus: \"building\",\n\t\t\tfileCount: 0,\n\t\t\tchunkCount: 0,\n\t\t\tsymbolCount: 0,\n\t\t\tembeddingCount: 0,\n\t\t};\n\n\t\t// The old ready generation (if any) stays authoritative during build.\n\t\tonEvent(\"WORKSPACE_INDEX_BUILD_STARTED\", { generationId: genId });\n\n\t\tconst embed = opts.embedBackend ?? resolveEmbeddingBackend(this.config.embedding);\n\t\tconst embedEnabled = embed.backendId !== \"disabled\";\n\n\t\ttry {\n\t\t\tconst discovery = discoverWorkspaceFiles({\n\t\t\t\tcwd: identity.canonicalRoot,\n\t\t\t\troot: identity.canonicalRoot,\n\t\t\t\tmaxFileBytes: this.config.maxFileBytes,\n\t\t\t\tadditionalIgnores: this.config.additionalIgnores,\n\t\t\t\tincludeLockfilesAsMetadata: this.config.includeLockfilesAsMetadata,\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tconst fingerprintBundle = computeWorktreeFingerprint(identity.canonicalRoot, identity.worktreeId);\n\t\t\tgen.sourceSnapshot.worktreeFingerprint = fingerprintBundle.fingerprint;\n\t\t\tgen.sourceSnapshot.gitHead = fingerprintBundle.gitHead;\n\n\t\t\tthis.db.insertGeneration(gen);\n\n\t\t\tconst files: FileModel[] = [];\n\t\t\tlet sensitiveSkipped = 0;\n\n\t\t\tfor (const f of discovery.files) {\n\t\t\t\tif (signal?.aborted) throwObject(\"WORKSPACE_INDEX_BUILD_CANCELLED\");\n\t\t\t\tif (f.classification.isSensitive || f.classification.isBinary) {\n\t\t\t\t\t// Metadata-only handling below; sensitive content never chunked.\n\t\t\t\t\tsensitiveSkipped++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst fileModel = this.modelFile(f.absolutePath, f.workspaceRelativePath, f.classification, f.sizeBytes);\n\t\t\t\tfiles.push(fileModel);\n\t\t\t}\n\n\t\t\t// Batch DB writes in a transaction for atomicity of the generation.\n\t\t\tthis.db.begin();\n\t\t\ttry {\n\t\t\t\tawait this.writeFilesToGeneration(genId, files, embed, embedEnabled, signal, onEvent);\n\t\t\t\tconst counts = this.db.counts(genId);\n\t\t\t\tgen.fileCount = counts.files;\n\t\t\t\tgen.chunkCount = counts.chunks;\n\t\t\t\tgen.symbolCount = counts.symbols;\n\t\t\t\tgen.embeddingCount = counts.embeddings;\n\t\t\t\tgen.sourceSnapshot.fileManifestHash = this.db.fileManifestHash(genId);\n\t\t\t\tgen.status = \"ready\";\n\t\t\t\tgen.completedAt = new Date().toISOString();\n\t\t\t\tthis.db.insertGeneration(gen);\n\t\t\t\tthis.db.commit();\n\t\t\t} catch (err) {\n\t\t\t\tthis.db.rollback();\n\t\t\t\tthis.db.updateGenerationStatus(genId, \"failed\");\n\t\t\t\tthrow err;\n\t\t\t}\n\n\t\t\tonEvent(\"WORKSPACE_INDEX_GENERATION_READY\", {\n\t\t\t\tgenerationId: genId,\n\t\t\t\tfiles: gen.fileCount,\n\t\t\t\tchunks: gen.chunkCount,\n\t\t\t\tsymbols: gen.symbolCount,\n\t\t\t\tembeddings: gen.embeddingCount,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\tgenerationId: genId,\n\t\t\t\tstatus: \"ready\",\n\t\t\t\tfilesIndexed: gen.fileCount,\n\t\t\t\tchunksIndexed: gen.chunkCount,\n\t\t\t\tsymbolsIndexed: gen.symbolCount,\n\t\t\t\tembeddingsIndexed: gen.embeddingCount,\n\t\t\t\tdiscovered: discovery.files.length,\n\t\t\t\tskippedIgnored: discovery.skippedIgnored + discovery.skippedBinary + discovery.skippedTooLarge,\n\t\t\t\tsensitiveSkipped,\n\t\t\t\tchanged: files.length,\n\t\t\t\tadded: files.length,\n\t\t\t\tremoved: 0,\n\t\t\t\trebuild: true,\n\t\t\t};\n\t\t} catch (err) {\n\t\t\tconst cancelled = err instanceof Object && (err as Error).name === \"AbortError\";\n\t\t\tthis.db.updateGenerationStatus(genId, cancelled ? \"failed\" : \"failed\");\n\t\t\tif (cancelled) onEvent(\"WORKSPACE_INDEX_BUILD_CANCELLED\", { generationId: genId });\n\t\t\telse onEvent(\"WORKSPACE_INDEX_BUILD_FAILED\", { generationId: genId });\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\t/** Incremental refresh against the current ready generation. */\n\tasync refresh(opts: BuildOptions = {}): Promise<BuildReport> {\n\t\tconst { signal } = opts;\n\t\tconst onEvent = opts.onEvent ?? (() => {});\n\t\tconst identity = this.identity;\n\t\tconst prev = this.db.currentReadyGeneration();\n\t\tconst genId = newGenerationId();\n\t\tconst gen: WorkspaceIndexGeneration = {\n\t\t\tgenerationId: genId,\n\t\t\tworkspaceId: identity.workspaceId,\n\t\t\tschemaVersion: toIdentityMeta(identity).indexVersion,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t\tsourceSnapshot: { worktreeFingerprint: \"\", fileManifestHash: \"\" },\n\t\t\tstatus: \"building\",\n\t\t\tfileCount: 0,\n\t\t\tchunkCount: 0,\n\t\t\tsymbolCount: 0,\n\t\t\tembeddingCount: 0,\n\t\t};\n\t\tthis.db.insertGeneration(gen);\n\t\tonEvent(\"WORKSPACE_INDEX_BUILD_STARTED\", { generationId: genId, incremental: true });\n\n\t\tconst embed = opts.embedBackend ?? resolveEmbeddingBackend(this.config.embedding);\n\t\tconst embedEnabled = embed.backendId !== \"disabled\";\n\n\t\ttry {\n\t\t\tconst discovery = discoverWorkspaceFiles({\n\t\t\t\tcwd: identity.canonicalRoot,\n\t\t\t\troot: identity.canonicalRoot,\n\t\t\t\tmaxFileBytes: this.config.maxFileBytes,\n\t\t\t\tadditionalIgnores: this.config.additionalIgnores,\n\t\t\t\tincludeLockfilesAsMetadata: this.config.includeLockfilesAsMetadata,\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tconst fingerprintBundle = computeWorktreeFingerprint(identity.canonicalRoot, identity.worktreeId);\n\t\t\tgen.sourceSnapshot.worktreeFingerprint = fingerprintBundle.fingerprint;\n\t\t\tgen.sourceSnapshot.gitHead = fingerprintBundle.gitHead;\n\n\t\t\t// Current manifest: path → contentSha256 (from previous ready generation).\n\t\t\tconst prevManifest = prev ? this.db.fileManifest(prev.generationId) : new Map<string, string>();\n\t\t\tconst byPath = new Map<string, { path: string; sha: string }>();\n\n\t\t\tconst prevFileIds = prev\n\t\t\t\t? new Map([...prevManifest.keys()].map((p) => [p, this.db.fileIdByPath(prev.generationId, p)]))\n\t\t\t\t: new Map();\n\n\t\t\tthis.db.begin();\n\t\t\ttry {\n\t\t\t\tlet added = 0;\n\t\t\t\tlet changed = 0;\n\t\t\t\tlet removed = 0;\n\n\t\t\t\t// Removed files: present in previous manifest but absent now.\n\t\t\t\tconst currentPaths = new Set<string>();\n\t\t\t\tfor (const f of discovery.files) {\n\t\t\t\t\tif (f.classification.isSensitive || f.classification.isBinary) continue;\n\t\t\t\t\tcurrentPaths.add(f.workspaceRelativePath);\n\t\t\t\t\tconst fileModel = this.modelFile(f.absolutePath, f.workspaceRelativePath, f.classification, f.sizeBytes);\n\t\t\t\t\tbyPath.set(f.workspaceRelativePath, { path: f.workspaceRelativePath, sha: fileModel.contentSha256 });\n\t\t\t\t\tconst prevSha = prevManifest.get(f.workspaceRelativePath);\n\t\t\t\t\tif (prevSha === undefined) {\n\t\t\t\t\t\tadded++;\n\t\t\t\t\t\tonEvent(\"WORKSPACE_INDEX_FILE_UPDATED\", { path: f.workspaceRelativePath, action: \"added\" });\n\t\t\t\t\t} else if (prevSha !== fileModel.contentSha256) {\n\t\t\t\t\t\tchanged++;\n\t\t\t\t\t\tonEvent(\"WORKSPACE_INDEX_FILE_UPDATED\", { path: f.workspaceRelativePath, action: \"modified\" });\n\t\t\t\t\t}\n\t\t\t\t\tawait this.writeFileModel(genId, fileModel, embed, embedEnabled, signal);\n\t\t\t\t}\n\n\t\t\t\tfor (const prevPath of prevManifest.keys()) {\n\t\t\t\t\tif (!currentPaths.has(prevPath)) {\n\t\t\t\t\t\tremoved++;\n\t\t\t\t\t\tonEvent(\"WORKSPACE_INDEX_FILE_REMOVED\", { path: prevPath });\n\t\t\t\t\t\tconst fid = prevFileIds.get(prevPath);\n\t\t\t\t\t\tif (fid) this.removeGenerationData(genId, fid, prevManifest.get(prevPath) ?? \"\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst counts = this.db.counts(genId);\n\t\t\t\tgen.fileCount = counts.files;\n\t\t\t\tgen.chunkCount = counts.chunks;\n\t\t\t\tgen.symbolCount = counts.symbols;\n\t\t\t\tgen.embeddingCount = counts.embeddings;\n\t\t\t\tgen.sourceSnapshot.fileManifestHash = this.db.fileManifestHash(genId);\n\t\t\t\tgen.status = \"ready\";\n\t\t\t\tgen.completedAt = new Date().toISOString();\n\t\t\t\tthis.db.insertGeneration(gen);\n\t\t\t\tthis.db.commit();\n\n\t\t\t\tonEvent(\"WORKSPACE_INDEX_GENERATION_READY\", {\n\t\t\t\t\tgenerationId: genId,\n\t\t\t\t\tadded,\n\t\t\t\t\tchanged,\n\t\t\t\t\tremoved,\n\t\t\t\t\tfiles: gen.fileCount,\n\t\t\t\t\tchunks: gen.chunkCount,\n\t\t\t\t});\n\t\t\t\tvoid prevFileIds;\n\t\t\t\tvoid byPath;\n\t\t\t\treturn {\n\t\t\t\t\tgenerationId: genId,\n\t\t\t\t\tstatus: \"ready\",\n\t\t\t\t\tfilesIndexed: gen.fileCount,\n\t\t\t\t\tchunksIndexed: gen.chunkCount,\n\t\t\t\t\tsymbolsIndexed: gen.symbolCount,\n\t\t\t\t\tembeddingsIndexed: gen.embeddingCount,\n\t\t\t\t\tdiscovered: discovery.files.length,\n\t\t\t\t\tskippedIgnored: discovery.skippedIgnored + discovery.skippedBinary + discovery.skippedTooLarge,\n\t\t\t\t\tsensitiveSkipped: 0,\n\t\t\t\t\tchanged,\n\t\t\t\t\tadded,\n\t\t\t\t\tremoved,\n\t\t\t\t\trebuild: false,\n\t\t\t\t};\n\t\t\t} catch (err) {\n\t\t\t\tthis.db.rollback();\n\t\t\t\tthis.db.updateGenerationStatus(genId, \"failed\");\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.db.updateGenerationStatus(genId, \"failed\");\n\t\t\tonEvent(\"WORKSPACE_INDEX_BUILD_FAILED\", { generationId: genId });\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\tprivate removeGenerationData(genId: string, fileId: string, _contentSha: string): void {\n\t\tconst chunks = this.db.chunksForFile(genId, fileId);\n\t\tfor (const c of chunks) {\n\t\t\tdeleteLexical(this.db, genId, c.chunkId);\n\t\t\tthis.db.db.prepare(\"DELETE FROM vectors WHERE generation_id = ? AND chunk_id = ?\").run(genId, c.chunkId);\n\t\t\tthis.db.db.prepare(\"DELETE FROM chunks WHERE generation_id = ? AND chunk_id = ?\").run(genId, c.chunkId);\n\t\t}\n\t\tthis.db.db.prepare(\"DELETE FROM symbols WHERE generation_id = ? AND file_id = ?\").run(genId, fileId);\n\t\tthis.db.db\n\t\t\t.prepare(\n\t\t\t\t\"DELETE FROM relations WHERE generation_id = ? AND source_symbol_id IN (SELECT symbol_id FROM symbols WHERE generation_id = ? AND file_id = ?)\",\n\t\t\t)\n\t\t\t.run(genId, genId, fileId);\n\t\tthis.db.db.prepare(\"DELETE FROM git_meta WHERE generation_id = ? AND file_id = ?\").run(genId, fileId);\n\t\tthis.db.db.prepare(\"DELETE FROM files WHERE generation_id = ? AND file_id = ?\").run(genId, fileId);\n\t}\n\n\tprivate modelFile(\n\t\tabs: string,\n\t\trel: string,\n\t\tcls: { classification: string; languageId?: string },\n\t\tsizeBytes: number,\n\t): FileModel {\n\t\tconst content = readFileSync(abs, \"utf-8\");\n\t\tconst contentSha256 = sha256(content);\n\t\tconst fileId = sha256(`${this.identity.workspaceId}:${rel}`).slice(0, 32);\n\t\tconst languageId = cls.languageId;\n\t\tconst classification = cls.classification;\n\t\tconst lineCount = content.replace(/\\r\\n/g, \"\\n\").split(\"\\n\").length;\n\n\t\tlet symbolsRaw = languageId ? extractSymbolsHeuristic(languageId, content) : [];\n\t\tif (languageId === \"markdown\" || classification === \"documentation\") symbolsRaw = markdownSections(content);\n\n\t\tconst chunks = chunkFile({\n\t\t\tfileId,\n\t\t\tcontent,\n\t\t\tcontentSha256,\n\t\t\tlanguageId,\n\t\t\tclassification,\n\t\t\tsymbols: symbolsRaw.length ? symbolsRaw : undefined,\n\t\t});\n\n\t\tconst symbols = symbolsRaw.map((s) => ({\n\t\t\tsymbolId: sha256(`${fileId}:${s.qualifiedName ?? s.name}`).slice(0, 32),\n\t\t\tname: s.name,\n\t\t\tqualifiedName: s.qualifiedName,\n\t\t\tkind: s.kind,\n\t\t\tlanguageId: s.languageId,\n\t\t\tstartLine: s.startLine,\n\t\t\tstartCharacter: s.startCharacter,\n\t\t\tendLine: s.endLine,\n\t\t\tendCharacter: s.endCharacter,\n\t\t}));\n\n\t\treturn {\n\t\t\tfileId,\n\t\t\trelPath: rel,\n\t\t\tcontent,\n\t\t\tcontentSha256,\n\t\t\tclassification,\n\t\t\tlanguageId,\n\t\t\tsizeBytes,\n\t\t\tlineCount,\n\t\t\tisSensitive: false,\n\t\t\tchunks,\n\t\t\tsymbols,\n\t\t};\n\t}\n\n\tprivate async writeFilesToGeneration(\n\t\tgenId: string,\n\t\tfiles: FileModel[],\n\t\tembed: EmbeddingBackend,\n\t\tembedEnabled: boolean,\n\t\tsignal?: AbortSignal,\n\t\t_onEvent: (e: string, p?: Record<string, unknown>) => void = () => {},\n\t): Promise<void> {\n\t\t// Git metadata in batches.\n\t\tconst gitMeta = collectGitMetadata(\n\t\t\tthis.identity.canonicalRoot,\n\t\t\tfiles.map((f) => f.relPath),\n\t\t\t{\n\t\t\t\thistoryDepth: this.config.historyDepth,\n\t\t\t},\n\t\t);\n\t\tfor (const f of files)\n\t\t\tawait this.writeFileModel(genId, f, embed, embedEnabled, signal, gitMeta.files.get(f.relPath));\n\t}\n\n\tprivate async writeFileModel(\n\t\tgenId: string,\n\t\tf: FileModel,\n\t\tembed: EmbeddingBackend,\n\t\tembedEnabled: boolean,\n\t\tsignal?: AbortSignal,\n\t\tgitMeta?: GitFileMeta,\n\t): Promise<void> {\n\t\tif (signal?.aborted) throw abortError();\n\t\tthis.db.insertFile(genId, {\n\t\t\tfileId: f.fileId,\n\t\t\tpath: f.relPath,\n\t\t\tlanguageId: f.languageId,\n\t\t\tclassification: f.classification,\n\t\t\tcontentSha256: f.contentSha256,\n\t\t\tsizeBytes: f.sizeBytes,\n\t\t\tlineCount: f.lineCount,\n\t\t\tgitBlobId: gitMeta?.gitBlobId,\n\t\t\tisTracked: gitMeta?.isTracked ?? true,\n\t\t\tisGenerated: f.classification === \"generated\",\n\t\t\tisSensitive: f.isSensitive,\n\t\t\tindexedAt: new Date().toISOString(),\n\t\t});\n\t\tif (gitMeta) {\n\t\t\tthis.db.insertGitMeta(genId, f.fileId, {\n\t\t\t\tlastCommit: gitMeta.lastCommit,\n\t\t\t\tcommitTime: gitMeta.lastCommitTime,\n\t\t\t\tchangeCount: gitMeta.changeCount,\n\t\t\t});\n\t\t}\n\n\t\t// Chunks\n\t\tfor (const chunk of f.chunks) {\n\t\t\tif (signal?.aborted) throw abortError();\n\t\t\tconst sensitiveExcluded = f.isSensitive;\n\t\t\tconst kind = chunk.chunkKind;\n\t\t\tthis.db.insertChunk(genId, {\n\t\t\t\tchunkId: chunk.chunkId,\n\t\t\t\tfileId: f.fileId,\n\t\t\t\tcontentSha256: f.contentSha256,\n\t\t\t\tstartLine: chunk.startLine,\n\t\t\t\tendLine: chunk.endLine,\n\t\t\t\tstartByte: chunk.startByte,\n\t\t\t\tendByte: chunk.endByte,\n\t\t\t\tlanguageId: chunk.languageId,\n\t\t\t\tsymbolId: chunk.symbolId,\n\t\t\t\tchunkKind: kind,\n\t\t\t\ttextHash: chunk.textHash,\n\t\t\t\tembeddingStatus: sensitiveExcluded ? \"excluded\" : \"not_requested\",\n\t\t\t\ttext: chunk.text,\n\t\t\t});\n\n\t\t\t// Lexical index (even excluded text is NOT indexed → secret policy).\n\t\t\tif (!sensitiveExcluded) {\n\t\t\t\tconst lexText = buildLexText(f.relPath, f.languageId, chunk);\n\t\t\t\twriteLexical(this.db, genId, chunk.chunkId, f.relPath, lexText);\n\t\t\t}\n\n\t\t\t// Embedding (skip excluded / generated/vendor / lockfile)\n\t\t\tif (\n\t\t\t\tembedEnabled &&\n\t\t\t\t!sensitiveExcluded &&\n\t\t\t\t!(f.classification === \"generated\") &&\n\t\t\t\tf.classification !== \"lockfile\"\n\t\t\t) {\n\t\t\t\tconst embedText = buildEmbedText(f.relPath, f.languageId, chunk);\n\t\t\t\tconst truncated = truncateForEmbedding(embedText, embed.maximumInputTokens);\n\t\t\t\tconst vector = await embedOne(embed, truncated);\n\t\t\t\tif (vector) {\n\t\t\t\t\tthis.db.db\n\t\t\t\t\t\t.prepare(\"UPDATE chunks SET embedding_status = 'ready' WHERE generation_id = ? AND chunk_id = ?\")\n\t\t\t\t\t\t.run(genId, chunk.chunkId);\n\t\t\t\t\tnew VectorIndex(this.db).store(\n\t\t\t\t\t\tgenId,\n\t\t\t\t\t\tchunk.chunkId,\n\t\t\t\t\t\tsha256(truncated),\n\t\t\t\t\t\tembed.modelId,\n\t\t\t\t\t\tembed.dimensions,\n\t\t\t\t\t\tvector,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Symbols\n\t\tfor (const s of f.symbols) {\n\t\t\tthis.db.insertSymbol(genId, {\n\t\t\t\tsymbolId: s.symbolId,\n\t\t\t\tfileId: f.fileId,\n\t\t\t\tname: s.name,\n\t\t\t\tqualifiedName: s.qualifiedName,\n\t\t\t\tkind: s.kind,\n\t\t\t\tlanguageId: s.languageId,\n\t\t\t\tstartLine: s.startLine,\n\t\t\t\tstartCharacter: s.startCharacter,\n\t\t\t\tendLine: s.endLine,\n\t\t\t\tendCharacter: s.endCharacter,\n\t\t\t\tcontainerSymbolId: undefined,\n\t\t\t});\n\t\t\t// Self/define relation from parser source (low confidence).\n\t\t\tthis.db.insertRelation(genId, {\n\t\t\t\trelationId: `${s.symbolId}:def`,\n\t\t\t\tsourceSymbolId: s.symbolId,\n\t\t\t\trelationType: \"defines\",\n\t\t\t\tsource: \"parser\",\n\t\t\t\tconfidence: 0.5,\n\t\t\t});\n\t\t}\n\t}\n}\n\nfunction buildLexText(rel: string, languageId: string | undefined, chunk: ChunkOutput): string {\n\tconst nameParts = chunk.symbolId ? chunk.symbolId : \"\";\n\treturn [path.posix.basename(rel), rel, languageId ?? \"\", nameParts, chunk.text].join(\" \");\n}\n\nfunction buildEmbedText(rel: string, languageId: string | undefined, chunk: ChunkOutput): string {\n\treturn [\n\t\t`path: ${rel}`,\n\t\t`language: ${languageId ?? \"\"}`,\n\t\tchunk.symbolId ? `symbol: ${chunk.symbolId}` : \"\",\n\t\tchunk.text,\n\t].join(\"\\n\");\n}\n\nasync function embedOne(embed: EmbeddingBackend, text: string): Promise<number[] | undefined> {\n\tif (embed.backendId === \"fixture\") {\n\t\tconst r = embed as unknown as { vectorFor(text: string): number[] };\n\t\treturn r.vectorFor(text);\n\t}\n\t// Async local/remote backends.\n\ttry {\n\t\tconst result = await embed.embed({ texts: [text] });\n\t\treturn result.embeddings[0];\n\t} catch {\n\t\treturn undefined; // embedding failure degrades gracefully\n\t}\n}\n\nfunction throwObject(_code: string): never {\n\tconst err = new Error(\"cancelled\");\n\terr.name = \"AbortError\";\n\tthrow err;\n}\n\nfunction abortError(): Error {\n\tconst e = new Error(\"cancelled\");\n\te.name = \"AbortError\";\n\treturn e;\n}\n"]}