{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable index storage backed by the bundled Node.js SQLite module.\n *\n * Uses `node:sqlite` (DatabaseSync) — a zero-dependency, synchronous,\n * transactional, cross-platform backend with crash recovery. Lexical search uses\n * FTS5 when the bundled SQLite provides it; otherwise a pure-JS inverted index\n * supplies equivalent semantics.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport { INDEX_SCHEMA_VERSION } from \"./identity.js\";\nimport type { IndexedFileRecord, WorkspaceIndexGeneration } from \"./types.js\";\n\ninterface PreparedQuery {\n\tget(...parameters: unknown[]): unknown;\n\tall(...parameters: unknown[]): unknown[];\n\trun(...parameters: unknown[]): unknown;\n}\n\ninterface DatabaseSyncLike {\n\tprepare(query: string): PreparedQuery;\n\texec(query: string): void;\n\tclose(): void;\n}\n\ntype DatabaseSyncConstructor = new (path: string) => DatabaseSyncLike;\n\nconst require = createRequire(import.meta.url);\n\nfunction loadDatabaseSync(): DatabaseSyncConstructor {\n\ttry {\n\t\tconst sqlite = require(\"node:sqlite\") as { DatabaseSync?: unknown };\n\t\tif (typeof sqlite.DatabaseSync !== \"function\") throw new Error(\"DatabaseSync export is unavailable\");\n\t\treturn sqlite.DatabaseSync as DatabaseSyncConstructor;\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`workspace indexing requires Node.js node:sqlite: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n}\n\nexport interface LexicalRow {\n\trowid: number;\n\tsnippet?: string;\n}\n\n/** Detects FTS5 availability in the current Node SQLite build. */\nexport function detectFts5(db: DatabaseSyncLike): boolean {\n\ttry {\n\t\tdb.exec(\"CREATE VIRTUAL TABLE IF NOT EXISTS __fts_probe USING fts5(c)\");\n\t\tdb.exec(\"DROP TABLE __fts_probe\");\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * A workspace index database handle.\n */\nexport class WorkspaceDb {\n\treadonly db: DatabaseSyncLike;\n\treadonly workspaceId: string;\n\treadonly directory: string;\n\treadonly fts5: boolean;\n\tprivate generationCache = new Map<string, WorkspaceIndexGeneration>();\n\n\tconstructor(directory: string, workspaceId: string, db: DatabaseSyncLike, fts5: boolean) {\n\t\tthis.directory = directory;\n\t\tthis.workspaceId = workspaceId;\n\t\tthis.db = db;\n\t\tthis.fts5 = fts5;\n\t\tthis.migrate();\n\t}\n\n\t/** Open (creating if needed) the workspace DB. */\n\tstatic open(directory: string, workspaceId: string): WorkspaceDb {\n\t\tmkdirSync(directory, { recursive: true });\n\t\tconst dbPath = path.join(directory, \"index.sqlite\");\n\t\tconst DatabaseSync = loadDatabaseSync();\n\t\tconst db = new DatabaseSync(dbPath);\n\t\tconst fts5 = detectFts5(db);\n\t\tdb.exec(\"PRAGMA journal_mode = WAL\");\n\t\tdb.exec(\"PRAGMA synchronous = NORMAL\");\n\t\tconst store = new WorkspaceDb(directory, workspaceId, db, fts5);\n\t\tstore.installSchema(fts5);\n\t\treturn store;\n\t}\n\n\tprivate migrate(): void {\n\t\tconst rows = this.db.prepare(\"PRAGMA user_version\").get() as { user_version: number };\n\t\tconst version = Number(rows.user_version);\n\t\tif (version > INDEX_SCHEMA_VERSION) {\n\t\t\t// Newer schema than this code understands: rebuild path required.\n\t\t\tthis.db.prepare(\"PRAGMA user_version = 0\").get();\n\t\t}\n\t}\n\n\tprivate installSchema(fts5: boolean): void {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);\n\t\t\tCREATE TABLE IF NOT EXISTS generations (\n\t\t\t\tgeneration_id TEXT PRIMARY KEY,\n\t\t\t\tworkspace_id TEXT NOT NULL,\n\t\t\t\tschema_version INTEGER NOT NULL,\n\t\t\t\tcreated_at TEXT NOT NULL,\n\t\t\t\tcompleted_at TEXT,\n\t\t\t\tgit_head TEXT,\n\t\t\t\tworktree_fingerprint TEXT NOT NULL,\n\t\t\t\tfile_manifest_hash TEXT NOT NULL,\n\t\t\t\tstatus TEXT NOT NULL,\n\t\t\t\tfile_count INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tchunk_count INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tsymbol_count INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tembedding_count INTEGER NOT NULL DEFAULT 0\n\t\t\t);\n\t\t\tCREATE TABLE IF NOT EXISTS files (\n\t\t\t\tgeneration_id TEXT NOT NULL,\n\t\t\t\tfile_id TEXT NOT NULL,\n\t\t\t\tpath TEXT NOT NULL,\n\t\t\t\tlanguage_id TEXT,\n\t\t\t\tclassification TEXT NOT NULL,\n\t\t\t\tcontent_sha256 TEXT NOT NULL,\n\t\t\t\tsize_bytes INTEGER NOT NULL,\n\t\t\t\tline_count INTEGER NOT NULL,\n\t\t\t\tgit_blob_id TEXT,\n\t\t\t\tis_tracked INTEGER NOT NULL,\n\t\t\t\tis_generated INTEGER NOT NULL,\n\t\t\t\tis_sensitive INTEGER NOT NULL,\n\t\t\t\tindexed_at TEXT NOT NULL,\n\t\t\t\tPRIMARY KEY (generation_id, file_id)\n\t\t\t);\n\t\t\tCREATE TABLE IF NOT EXISTS chunks (\n\t\t\t\tgeneration_id TEXT NOT NULL,\n\t\t\t\tchunk_id TEXT NOT NULL,\n\t\t\t\tfile_id TEXT NOT NULL,\n\t\t\t\tcontent_sha256 TEXT NOT NULL,\n\t\t\t\tstart_line INTEGER NOT NULL,\n\t\t\t\tend_line INTEGER NOT NULL,\n\t\t\t\tstart_byte INTEGER,\n\t\t\t\tend_byte INTEGER,\n\t\t\t\tlanguage_id TEXT,\n\t\t\t\tsymbol_id TEXT,\n\t\t\t\tchunk_kind TEXT NOT NULL,\n\t\t\t\ttext_hash TEXT NOT NULL,\n\t\t\t\tembedding_status TEXT NOT NULL,\n\t\t\t\ttext TEXT,\n\t\t\t\tPRIMARY KEY (generation_id, chunk_id)\n\t\t\t);\n\t\t\tCREATE TABLE IF NOT EXISTS symbols (\n\t\t\t\tgeneration_id TEXT NOT NULL,\n\t\t\t\tsymbol_id TEXT NOT NULL,\n\t\t\t\tfile_id TEXT NOT NULL,\n\t\t\t\tname TEXT NOT NULL,\n\t\t\t\tqualified_name TEXT,\n\t\t\t\tkind TEXT NOT NULL,\n\t\t\t\tlanguage_id TEXT NOT NULL,\n\t\t\t\tstart_line INTEGER NOT NULL,\n\t\t\t\tstart_character INTEGER NOT NULL,\n\t\t\t\tend_line INTEGER NOT NULL,\n\t\t\t\tend_character INTEGER NOT NULL,\n\t\t\t\tsignature_hash TEXT,\n\t\t\t\tcontainer_symbol_id TEXT,\n\t\t\t\tPRIMARY KEY (generation_id, symbol_id)\n\t\t\t);\n\t\t\tCREATE TABLE IF NOT EXISTS relations (\n\t\t\t\tgeneration_id TEXT NOT NULL,\n\t\t\t\trelation_id TEXT NOT NULL,\n\t\t\t\tsource_symbol_id TEXT NOT NULL,\n\t\t\t\ttarget_symbol_id TEXT,\n\t\t\t\trelation_type TEXT NOT NULL,\n\t\t\t\tsource TEXT NOT NULL,\n\t\t\t\tconfidence REAL NOT NULL,\n\t\t\t\tPRIMARY KEY (generation_id, relation_id)\n\t\t\t);\n\t\t\tCREATE TABLE IF NOT EXISTS git_meta (\n\t\t\t\tgeneration_id TEXT NOT NULL,\n\t\t\t\tfile_id TEXT NOT NULL,\n\t\t\t\tlast_commit TEXT,\n\t\t\t\tcommit_time TEXT,\n\t\t\t\tchange_count INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tPRIMARY KEY (generation_id, file_id)\n\t\t\t);\n\t\t\tCREATE TABLE IF NOT EXISTS vectors (\n\t\t\t\tgeneration_id TEXT NOT NULL,\n\t\t\t\tchunk_id TEXT NOT NULL,\n\t\t\t\tcontent_hash TEXT NOT NULL,\n\t\t\t\tmodel_id TEXT NOT NULL,\n\t\t\t\tdimensions INTEGER NOT NULL,\n\t\t\t\tembedding BLOB NOT NULL,\n\t\t\t\tPRIMARY KEY (generation_id, chunk_id, model_id)\n\t\t\t);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks (generation_id, file_id);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols (generation_id, name);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_files_path ON files (generation_id, path);\n\t\t\tCREATE TABLE IF NOT EXISTS postings (\n\t\t\t\tgeneration_id TEXT NOT NULL,\n\t\t\t\ttoken TEXT NOT NULL,\n\t\t\t\tchunk_id TEXT NOT NULL\n\t\t\t);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_postings_token ON postings (generation_id, token);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_postings_chunk ON postings (generation_id, chunk_id);\n\t\t\tCREATE TABLE IF NOT EXISTS lex (\n\t\t\t\tgeneration_id TEXT NOT NULL,\n\t\t\t\tchunk_id TEXT NOT NULL,\n\t\t\t\tpath TEXT NOT NULL,\n\t\t\t\tdoc_tokens TEXT NOT NULL,\n\t\t\t\tdoc_length INTEGER NOT NULL,\n\t\t\t\tPRIMARY KEY (generation_id, chunk_id)\n\t\t\t);\n\t\t`);\n\t\tif (fts5) {\n\t\t\tthis.db.exec(`\n\t\t\t\tCREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n\t\t\t\t\tchunk_id UNINDEXED,\n\t\t\t\t\ttext,\n\t\t\t\t\tpath UNINDEXED,\n\t\t\t\t\ttokenize='unicode61'\n\t\t\t\t);\n\t\t\t`);\n\t\t}\n\t}\n\n\t// ---- Generations ----\n\n\tinsertGeneration(gen: WorkspaceIndexGeneration): void {\n\t\tthis.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT OR REPLACE INTO generations (\n\t\t\t\t\tgeneration_id, workspace_id, schema_version, created_at, completed_at,\n\t\t\t\t\tgit_head, worktree_fingerprint, file_manifest_hash, status,\n\t\t\t\t\tfile_count, chunk_count, symbol_count, embedding_count\n\t\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t\t\t)\n\t\t\t.run(\n\t\t\t\tgen.generationId,\n\t\t\t\tgen.workspaceId,\n\t\t\t\tgen.schemaVersion,\n\t\t\t\tgen.createdAt,\n\t\t\t\tgen.completedAt ?? null,\n\t\t\t\tgen.sourceSnapshot.gitHead ?? null,\n\t\t\t\tgen.sourceSnapshot.worktreeFingerprint,\n\t\t\t\tgen.sourceSnapshot.fileManifestHash,\n\t\t\t\tgen.status,\n\t\t\t\tgen.fileCount,\n\t\t\t\tgen.chunkCount,\n\t\t\t\tgen.symbolCount,\n\t\t\t\tgen.embeddingCount,\n\t\t\t);\n\t\tthis.generationCache.set(gen.generationId, gen);\n\t}\n\n\tupdateGenerationStatus(\n\t\tgenerationId: string,\n\t\tstatus: WorkspaceIndexGeneration[\"status\"],\n\t\tcompletedAt?: string,\n\t): void {\n\t\tthis.db\n\t\t\t.prepare(\"UPDATE generations SET status = ?, completed_at = ? WHERE generation_id = ?\")\n\t\t\t.run(status, completedAt ?? null, generationId);\n\t\tthis.generationCache.delete(generationId);\n\t}\n\n\tgetGeneration(generationId: string): WorkspaceIndexGeneration | undefined {\n\t\tconst cached = this.generationCache.get(generationId);\n\t\tif (cached) return cached;\n\t\tconst row = this.db.prepare(\"SELECT * FROM generations WHERE generation_id = ?\").get(generationId) as\n\t\t\t| Record<string, unknown>\n\t\t\t| undefined;\n\t\tif (!row) return undefined;\n\t\tconst gen = this.rowToGeneration(row);\n\t\tthis.generationCache.set(generationId, gen);\n\t\treturn gen;\n\t}\n\n\t/** Most recent generation with status ready (validation authority). */\n\tcurrentReadyGeneration(): WorkspaceIndexGeneration | undefined {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT * FROM generations WHERE status = 'ready' ORDER BY created_at DESC LIMIT 1\")\n\t\t\t.get() as Record<string, unknown> | undefined;\n\t\treturn row ? this.rowToGeneration(row) : undefined;\n\t}\n\n\tcurrentBuildingGeneration(): WorkspaceIndexGeneration | undefined {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT * FROM generations WHERE status = 'building' ORDER BY created_at DESC LIMIT 1\")\n\t\t\t.get() as Record<string, unknown> | undefined;\n\t\treturn row ? this.rowToGeneration(row) : undefined;\n\t}\n\n\tlistGenerations(): WorkspaceIndexGeneration[] {\n\t\tconst rows = this.db.prepare(\"SELECT * FROM generations ORDER BY created_at DESC\").all() as Array<\n\t\t\tRecord<string, unknown>\n\t\t>;\n\t\treturn rows.map((r) => this.rowToGeneration(r));\n\t}\n\n\tprivate rowToGeneration(row: Record<string, unknown>): WorkspaceIndexGeneration {\n\t\treturn {\n\t\t\tgenerationId: String(row.generation_id),\n\t\t\tworkspaceId: String(row.workspace_id),\n\t\t\tschemaVersion: Number(row.schema_version),\n\t\t\tcreatedAt: String(row.created_at),\n\t\t\tcompletedAt: row.completed_at ? String(row.completed_at) : undefined,\n\t\t\tsourceSnapshot: {\n\t\t\t\tgitHead: row.git_head ? String(row.git_head) : undefined,\n\t\t\t\tworktreeFingerprint: String(row.worktree_fingerprint),\n\t\t\t\tfileManifestHash: String(row.file_manifest_hash),\n\t\t\t},\n\t\t\tstatus: row.status as WorkspaceIndexGeneration[\"status\"],\n\t\t\tfileCount: Number(row.file_count),\n\t\t\tchunkCount: Number(row.chunk_count),\n\t\t\tsymbolCount: Number(row.symbol_count),\n\t\t\tembeddingCount: Number(row.embedding_count),\n\t\t};\n\t}\n\n\t// ---- File / chunk / symbol writes ----\n\n\tinsertFile(\n\t\tgen: string,\n\t\tf: {\n\t\t\tfileId: string;\n\t\t\tpath: string;\n\t\t\tlanguageId?: string;\n\t\t\tclassification: string;\n\t\t\tcontentSha256: string;\n\t\t\tsizeBytes: number;\n\t\t\tlineCount: number;\n\t\t\tgitBlobId?: string;\n\t\t\tisTracked: boolean;\n\t\t\tisGenerated: boolean;\n\t\t\tisSensitive: boolean;\n\t\t\tindexedAt: string;\n\t\t},\n\t): void {\n\t\tthis.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT OR REPLACE INTO files (\n\t\t\t\t\tgeneration_id, file_id, path, language_id, classification, content_sha256,\n\t\t\t\t\tsize_bytes, line_count, git_blob_id, is_tracked, is_generated, is_sensitive, indexed_at\n\t\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t\t\t)\n\t\t\t.run(\n\t\t\t\tgen,\n\t\t\t\tf.fileId,\n\t\t\t\tf.path,\n\t\t\t\tf.languageId ?? null,\n\t\t\t\tf.classification,\n\t\t\t\tf.contentSha256,\n\t\t\t\tf.sizeBytes,\n\t\t\t\tf.lineCount,\n\t\t\t\tf.gitBlobId ?? null,\n\t\t\t\tf.isTracked ? 1 : 0,\n\t\t\t\tf.isGenerated ? 1 : 0,\n\t\t\t\tf.isSensitive ? 1 : 0,\n\t\t\t\tf.indexedAt,\n\t\t\t);\n\t}\n\n\tinsertChunk(\n\t\tgen: string,\n\t\tc: {\n\t\t\tchunkId: string;\n\t\t\tfileId: string;\n\t\t\tcontentSha256: string;\n\t\t\tstartLine: number;\n\t\t\tendLine: number;\n\t\t\tstartByte?: number;\n\t\t\tendByte?: number;\n\t\t\tlanguageId?: string;\n\t\t\tsymbolId?: string;\n\t\t\tchunkKind: string;\n\t\t\ttextHash: string;\n\t\t\tembeddingStatus: string;\n\t\t\ttext?: string;\n\t\t},\n\t): void {\n\t\tthis.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT OR REPLACE INTO chunks (\n\t\t\t\t\tgeneration_id, chunk_id, file_id, content_sha256, start_line, end_line,\n\t\t\t\t\tstart_byte, end_byte, language_id, symbol_id, chunk_kind, text_hash, embedding_status, text\n\t\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t\t\t)\n\t\t\t.run(\n\t\t\t\tgen,\n\t\t\t\tc.chunkId,\n\t\t\t\tc.fileId,\n\t\t\t\tc.contentSha256,\n\t\t\t\tc.startLine,\n\t\t\t\tc.endLine,\n\t\t\t\tc.startByte ?? null,\n\t\t\t\tc.endByte ?? null,\n\t\t\t\tc.languageId ?? null,\n\t\t\t\tc.symbolId ?? null,\n\t\t\t\tc.chunkKind,\n\t\t\t\tc.textHash,\n\t\t\t\tc.embeddingStatus,\n\t\t\t\tc.text ?? null,\n\t\t\t);\n\t}\n\n\tinsertSymbol(\n\t\tgen: string,\n\t\ts: {\n\t\t\tsymbolId: string;\n\t\t\tfileId: string;\n\t\t\tname: string;\n\t\t\tqualifiedName?: string;\n\t\t\tkind: string;\n\t\t\tlanguageId: string;\n\t\t\tstartLine: number;\n\t\t\tstartCharacter: number;\n\t\t\tendLine: number;\n\t\t\tendCharacter: number;\n\t\t\tsignatureHash?: string;\n\t\t\tcontainerSymbolId?: string;\n\t\t},\n\t): void {\n\t\tthis.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT OR REPLACE INTO symbols (\n\t\t\t\t\tgeneration_id, symbol_id, file_id, name, qualified_name, kind, language_id,\n\t\t\t\t\tstart_line, start_character, end_line, end_character, signature_hash, container_symbol_id\n\t\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t\t\t)\n\t\t\t.run(\n\t\t\t\tgen,\n\t\t\t\ts.symbolId,\n\t\t\t\ts.fileId,\n\t\t\t\ts.name,\n\t\t\t\ts.qualifiedName ?? null,\n\t\t\t\ts.kind,\n\t\t\t\ts.languageId,\n\t\t\t\ts.startLine,\n\t\t\t\ts.startCharacter,\n\t\t\t\ts.endLine,\n\t\t\t\ts.endCharacter,\n\t\t\t\ts.signatureHash ?? null,\n\t\t\t\ts.containerSymbolId ?? null,\n\t\t\t);\n\t}\n\n\tinsertRelation(\n\t\tgen: string,\n\t\tr: {\n\t\t\trelationId: string;\n\t\t\tsourceSymbolId: string;\n\t\t\ttargetSymbolId?: string;\n\t\t\trelationType: string;\n\t\t\tsource: string;\n\t\t\tconfidence: number;\n\t\t},\n\t): void {\n\t\tthis.db\n\t\t\t.prepare(\n\t\t\t\t\"INSERT OR REPLACE INTO relations (generation_id, relation_id, source_symbol_id, target_symbol_id, relation_type, source, confidence) VALUES (?, ?, ?, ?, ?, ?, ?)\",\n\t\t\t)\n\t\t\t.run(gen, r.relationId, r.sourceSymbolId, r.targetSymbolId ?? null, r.relationType, r.source, r.confidence);\n\t}\n\n\tinsertGitMeta(\n\t\tgen: string,\n\t\tfileId: string,\n\t\tg: { lastCommit?: string; commitTime?: string; changeCount: number },\n\t): void {\n\t\tthis.db\n\t\t\t.prepare(\n\t\t\t\t\"INSERT OR REPLACE INTO git_meta (generation_id, file_id, last_commit, commit_time, change_count) VALUES (?, ?, ?, ?, ?)\",\n\t\t\t)\n\t\t\t.run(gen, fileId, g.lastCommit ?? null, g.commitTime ?? null, g.changeCount);\n\t}\n\n\tgetFile(gen: string, fileId: string): IndexedFileRecord | undefined {\n\t\tconst row = this.db.prepare(\"SELECT * FROM files WHERE generation_id = ? AND file_id = ?\").get(gen, fileId) as\n\t\t\t| Record<string, unknown>\n\t\t\t| undefined;\n\t\treturn row\n\t\t\t? {\n\t\t\t\t\tfileId: String(row.file_id),\n\t\t\t\t\tworkspaceRelativePath: String(row.path),\n\t\t\t\t\tlanguageId: row.language_id ? String(row.language_id) : undefined,\n\t\t\t\t\tclassification: String(row.classification),\n\t\t\t\t\tcontentSha256: String(row.content_sha256),\n\t\t\t\t\tsizeBytes: Number(row.size_bytes),\n\t\t\t\t\tlineCount: Number(row.line_count),\n\t\t\t\t\tgitBlobId: row.git_blob_id ? String(row.git_blob_id) : undefined,\n\t\t\t\t\tisTracked: Boolean(row.is_tracked),\n\t\t\t\t\tisGenerated: Boolean(row.is_generated),\n\t\t\t\t\tisSensitive: Boolean(row.is_sensitive),\n\t\t\t\t\tindexedAt: String(row.indexed_at),\n\t\t\t\t}\n\t\t\t: undefined;\n\t}\n\n\t/** Map of path → contentSha256 for the given generation. */\n\tfileManifest(gen: string): Map<string, string> {\n\t\tconst rows = this.db.prepare(\"SELECT path, content_sha256 FROM files WHERE generation_id = ?\").all(gen) as Array<{\n\t\t\tpath: string;\n\t\t\tcontent_sha256: string;\n\t\t}>;\n\t\treturn new Map(rows.map((r) => [r.path, r.content_sha256]));\n\t}\n\n\tfileIdByPath(gen: string, relPath: string): string | undefined {\n\t\tconst row = this.db.prepare(\"SELECT file_id FROM files WHERE generation_id = ? AND path = ?\").get(gen, relPath) as\n\t\t\t| { file_id: string }\n\t\t\t| undefined;\n\t\treturn row?.file_id;\n\t}\n\n\tchunkById(\n\t\tgen: string,\n\t\tchunkId: string,\n\t):\n\t\t| {\n\t\t\t\tchunkId: string;\n\t\t\t\tfileId: string;\n\t\t\t\tstartLine: number;\n\t\t\t\tendLine: number;\n\t\t\t\ttext?: string;\n\t\t\t\tsymbolId?: string;\n\t\t\t\tchunkKind: string;\n\t\t  }\n\t\t| undefined {\n\t\tconst row = this.db.prepare(\"SELECT * FROM chunks WHERE generation_id = ? AND chunk_id = ?\").get(gen, chunkId) as\n\t\t\t| Record<string, unknown>\n\t\t\t| undefined;\n\t\treturn row\n\t\t\t? {\n\t\t\t\t\tchunkId: String(row.chunk_id),\n\t\t\t\t\tfileId: String(row.file_id),\n\t\t\t\t\tstartLine: Number(row.start_line),\n\t\t\t\t\tendLine: Number(row.end_line),\n\t\t\t\t\ttext: row.text ? String(row.text) : undefined,\n\t\t\t\t\tsymbolId: row.symbol_id ? String(row.symbol_id) : undefined,\n\t\t\t\t\tchunkKind: String(row.chunk_kind),\n\t\t\t\t}\n\t\t\t: undefined;\n\t}\n\n\tchunksForFile(\n\t\tgen: string,\n\t\tfileId: string,\n\t): Array<{ chunkId: string; startLine: number; endLine: number; text?: string }> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT chunk_id, start_line, end_line, text FROM chunks WHERE generation_id = ? AND file_id = ? ORDER BY start_line\",\n\t\t\t)\n\t\t\t.all(gen, fileId) as Array<{ chunk_id: string; start_line: number; end_line: number; text: string | null }>;\n\t\treturn rows.map((r) => ({\n\t\t\tchunkId: r.chunk_id,\n\t\t\tstartLine: r.start_line,\n\t\t\tendLine: r.end_line,\n\t\t\ttext: r.text ?? undefined,\n\t\t}));\n\t}\n\n\tsymbolsForFile(\n\t\tgen: string,\n\t\tfileId: string,\n\t): Array<{ symbolId: string; name: string; kind: string; startLine: number; endLine: number }> {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT symbol_id, name, kind, start_line, end_line FROM symbols WHERE generation_id = ? AND file_id = ? ORDER BY start_line\",\n\t\t\t)\n\t\t\t.all(gen, fileId) as Array<{\n\t\t\tsymbol_id: string;\n\t\t\tname: string;\n\t\t\tkind: string;\n\t\t\tstart_line: number;\n\t\t\tend_line: number;\n\t\t}>;\n\t\treturn rows.map((r) => ({\n\t\t\tsymbolId: r.symbol_id,\n\t\t\tname: r.name,\n\t\t\tkind: r.kind,\n\t\t\tstartLine: r.start_line,\n\t\t\tendLine: r.end_line,\n\t\t}));\n\t}\n\n\t// ---- Counts (only ready generation is authoritative) ----\n\n\tcounts(generationId: string): { files: number; chunks: number; symbols: number; embeddings: number } {\n\t\tconst files = (\n\t\t\tthis.db.prepare(\"SELECT COUNT(*) c FROM files WHERE generation_id = ?\").get(generationId) as {\n\t\t\t\tc: number;\n\t\t\t}\n\t\t).c;\n\t\tconst chunks = (\n\t\t\tthis.db.prepare(\"SELECT COUNT(*) c FROM chunks WHERE generation_id = ?\").get(generationId) as {\n\t\t\t\tc: number;\n\t\t\t}\n\t\t).c;\n\t\tconst symbols = (\n\t\t\tthis.db.prepare(\"SELECT COUNT(*) c FROM symbols WHERE generation_id = ?\").get(generationId) as {\n\t\t\t\tc: number;\n\t\t\t}\n\t\t).c;\n\t\tconst embeddings = (\n\t\t\tthis.db.prepare(\"SELECT COUNT(*) c FROM vectors WHERE generation_id = ?\").get(generationId) as {\n\t\t\t\tc: number;\n\t\t\t}\n\t\t).c;\n\t\treturn { files, chunks, symbols, embeddings };\n\t}\n\n\t// ---- Transactions ----\n\n\tbegin(): void {\n\t\tthis.db.exec(\"BEGIN\");\n\t}\n\tcommit(): void {\n\t\tthis.db.exec(\"COMMIT\");\n\t}\n\trollback(): void {\n\t\tthis.db.exec(\"ROLLBACK\");\n\t}\n\n\tclose(): void {\n\t\tthis.db.close();\n\t}\n\n\tgetMeta(key: string): string | undefined {\n\t\tconst row = this.db.prepare(\"SELECT value FROM meta WHERE key = ?\").get(key) as { value: string } | undefined;\n\t\treturn row?.value;\n\t}\n\n\tsetMeta(key: string, value: string): void {\n\t\tthis.db.prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)\").run(key, value);\n\t}\n\n\t/** Compute a stable manifest hash over the ready generation's file set. */\n\tfileManifestHash(generationId: string): string {\n\t\tconst rows = this.db\n\t\t\t.prepare(\"SELECT path, content_sha256 FROM files WHERE generation_id = ? ORDER BY path\")\n\t\t\t.all(generationId) as Array<{ path: string; content_sha256: string }>;\n\t\tconst hash = createHash(\"sha256\");\n\t\tfor (const r of rows) hash.update(`${r.path}\\u0000${r.content_sha256}\\u0000`);\n\t\treturn hash.digest(\"hex\");\n\t}\n}\n\nexport function newGenerationId(): string {\n\treturn `gen_${randomUUID().replace(/-/g, \"\")}`;\n}\n"]}