{"version":3,"file":"db-DJ8rk0VM.mjs","names":[],"sources":["../src/memory/schema.ts","../src/memory/db.ts"],"sourcesContent":["/**\n * SQLite DDL for the PAI federation database (federation.db).\n *\n * The federation DB is the cross-project search index — a single SQLite file\n * at ~/.pai/federation.db that holds chunked text from every registered\n * project's memory/ and Notes/ directories.\n *\n * Tables:\n *  - memory_files      — file-level metadata (hash, mtime, size) for change detection\n *  - memory_chunks     — chunked text with line numbers, tier classification, and optional embedding\n *  - memory_fts        — FTS5 virtual table backed by memory_chunks text\n *\n * Vault tables (vault_files, vault_aliases, vault_links, vault_name_index, vault_health)\n * have been migrated to Postgres (docker/init.sql) and are no longer created here.\n *\n * Schema version history:\n *  v1 — initial schema (BM25 search only)\n *  v2 — added embedding BLOB column to memory_chunks (Phase 2.5, vector search)\n *  v3 — added vault tables (now removed — vault tables live in Postgres)\n */\n\nimport type { Database } from \"better-sqlite3\";\n\n/** Current schema version. Bump when adding new columns or tables. */\nexport const SCHEMA_VERSION = 5;\n\nexport const FEDERATION_SCHEMA_SQL = `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS memory_files (\n  project_id   INTEGER NOT NULL,\n  path         TEXT    NOT NULL,\n  source       TEXT    NOT NULL DEFAULT 'memory',\n  tier         TEXT    NOT NULL DEFAULT 'topic',\n  hash         TEXT    NOT NULL,\n  mtime        INTEGER NOT NULL,\n  size         INTEGER NOT NULL,\n  PRIMARY KEY (project_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS memory_chunks (\n  id               TEXT    PRIMARY KEY,\n  project_id       INTEGER NOT NULL,\n  source           TEXT    NOT NULL DEFAULT 'memory',\n  tier             TEXT    NOT NULL DEFAULT 'topic',\n  path             TEXT    NOT NULL,\n  start_line       INTEGER NOT NULL,\n  end_line         INTEGER NOT NULL,\n  hash             TEXT    NOT NULL,\n  text             TEXT    NOT NULL,\n  updated_at       INTEGER NOT NULL,\n  last_accessed_at INTEGER,\n  relevance_score  REAL    DEFAULT 0.5,\n  embedding        BLOB\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(\n  text,\n  id UNINDEXED,\n  project_id UNINDEXED,\n  path UNINDEXED,\n  source UNINDEXED,\n  tier UNINDEXED,\n  start_line UNINDEXED,\n  end_line UNINDEXED\n);\n\nCREATE INDEX IF NOT EXISTS idx_mc_project ON memory_chunks(project_id);\nCREATE INDEX IF NOT EXISTS idx_mc_source  ON memory_chunks(project_id, source);\nCREATE INDEX IF NOT EXISTS idx_mc_tier    ON memory_chunks(tier);\nCREATE INDEX IF NOT EXISTS idx_mf_project ON memory_files(project_id);\n\nCREATE TABLE IF NOT EXISTS kg_entities (\n  entity_id       TEXT    PRIMARY KEY,\n  tenant_id       TEXT    NOT NULL DEFAULT 'default',\n  name            TEXT    NOT NULL,\n  type            TEXT    NOT NULL DEFAULT 'unknown',\n  description     TEXT,\n  first_seen      INTEGER,\n  last_seen       INTEGER,\n  mention_count   INTEGER NOT NULL DEFAULT 1,\n  feedback_weight REAL    NOT NULL DEFAULT 0.5,\n  UNIQUE(tenant_id, entity_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_kge_tenant    ON kg_entities(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_kge_name      ON kg_entities(tenant_id, name);\nCREATE INDEX IF NOT EXISTS idx_kge_type      ON kg_entities(tenant_id, type);\n`;\n\n/**\n * Apply the full federation schema to an open database.\n *\n * Idempotent — all statements use IF NOT EXISTS so calling this on an\n * already-initialised database is safe.\n *\n * Also runs any necessary migrations for existing databases (e.g. adding the\n * embedding column to an older schema that was created without it).\n */\nexport function initializeFederationSchema(db: Database): void {\n  db.exec(FEDERATION_SCHEMA_SQL);\n  runMigrations(db);\n}\n\n// ---------------------------------------------------------------------------\n// Migrations\n// ---------------------------------------------------------------------------\n\n/**\n * Apply incremental migrations to an existing database.\n *\n * Each migration is idempotent — safe to call on a database that has already\n * been migrated.\n */\nfunction runMigrations(db: Database): void {\n  const columns = db.prepare(\"PRAGMA table_info(memory_chunks)\").all() as Array<{\n    name: string;\n  }>;\n\n  // Migration v1→v2: add embedding BLOB column (schema v2, Phase 2.5)\n  const hasEmbedding = columns.some((c) => c.name === \"embedding\");\n  if (!hasEmbedding) {\n    db.exec(\"ALTER TABLE memory_chunks ADD COLUMN embedding BLOB\");\n  }\n\n  // Create the partial index for embedded chunks (safe now that the column exists)\n  db.exec(\n    \"CREATE INDEX IF NOT EXISTS idx_mc_embedding ON memory_chunks(id) WHERE embedding IS NOT NULL\",\n  );\n\n  // Migration v4→v5: add last_accessed_at and relevance_score columns (QW2 + MR2)\n  const hasLastAccessedAt = columns.some((c) => c.name === \"last_accessed_at\");\n  if (!hasLastAccessedAt) {\n    db.exec(\"ALTER TABLE memory_chunks ADD COLUMN last_accessed_at INTEGER\");\n  }\n\n  const hasRelevanceScore = columns.some((c) => c.name === \"relevance_score\");\n  if (!hasRelevanceScore) {\n    db.exec(\"ALTER TABLE memory_chunks ADD COLUMN relevance_score REAL DEFAULT 0.5\");\n  }\n}\n","/**\n * Database connection helper for the PAI federation DB.\n *\n * Uses better-sqlite3 (synchronous API) to open or create federation.db.\n * On first open it runs the full DDL via initializeFederationSchema().\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport BetterSqlite3 from \"better-sqlite3\";\nimport type { Database } from \"better-sqlite3\";\nimport { initializeFederationSchema } from \"./schema.js\";\nimport { paiHomePath, resolvePaiFile } from \"../config/pai-home.js\";\n\nexport type { Database };\n\n/** Old federation.db path, inside the ~/.pai/ directory (pre-2026-09-19). */\nexport function oldFederationPath(): string {\n  return join(homedir(), \".pai\", \"federation.db\");\n}\n\n/** Federation DB path: PAI_HOME/federation.db if present, else the old\n *  ~/.pai/federation.db (one-time stderr notice), else the new path. */\nexport function federationDbPath(): string {\n  return resolvePaiFile(paiHomePath(\"federation.db\"), [oldFederationPath()], \"pai config migrate --federation\");\n}\n\n/**\n * Open (or create) the PAI federation database.\n *\n * @param path  Absolute path to federation.db.  Defaults to PAI_HOME/federation.db\n *              (falling back to the pre-2026-09-19 ~/.pai/federation.db).\n * @returns     An open better-sqlite3 Database instance.\n *\n * Side effects on first call:\n *  - Creates the parent directory if it does not exist.\n *  - Enables WAL journal mode.\n *  - Runs initializeFederationSchema() to ensure tables exist.\n */\nexport function openFederation(path: string = federationDbPath()): Database {\n  // Ensure the directory exists before SQLite tries to create the file\n  mkdirSync(dirname(path), { recursive: true });\n\n  const db = new BetterSqlite3(path);\n\n  // WAL gives better concurrent read performance and crash safety\n  db.pragma(\"journal_mode = WAL\");\n  db.pragma(\"foreign_keys = ON\");\n\n  // Apply schema (idempotent — all statements use IF NOT EXISTS)\n  initializeFederationSchema(db);\n\n  return db;\n}\n"],"mappings":";;;;;;;AA0BA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0ErC,SAAgB,2BAA2B,IAAoB;AAC7D,IAAG,KAAK,sBAAsB;AAC9B,eAAc,GAAG;;;;;;;;AAanB,SAAS,cAAc,IAAoB;CACzC,MAAM,UAAU,GAAG,QAAQ,mCAAmC,CAAC,KAAK;AAMpE,KAAI,CADiB,QAAQ,MAAM,MAAM,EAAE,SAAS,YAAY,CAE9D,IAAG,KAAK,sDAAsD;AAIhE,IAAG,KACD,+FACD;AAID,KAAI,CADsB,QAAQ,MAAM,MAAM,EAAE,SAAS,mBAAmB,CAE1E,IAAG,KAAK,gEAAgE;AAI1E,KAAI,CADsB,QAAQ,MAAM,MAAM,EAAE,SAAS,kBAAkB,CAEzE,IAAG,KAAK,wEAAwE;;;;;;;;;;;;ACzHpF,SAAgB,oBAA4B;AAC1C,QAAO,KAAK,SAAS,EAAE,QAAQ,gBAAgB;;;;AAKjD,SAAgB,mBAA2B;AACzC,QAAO,eAAe,YAAY,gBAAgB,EAAE,CAAC,mBAAmB,CAAC,EAAE,kCAAkC;;;;;;;;;;;;;;AAe/G,SAAgB,eAAe,OAAe,kBAAkB,EAAY;AAE1E,WAAU,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;CAE7C,MAAM,KAAK,IAAI,cAAc,KAAK;AAGlC,IAAG,OAAO,qBAAqB;AAC/B,IAAG,OAAO,oBAAoB;AAG9B,4BAA2B,GAAG;AAE9B,QAAO"}