{"version":3,"file":"db-Ddns80yf.mjs","names":[],"sources":["../src/registry/schema.ts","../src/registry/db.ts"],"sourcesContent":["/**\n * SQLite DDL for the PAI registry database.\n *\n * Tables:\n *  - projects        — tracked project directories with type and status\n *  - sessions        — per-project session notes\n *  - tags            — normalised tag vocabulary\n *  - project_tags    — M:N join between projects and tags\n *  - session_tags    — M:N join between sessions and tags\n *  - aliases         — alternative slugs that resolve to a project\n *  - compaction_log  — audit trail for context-compaction events\n *  - schema_version  — single-row migration version tracking\n */\n\nimport type { Database } from \"better-sqlite3\";\n\nexport const SCHEMA_VERSION = 4;\n\nexport const CREATE_TABLES_SQL = `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS projects (\n  id            INTEGER PRIMARY KEY AUTOINCREMENT,\n  slug          TEXT    NOT NULL UNIQUE,\n  display_name  TEXT    NOT NULL,\n  root_path     TEXT    NOT NULL UNIQUE,\n  encoded_dir   TEXT    NOT NULL UNIQUE,\n  type          TEXT    NOT NULL DEFAULT 'local'\n                        CHECK(type IN ('local','central','obsidian-linked','external')),\n  status        TEXT    NOT NULL DEFAULT 'active'\n                        CHECK(status IN ('active','archived','migrating')),\n  parent_id       INTEGER,\n  obsidian_link   TEXT,\n  claude_notes_dir TEXT,\n  session_config  TEXT,\n  created_at      INTEGER NOT NULL,\n  updated_at      INTEGER NOT NULL,\n  archived_at     INTEGER,\n  FOREIGN KEY (parent_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n  id                INTEGER PRIMARY KEY AUTOINCREMENT,\n  project_id        INTEGER NOT NULL,\n  number            INTEGER NOT NULL,\n  date              TEXT    NOT NULL,\n  slug              TEXT    NOT NULL,\n  title             TEXT    NOT NULL,\n  filename          TEXT    NOT NULL,\n  status            TEXT    NOT NULL DEFAULT 'open'\n                            CHECK(status IN ('open','completed','compacted')),\n  claude_session_id TEXT,\n  token_count       INTEGER,\n  created_at        INTEGER NOT NULL,\n  closed_at         INTEGER,\n  UNIQUE (project_id, number),\n  FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS tags (\n  id   INTEGER PRIMARY KEY AUTOINCREMENT,\n  name TEXT    NOT NULL UNIQUE\n);\n\nCREATE TABLE IF NOT EXISTS project_tags (\n  project_id INTEGER NOT NULL,\n  tag_id     INTEGER NOT NULL,\n  PRIMARY KEY (project_id, tag_id),\n  FOREIGN KEY (project_id) REFERENCES projects(id),\n  FOREIGN KEY (tag_id)     REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS session_tags (\n  session_id INTEGER NOT NULL,\n  tag_id     INTEGER NOT NULL,\n  PRIMARY KEY (session_id, tag_id),\n  FOREIGN KEY (session_id) REFERENCES sessions(id),\n  FOREIGN KEY (tag_id)     REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS aliases (\n  alias      TEXT    PRIMARY KEY,\n  project_id INTEGER NOT NULL,\n  FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS compaction_log (\n  id            INTEGER PRIMARY KEY AUTOINCREMENT,\n  project_id    INTEGER NOT NULL,\n  session_id    INTEGER,\n  trigger       TEXT    NOT NULL\n                        CHECK(trigger IN ('precompact','manual','end-session')),\n  files_written TEXT    NOT NULL,\n  token_count   INTEGER,\n  created_at    INTEGER NOT NULL,\n  FOREIGN KEY (project_id) REFERENCES projects(id),\n  FOREIGN KEY (session_id) REFERENCES sessions(id)\n);\n\nCREATE TABLE IF NOT EXISTS links (\n  id               INTEGER PRIMARY KEY AUTOINCREMENT,\n  session_id       INTEGER NOT NULL,\n  target_project_id INTEGER NOT NULL,\n  link_type        TEXT    NOT NULL DEFAULT 'related'\n                           CHECK(link_type IN ('related','follow-up','reference')),\n  created_at       INTEGER NOT NULL,\n  UNIQUE (session_id, target_project_id),\n  FOREIGN KEY (session_id)        REFERENCES sessions(id),\n  FOREIGN KEY (target_project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS schema_version (\n  version    INTEGER PRIMARY KEY,\n  applied_at INTEGER NOT NULL\n);\n\n-- Indexes\nCREATE INDEX IF NOT EXISTS idx_projects_slug    ON projects(slug);\nCREATE INDEX IF NOT EXISTS idx_projects_status  ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_projects_type    ON projects(type);\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_date    ON sessions(date);\nCREATE INDEX IF NOT EXISTS idx_sessions_status  ON sessions(status);\nCREATE INDEX IF NOT EXISTS idx_sessions_claude  ON sessions(claude_session_id);\nCREATE INDEX IF NOT EXISTS idx_pc_project       ON project_tags(project_id);\n`;\n\n/**\n * Run the full DDL against an open database connection.\n *\n * The function is idempotent — every statement uses IF NOT EXISTS so it is\n * safe to call on an already-initialised database.  After creating the tables\n * it inserts the current SCHEMA_VERSION into schema_version if no row exists\n * yet.\n */\nexport function initializeSchema(db: Database): void {\n  // better-sqlite3's exec() runs multiple semicolon-separated statements\n  db.exec(CREATE_TABLES_SQL);\n\n  const row = db\n    .prepare(\"SELECT version FROM schema_version WHERE version = ?\")\n    .get(SCHEMA_VERSION);\n\n  if (!row) {\n    db.prepare(\n      \"INSERT INTO schema_version (version, applied_at) VALUES (?, ?)\"\n    ).run(SCHEMA_VERSION, Date.now());\n  }\n}\n\n/**\n * Apply incremental schema migrations to an already-initialised database.\n *\n * Each migration is guarded by a version check so it is safe to call on\n * databases at any schema version — already-applied migrations are skipped.\n */\nexport function runMigrations(db: Database): void {\n  const currentRow = db\n    .prepare(\"SELECT version FROM schema_version ORDER BY version DESC LIMIT 1\")\n    .get() as { version: number } | undefined;\n\n  const current = currentRow?.version ?? 0;\n\n  // Migration v1 → v2: add claude_notes_dir column to projects\n  if (current < 2) {\n    db.transaction(() => {\n      // Use a try/catch so re-running on a DB that already has the column is safe\n      try {\n        db.exec(\"ALTER TABLE projects ADD COLUMN claude_notes_dir TEXT\");\n      } catch {\n        // Column may already exist (e.g. fresh DB created with v2 DDL)\n      }\n      db.prepare(\n        \"INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)\"\n      ).run(2, Date.now());\n    })();\n  }\n\n  // Migration v2 → v3: add links table for cross-project session references\n  if (current < 3) {\n    db.transaction(() => {\n      try {\n        db.exec(`\n          CREATE TABLE IF NOT EXISTS links (\n            id               INTEGER PRIMARY KEY AUTOINCREMENT,\n            session_id       INTEGER NOT NULL,\n            target_project_id INTEGER NOT NULL,\n            link_type        TEXT    NOT NULL DEFAULT 'related'\n                                     CHECK(link_type IN ('related','follow-up','reference')),\n            created_at       INTEGER NOT NULL,\n            UNIQUE (session_id, target_project_id),\n            FOREIGN KEY (session_id)        REFERENCES sessions(id),\n            FOREIGN KEY (target_project_id) REFERENCES projects(id)\n          )\n        `);\n      } catch {\n        // Table may already exist (fresh DB created with v3 DDL)\n      }\n      db.prepare(\n        \"INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)\"\n      ).run(3, Date.now());\n    })();\n  }\n\n  // Migration v3 → v4: add session_config column to projects\n  if (current < 4) {\n    db.transaction(() => {\n      try {\n        db.exec(\"ALTER TABLE projects ADD COLUMN session_config TEXT\");\n      } catch {\n        // Column may already exist\n      }\n      db.prepare(\n        \"INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)\"\n      ).run(4, Date.now());\n    })();\n  }\n}\n","/**\n * Database connection helper for the PAI registry.\n *\n * Uses better-sqlite3 (synchronous API) to open or create registry.db.\n * On first open it runs the full DDL via initializeSchema().\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 { initializeSchema, runMigrations } from \"./schema.js\";\nimport { paiHomePath, resolvePaiFile } from \"../config/pai-home.js\";\n\nexport type { Database };\n\n/** Old registry path, inside the ~/.pai/ directory (pre-2026-09-19). */\nexport function oldRegistryPath(): string {\n  return join(homedir(), \".pai\", \"registry.db\");\n}\n\n/** Registry path: PAI_HOME/registry.db if present, else the old\n *  ~/.pai/registry.db (one-time stderr notice), else the new path. */\nexport function registryDbPath(): string {\n  return resolvePaiFile(paiHomePath(\"registry.db\"), [oldRegistryPath()], \"pai config migrate --registry\");\n}\n\n/**\n * Open (or create) the PAI registry database.\n *\n * @param path  Absolute path to registry.db.  Defaults to PAI_HOME/registry.db\n *              (falling back to the pre-2026-09-19 ~/.pai/registry.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 initializeSchema() if schema_version is empty.\n */\nexport function openRegistry(path: string = registryDbPath()): 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  // Check whether the schema has been applied before\n  const tableExists = db\n    .prepare(\n      `SELECT name FROM sqlite_master\n       WHERE type = 'table' AND name = 'schema_version'`\n    )\n    .get();\n\n  if (!tableExists) {\n    // Brand-new database — apply the full schema\n    initializeSchema(db);\n  } else {\n    const row = db\n      .prepare(\"SELECT version FROM schema_version LIMIT 1\")\n      .get() as { version: number } | undefined;\n\n    if (!row) {\n      // Table exists but is empty — apply schema (handles partial init)\n      initializeSchema(db);\n    }\n  }\n\n  // Apply any pending incremental migrations\n  runMigrations(db);\n\n  return db;\n}\n"],"mappings":";;;;;;;AAgBA,MAAa,iBAAiB;AAE9B,MAAa,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsHjC,SAAgB,iBAAiB,IAAoB;AAEnD,IAAG,KAAK,kBAAkB;AAM1B,KAAI,CAJQ,GACT,QAAQ,uDAAuD,CAC/D,IAAI,eAAe,CAGpB,IAAG,QACD,iEACD,CAAC,IAAI,gBAAgB,KAAK,KAAK,CAAC;;;;;;;;AAUrC,SAAgB,cAAc,IAAoB;CAKhD,MAAM,UAJa,GAChB,QAAQ,mEAAmE,CAC3E,KAAK,EAEoB,WAAW;AAGvC,KAAI,UAAU,EACZ,IAAG,kBAAkB;AAEnB,MAAI;AACF,MAAG,KAAK,wDAAwD;UAC1D;AAGR,KAAG,QACD,4EACD,CAAC,IAAI,GAAG,KAAK,KAAK,CAAC;GACpB,EAAE;AAIN,KAAI,UAAU,EACZ,IAAG,kBAAkB;AACnB,MAAI;AACF,MAAG,KAAK;;;;;;;;;;;;UAYN;UACI;AAGR,KAAG,QACD,4EACD,CAAC,IAAI,GAAG,KAAK,KAAK,CAAC;GACpB,EAAE;AAIN,KAAI,UAAU,EACZ,IAAG,kBAAkB;AACnB,MAAI;AACF,MAAG,KAAK,sDAAsD;UACxD;AAGR,KAAG,QACD,4EACD,CAAC,IAAI,GAAG,KAAK,KAAK,CAAC;GACpB,EAAE;;;;;;;;;;;;ACtMR,SAAgB,kBAA0B;AACxC,QAAO,KAAK,SAAS,EAAE,QAAQ,cAAc;;;;AAK/C,SAAgB,iBAAyB;AACvC,QAAO,eAAe,YAAY,cAAc,EAAE,CAAC,iBAAiB,CAAC,EAAE,gCAAgC;;;;;;;;;;;;;;AAezG,SAAgB,aAAa,OAAe,gBAAgB,EAAY;AAEtE,WAAU,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;CAE7C,MAAM,KAAK,IAAI,cAAc,KAAK;AAGlC,IAAG,OAAO,qBAAqB;AAC/B,IAAG,OAAO,oBAAoB;AAU9B,KAAI,CAPgB,GACjB,QACC;yDAED,CACA,KAAK,CAIN,kBAAiB,GAAG;UAMhB,CAJQ,GACT,QAAQ,6CAA6C,CACrD,KAAK,CAIN,kBAAiB,GAAG;AAKxB,eAAc,GAAG;AAEjB,QAAO"}