// engine/small-state-store.js — SQL-backed implementations for the
// four Phase 7 small state files:
//
//   schedule-runs.json      <-> schedule_runs       (object → row-per-id)
//   pipeline-runs.json      <-> pipeline_runs       ({id:[runs]} → row-per-run)
//   managed-processes.json  <-> managed_processes   ({specs:[]} → row-per-name)
//   worktree-pool.json      <-> worktree_pool       ({entries:[]} → row-per-entry)
//
// They share enough infrastructure (content-hash fingerprint, mirror
// writer, withTransaction wiring) that it's clearer to keep them in one
// module — each store is small, and the JSON-shape transformations are
// the only per-file divergence.

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

function _toMs(v) {
  if (v == null) return null;
  if (typeof v === 'number') return Number.isFinite(v) ? v : null;
  const parsed = Date.parse(v);
  return Number.isFinite(parsed) ? parsed : null;
}

function _fileContentHash(filePath) {
  try {
    const buf = fs.readFileSync(filePath);
    return crypto.createHash('sha1').update(buf).digest('hex');
  }
  catch { return null; }
}

function _readJson(filePath) {
  let raw;
  try { raw = fs.readFileSync(filePath, 'utf8'); }
  catch { return null; }
  try { return JSON.parse(raw); }
  catch (e) {
    try {
      // eslint-disable-next-line no-console
      console.warn(`[small-state-store] corrupt JSON in ${filePath}: ${e.message}`);
    } catch { /* console wrapped */ }
    return null;
  }
}

function _resolveFilePath(rel) {
  const shared = require('./shared');
  return path.join(shared.MINIONS_DIR, 'engine', rel);
}

// ─── schedule_runs ──────────────────────────────────────────────────────────
// Shape: { [scheduleId]: { lastRun, lastWorkItemId, lastResult, lastCompletedAt } }
// SQL:   row per scheduleId, data = the value blob.

let _scheduleRunsHash = null;

function _hydrateScheduleRuns(db) {
  const fp = _resolveFilePath('schedule-runs.json');
  const raw = _readJson(fp) || {};
  db.prepare('DELETE FROM schedule_runs').run();
  const now = Date.now();
  const ins = db.prepare('INSERT INTO schedule_runs (schedule_id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(schedule_id) DO NOTHING');
  for (const [id, value] of Object.entries(raw)) {
    ins.run(id, JSON.stringify(value), now);
  }
}

function _resyncScheduleRunsIfDiverged(db) {
  const fp = _resolveFilePath('schedule-runs.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_scheduleRunsHash != null && currentHash === _scheduleRunsHash) return;
  if (_scheduleRunsHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM schedule_runs LIMIT 1').get();
    if (sqlHas) {
      _scheduleRunsHash = currentHash;
      return;
    }
  }
  _hydrateScheduleRuns(db);
  _scheduleRunsHash = currentHash;
}

function _readScheduleRunsFromSql(db) {
  const rows = db.prepare('SELECT schedule_id, data FROM schedule_runs').all();
  const out = {};
  for (const row of rows) {
    try { out[row.schedule_id] = JSON.parse(row.data); }
    catch { /* skip malformed */ }
  }
  return out;
}

function readScheduleRuns() {
  const { getDb } = require('./db');
  const db = getDb();
  _resyncScheduleRunsIfDiverged(db);
  const out = _readScheduleRunsFromSql(db);
  if (Object.keys(out).length === 0) {
    const fallback = _readJson(_resolveFilePath('schedule-runs.json'));
    if (fallback && Object.keys(fallback).length > 0) return fallback;
    return {};
  }
  return out;
}

function applyScheduleRunsMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncScheduleRunsIfDiverged(db);
    const before = _readScheduleRunsFromSql(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null) ? before : next;
    const afterIds = new Set(Object.keys(after));
    const beforeIds = new Set(Object.keys(beforeSnap));
    let wrote = false;
    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO schedule_runs (schedule_id, data, updated_at)
      VALUES (?, ?, ?)
      ON CONFLICT(schedule_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
    `);
    const del = db.prepare('DELETE FROM schedule_runs WHERE schedule_id = ?');
    for (const id of afterIds) {
      if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
        upsert.run(id, JSON.stringify(after[id]), now);
        wrote = true;
      }
    }
    for (const id of beforeIds) {
      if (!afterIds.has(id)) { del.run(id); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorScheduleRunsJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const obj = _readScheduleRunsFromSql(getDb());
    const target = filePath || _resolveFilePath('schedule-runs.json');
    shared.safeWrite(target, obj);
    const h = _fileContentHash(target);
    if (h != null) _scheduleRunsHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── pipeline_runs ──────────────────────────────────────────────────────────
// Shape: { [pipelineId]: [run, run, ...] }
// SQL:   row per (pipelineId, runId). Reconstruct on read by grouping.

let _pipelineRunsHash = null;

function _hydratePipelineRuns(db) {
  const fp = _resolveFilePath('pipeline-runs.json');
  const raw = _readJson(fp) || {};
  db.prepare('DELETE FROM pipeline_runs').run();
  const now = Date.now();
  const ins = db.prepare(`
    INSERT INTO pipeline_runs (pipeline_id, run_id, status, started_at, data, updated_at)
    VALUES (?, ?, ?, ?, ?, ?)
    ON CONFLICT(pipeline_id, run_id) DO NOTHING
  `);
  for (const [pipelineId, runs] of Object.entries(raw)) {
    if (!Array.isArray(runs)) continue;
    for (const run of runs) {
      if (!run || !run.runId) continue;
      ins.run(pipelineId, String(run.runId), run.status || null, _toMs(run.startedAt), JSON.stringify(run), now);
    }
  }
}

function _resyncPipelineRunsIfDiverged(db) {
  const fp = _resolveFilePath('pipeline-runs.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_pipelineRunsHash != null && currentHash === _pipelineRunsHash) return;
  if (_pipelineRunsHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM pipeline_runs LIMIT 1').get();
    if (sqlHas) { _pipelineRunsHash = currentHash; return; }
  }
  _hydratePipelineRuns(db);
  _pipelineRunsHash = currentHash;
}

function _readPipelineRunsFromSql(db) {
  const rows = db.prepare('SELECT pipeline_id, data FROM pipeline_runs ORDER BY pipeline_id, rowid').all();
  const out = {};
  for (const row of rows) {
    try {
      const run = JSON.parse(row.data);
      if (!out[row.pipeline_id]) out[row.pipeline_id] = [];
      out[row.pipeline_id].push(run);
    } catch { /* skip malformed */ }
  }
  return out;
}

function readPipelineRuns() {
  const { getDb } = require('./db');
  const db = getDb();
  _resyncPipelineRunsIfDiverged(db);
  const out = _readPipelineRunsFromSql(db);
  if (Object.keys(out).length === 0) {
    const fallback = _readJson(_resolveFilePath('pipeline-runs.json'));
    if (fallback && Object.keys(fallback).length > 0) return fallback;
    return {};
  }
  return out;
}

function applyPipelineRunsMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncPipelineRunsIfDiverged(db);
    const before = _readPipelineRunsFromSql(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null) ? before : next;

    // Index by (pipelineId, runId).
    const flatten = (obj) => {
      const out = new Map();
      for (const [pid, runs] of Object.entries(obj || {})) {
        if (!Array.isArray(runs)) continue;
        for (const run of runs) {
          if (!run || !run.runId) continue;
          out.set(`${pid}|${run.runId}`, { pid, run });
        }
      }
      return out;
    };
    const beforeMap = flatten(beforeSnap);
    const afterMap = flatten(after);

    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO pipeline_runs (pipeline_id, run_id, status, started_at, data, updated_at)
      VALUES (?, ?, ?, ?, ?, ?)
      ON CONFLICT(pipeline_id, run_id) DO UPDATE SET
        status = excluded.status,
        started_at = excluded.started_at,
        data = excluded.data,
        updated_at = excluded.updated_at
    `);
    const del = db.prepare('DELETE FROM pipeline_runs WHERE pipeline_id = ? AND run_id = ?');
    let wrote = false;
    for (const [key, { pid, run }] of afterMap) {
      const prev = beforeMap.get(key);
      if (!prev || JSON.stringify(prev.run) !== JSON.stringify(run)) {
        upsert.run(pid, String(run.runId), run.status || null, _toMs(run.startedAt), JSON.stringify(run), now);
        wrote = true;
      }
    }
    for (const [key, { pid, run }] of beforeMap) {
      if (!afterMap.has(key)) { del.run(pid, String(run.runId)); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorPipelineRunsJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const obj = _readPipelineRunsFromSql(getDb());
    const target = filePath || _resolveFilePath('pipeline-runs.json');
    shared.safeWrite(target, obj);
    const h = _fileContentHash(target);
    if (h != null) _pipelineRunsHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── managed_processes ─────────────────────────────────────────────────────
// Shape: { specs: [ {name, ...}, ... ] }
// SQL:   row per name.

let _managedProcessesHash = null;

function _hydrateManagedProcesses(db) {
  const fp = _resolveFilePath('managed-processes.json');
  const raw = _readJson(fp) || {};
  db.prepare('DELETE FROM managed_processes').run();
  const specs = Array.isArray(raw.specs) ? raw.specs : [];
  const now = Date.now();
  const ins = db.prepare('INSERT INTO managed_processes (name, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(name) DO NOTHING');
  for (const spec of specs) {
    if (!spec || !spec.name) continue;
    ins.run(String(spec.name), JSON.stringify(spec), now);
  }
}

function _resyncManagedProcessesIfDiverged(db) {
  const fp = _resolveFilePath('managed-processes.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_managedProcessesHash != null && currentHash === _managedProcessesHash) return;
  if (_managedProcessesHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM managed_processes LIMIT 1').get();
    if (sqlHas) { _managedProcessesHash = currentHash; return; }
  }
  _hydrateManagedProcesses(db);
  _managedProcessesHash = currentHash;
}

function _readManagedProcessesFromSql(db) {
  const rows = db.prepare('SELECT data FROM managed_processes ORDER BY rowid').all();
  const specs = [];
  for (const row of rows) {
    try { specs.push(JSON.parse(row.data)); } catch { /* skip */ }
  }
  return { specs };
}

function readManagedProcesses() {
  const { getDb } = require('./db');
  const db = getDb();
  _resyncManagedProcessesIfDiverged(db);
  const out = _readManagedProcessesFromSql(db);
  if (out.specs.length === 0) {
    const fallback = _readJson(_resolveFilePath('managed-processes.json'));
    if (fallback && Array.isArray(fallback.specs) && fallback.specs.length > 0) return fallback;
    return { specs: [] };
  }
  return out;
}

function applyManagedProcessesMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncManagedProcessesIfDiverged(db);
    const before = _readManagedProcessesFromSql(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null) ? before : next;
    if (!after || !Array.isArray(after.specs)) after.specs = [];

    const indexByName = (arr) => {
      const out = new Map();
      for (const spec of arr) {
        if (spec && spec.name) out.set(String(spec.name), spec);
      }
      return out;
    };
    const beforeMap = indexByName(beforeSnap.specs);
    const afterMap = indexByName(after.specs);

    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO managed_processes (name, data, updated_at)
      VALUES (?, ?, ?)
      ON CONFLICT(name) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
    `);
    const del = db.prepare('DELETE FROM managed_processes WHERE name = ?');
    let wrote = false;
    for (const [name, spec] of afterMap) {
      const prev = beforeMap.get(name);
      if (!prev || JSON.stringify(prev) !== JSON.stringify(spec)) {
        upsert.run(name, JSON.stringify(spec), now);
        wrote = true;
      }
    }
    for (const [name] of beforeMap) {
      if (!afterMap.has(name)) { del.run(name); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorManagedProcessesJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const obj = _readManagedProcessesFromSql(getDb());
    const target = filePath || _resolveFilePath('managed-processes.json');
    shared.safeWrite(target, obj);
    const h = _fileContentHash(target);
    if (h != null) _managedProcessesHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── worktree_pool ─────────────────────────────────────────────────────────
// Shape: { entries: [ {path, ...}, ... ] }
// SQL:   row per entry, keyed by the worktree path (unique).

let _worktreePoolHash = null;

function _entryKey(entry) {
  if (!entry || typeof entry !== 'object') return null;
  return entry.path || entry.id || null;
}

function _hydrateWorktreePool(db) {
  const fp = _resolveFilePath('worktree-pool.json');
  const raw = _readJson(fp) || {};
  db.prepare('DELETE FROM worktree_pool').run();
  const entries = Array.isArray(raw.entries) ? raw.entries : [];
  const now = Date.now();
  const ins = db.prepare('INSERT INTO worktree_pool (entry_id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(entry_id) DO NOTHING');
  for (const entry of entries) {
    const id = _entryKey(entry);
    if (!id) continue;
    ins.run(String(id), JSON.stringify(entry), now);
  }
}

function _resyncWorktreePoolIfDiverged(db) {
  const fp = _resolveFilePath('worktree-pool.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_worktreePoolHash != null && currentHash === _worktreePoolHash) return;
  if (_worktreePoolHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM worktree_pool LIMIT 1').get();
    if (sqlHas) { _worktreePoolHash = currentHash; return; }
  }
  _hydrateWorktreePool(db);
  _worktreePoolHash = currentHash;
}

function _readWorktreePoolFromSql(db) {
  const rows = db.prepare('SELECT data FROM worktree_pool ORDER BY rowid').all();
  const entries = [];
  for (const row of rows) {
    try { entries.push(JSON.parse(row.data)); } catch { /* skip */ }
  }
  return { entries };
}

function readWorktreePool() {
  const { getDb } = require('./db');
  const db = getDb();
  _resyncWorktreePoolIfDiverged(db);
  const out = _readWorktreePoolFromSql(db);
  if (out.entries.length === 0) {
    const fallback = _readJson(_resolveFilePath('worktree-pool.json'));
    if (fallback && Array.isArray(fallback.entries) && fallback.entries.length > 0) return fallback;
    return { entries: [] };
  }
  return out;
}

function applyWorktreePoolMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncWorktreePoolIfDiverged(db);
    const before = _readWorktreePoolFromSql(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null) ? before : next;
    if (!after || !Array.isArray(after.entries)) after.entries = [];

    const indexByKey = (arr) => {
      const out = new Map();
      for (const e of arr) {
        const k = _entryKey(e);
        if (k) out.set(String(k), e);
      }
      return out;
    };
    const beforeMap = indexByKey(beforeSnap.entries);
    const afterMap = indexByKey(after.entries);

    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO worktree_pool (entry_id, data, updated_at)
      VALUES (?, ?, ?)
      ON CONFLICT(entry_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
    `);
    const del = db.prepare('DELETE FROM worktree_pool WHERE entry_id = ?');
    let wrote = false;
    for (const [id, entry] of afterMap) {
      const prev = beforeMap.get(id);
      if (!prev || JSON.stringify(prev) !== JSON.stringify(entry)) {
        upsert.run(id, JSON.stringify(entry), now);
        wrote = true;
      }
    }
    for (const [id] of beforeMap) {
      if (!afterMap.has(id)) { del.run(id); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorWorktreePoolJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const obj = _readWorktreePoolFromSql(getDb());
    const target = filePath || _resolveFilePath('worktree-pool.json');
    shared.safeWrite(target, obj);
    const h = _fileContentHash(target);
    if (h != null) _worktreePoolHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── qa_runs ───────────────────────────────────────────────────────────────
// Shape: [ {id, runbookId, targetName, project, workItemId, status, startedAt,
//           completedAt, createdAt, artifacts, summary, ...}, ... ]
// SQL:   row per id, extracted query columns + JSON blob `data`.
// Pattern: mirrors watches-store (top-level array, id-keyed diff).

let _qaRunsHash = null;

function _hydrateQaRuns(db) {
  const fp = _resolveFilePath('qa-runs.json');
  const raw = _readJson(fp) || [];
  if (!Array.isArray(raw)) return;
  db.prepare('DELETE FROM qa_runs').run();
  const now = Date.now();
  const ins = db.prepare(`
    INSERT INTO qa_runs (id, runbook_id, target_name, project, work_item_id, status, started_at, completed_at, created_at, data)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(id) DO NOTHING
  `);
  for (const run of raw) {
    if (!run || !run.id) continue;
    ins.run(
      String(run.id),
      String(run.runbookId || ''),
      String(run.targetName || ''),
      run.project || null,
      run.workItemId || null,
      String(run.status || 'pending'),
      _toMs(run.startedAt),
      _toMs(run.completedAt),
      _toMs(run.createdAt) || now,
      JSON.stringify(run),
    );
  }
}

function _resyncQaRunsIfDiverged(db) {
  const fp = _resolveFilePath('qa-runs.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_qaRunsHash != null && currentHash === _qaRunsHash) return;
  if (_qaRunsHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM qa_runs LIMIT 1').get();
    if (sqlHas) { _qaRunsHash = currentHash; return; }
  }
  _hydrateQaRuns(db);
  _qaRunsHash = currentHash;
}

function _readQaRunsFromSqlOnly(db) {
  const rows = db.prepare('SELECT data FROM qa_runs ORDER BY created_at, rowid').all();
  const out = [];
  for (const row of rows) {
    try { out.push(JSON.parse(row.data)); } catch { /* skip malformed */ }
  }
  return out;
}

function readQaRuns() {
  const { getDb } = require('./db');
  const db = getDb();
  _resyncQaRunsIfDiverged(db);
  const out = _readQaRunsFromSqlOnly(db);
  if (out.length === 0) {
    const fallback = _readJson(_resolveFilePath('qa-runs.json'));
    if (Array.isArray(fallback) && fallback.length > 0) return fallback;
    return [];
  }
  return out;
}

function applyQaRunsMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncQaRunsIfDiverged(db);
    const before = _readQaRunsFromSqlOnly(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null)
      ? before
      : (Array.isArray(next) ? next : before);

    const indexById = (arr) => {
      const out = new Map();
      for (const r of arr) {
        if (r && r.id) out.set(String(r.id), r);
      }
      return out;
    };
    const beforeMap = indexById(beforeSnap);
    const afterMap = indexById(after);

    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO qa_runs (id, runbook_id, target_name, project, work_item_id, status, started_at, completed_at, created_at, data)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      ON CONFLICT(id) DO UPDATE SET
        runbook_id   = excluded.runbook_id,
        target_name  = excluded.target_name,
        project      = excluded.project,
        work_item_id = excluded.work_item_id,
        status       = excluded.status,
        started_at   = excluded.started_at,
        completed_at = excluded.completed_at,
        created_at   = excluded.created_at,
        data         = excluded.data
    `);
    const del = db.prepare('DELETE FROM qa_runs WHERE id = ?');
    let wrote = false;
    for (const [id, run] of afterMap) {
      const prev = beforeMap.get(id);
      if (!prev || JSON.stringify(prev) !== JSON.stringify(run)) {
        upsert.run(
          id,
          String(run.runbookId || ''),
          String(run.targetName || ''),
          run.project || null,
          run.workItemId || null,
          String(run.status || 'pending'),
          _toMs(run.startedAt),
          _toMs(run.completedAt),
          _toMs(run.createdAt) || now,
          JSON.stringify(run),
        );
        wrote = true;
      }
    }
    for (const [id] of beforeMap) {
      if (!afterMap.has(id)) { del.run(id); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorQaRunsJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const arr = _readQaRunsFromSqlOnly(getDb());
    const target = filePath || _resolveFilePath('qa-runs.json');
    shared.safeWrite(target, arr);
    const h = _fileContentHash(target);
    if (h != null) _qaRunsHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── qa_sessions ───────────────────────────────────────────────────────────
// Shape: [ {id, state, spec:{...}, primaryProject, coServices, setupStatus,
//           workItems, managedSpawnName, qaRunId, testFile, summary,
//           failureClass, error, createdAt, updatedAt, completedAt, ...}, ... ]
// SQL:   row per id, extracted query columns + JSON blob.

let _qaSessionsHash = null;

function _qaSessionPrimaryProject(session) {
  if (!session || typeof session !== 'object') return null;
  if (session.primaryProject) return String(session.primaryProject);
  if (session.spec && session.spec.project) return String(session.spec.project);
  if (session.spec && Array.isArray(session.spec.projects) && session.spec.projects[0]) {
    return String(session.spec.projects[0]);
  }
  return null;
}

function _hydrateQaSessions(db) {
  const fp = _resolveFilePath('qa-sessions.json');
  const raw = _readJson(fp) || [];
  if (!Array.isArray(raw)) return;
  db.prepare('DELETE FROM qa_sessions').run();
  const now = Date.now();
  const ins = db.prepare(`
    INSERT INTO qa_sessions (id, state, primary_project, qa_run_id, created_at, updated_at, completed_at, data)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(id) DO NOTHING
  `);
  for (const session of raw) {
    if (!session || !session.id) continue;
    ins.run(
      String(session.id),
      String(session.state || 'pending'),
      _qaSessionPrimaryProject(session),
      session.qaRunId || null,
      _toMs(session.createdAt) || now,
      _toMs(session.updatedAt) || _toMs(session.createdAt) || now,
      _toMs(session.completedAt),
      JSON.stringify(session),
    );
  }
}

function _resyncQaSessionsIfDiverged(db) {
  const fp = _resolveFilePath('qa-sessions.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_qaSessionsHash != null && currentHash === _qaSessionsHash) return;
  if (_qaSessionsHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM qa_sessions LIMIT 1').get();
    if (sqlHas) { _qaSessionsHash = currentHash; return; }
  }
  _hydrateQaSessions(db);
  _qaSessionsHash = currentHash;
}

function _readQaSessionsFromSqlOnly(db) {
  const rows = db.prepare('SELECT data FROM qa_sessions ORDER BY created_at, rowid').all();
  const out = [];
  for (const row of rows) {
    try { out.push(JSON.parse(row.data)); } catch { /* skip */ }
  }
  return out;
}

function readQaSessions() {
  const { getDb } = require('./db');
  const db = getDb();
  _resyncQaSessionsIfDiverged(db);
  const out = _readQaSessionsFromSqlOnly(db);
  if (out.length === 0) {
    const fallback = _readJson(_resolveFilePath('qa-sessions.json'));
    if (Array.isArray(fallback) && fallback.length > 0) return fallback;
    return [];
  }
  return out;
}

function applyQaSessionsMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncQaSessionsIfDiverged(db);
    const before = _readQaSessionsFromSqlOnly(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null)
      ? before
      : (Array.isArray(next) ? next : before);

    const indexById = (arr) => {
      const out = new Map();
      for (const s of arr) {
        if (s && s.id) out.set(String(s.id), s);
      }
      return out;
    };
    const beforeMap = indexById(beforeSnap);
    const afterMap = indexById(after);

    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO qa_sessions (id, state, primary_project, qa_run_id, created_at, updated_at, completed_at, data)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
      ON CONFLICT(id) DO UPDATE SET
        state           = excluded.state,
        primary_project = excluded.primary_project,
        qa_run_id       = excluded.qa_run_id,
        created_at      = excluded.created_at,
        updated_at      = excluded.updated_at,
        completed_at    = excluded.completed_at,
        data            = excluded.data
    `);
    const del = db.prepare('DELETE FROM qa_sessions WHERE id = ?');
    let wrote = false;
    for (const [id, session] of afterMap) {
      const prev = beforeMap.get(id);
      if (!prev || JSON.stringify(prev) !== JSON.stringify(session)) {
        upsert.run(
          id,
          String(session.state || 'pending'),
          _qaSessionPrimaryProject(session),
          session.qaRunId || null,
          _toMs(session.createdAt) || now,
          _toMs(session.updatedAt) || _toMs(session.createdAt) || now,
          _toMs(session.completedAt),
          JSON.stringify(session),
        );
        wrote = true;
      }
    }
    for (const [id] of beforeMap) {
      if (!afterMap.has(id)) { del.run(id); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorQaSessionsJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const arr = _readQaSessionsFromSqlOnly(getDb());
    const target = filePath || _resolveFilePath('qa-sessions.json');
    shared.safeWrite(target, arr);
    const h = _fileContentHash(target);
    if (h != null) _qaSessionsHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── pr_links ───────────────────────────────────────────────────────────────
// Shape: { [prId]: <value> } where <value> is typically array of item IDs
// but may also be a bare string (legacy). Readers normalize.

let _prLinksHash = null;

function _hydratePrLinks(db) {
  const fp = _resolveFilePath('pr-links.json');
  const raw = _readJson(fp) || {};
  db.prepare('DELETE FROM pr_links').run();
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
  const now = Date.now();
  const ins = db.prepare('INSERT INTO pr_links (pr_id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(pr_id) DO NOTHING');
  for (const [prId, value] of Object.entries(raw)) {
    ins.run(String(prId), JSON.stringify(value), now);
  }
}

function _resyncPrLinksIfDiverged(db) {
  const fp = _resolveFilePath('pr-links.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_prLinksHash != null && currentHash === _prLinksHash) return;
  if (_prLinksHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM pr_links LIMIT 1').get();
    if (sqlHas) {
      _prLinksHash = currentHash;
      return;
    }
  }
  _hydratePrLinks(db);
  _prLinksHash = currentHash;
}

function _readPrLinksFromSql(db) {
  const rows = db.prepare('SELECT pr_id, data FROM pr_links').all();
  const out = {};
  for (const row of rows) {
    try { out[row.pr_id] = JSON.parse(row.data); }
    catch { /* skip malformed */ }
  }
  return out;
}

function readPrLinks() {
  const { getDb } = require('./db');
  const db = getDb();
  try { _resyncPrLinksIfDiverged(db); }
  catch { /* table may be missing on a stale install — fall back to JSON */ }
  let out;
  try { out = _readPrLinksFromSql(db); }
  catch { return _readJson(_resolveFilePath('pr-links.json')) || {}; }
  if (Object.keys(out).length === 0) {
    const fallback = _readJson(_resolveFilePath('pr-links.json'));
    if (fallback && Object.keys(fallback).length > 0) return fallback;
    return {};
  }
  return out;
}

function applyPrLinksMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncPrLinksIfDiverged(db);
    const before = _readPrLinksFromSql(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null) ? before : next;
    if (!after || typeof after !== 'object' || Array.isArray(after)) {
      return { wrote: false, result: beforeSnap };
    }
    const afterIds = new Set(Object.keys(after));
    const beforeIds = new Set(Object.keys(beforeSnap));
    let wrote = false;
    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO pr_links (pr_id, data, updated_at)
      VALUES (?, ?, ?)
      ON CONFLICT(pr_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
    `);
    const del = db.prepare('DELETE FROM pr_links WHERE pr_id = ?');
    for (const id of afterIds) {
      if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
        upsert.run(id, JSON.stringify(after[id]), now);
        wrote = true;
      }
    }
    for (const id of beforeIds) {
      if (!afterIds.has(id)) { del.run(id); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorPrLinksJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const obj = _readPrLinksFromSql(getDb());
    const target = filePath || _resolveFilePath('pr-links.json');
    shared.safeWrite(target, obj);
    const h = _fileContentHash(target);
    if (h != null) _prLinksHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── cooldowns ─────────────────────────────────────────────────────────────
// Shape: { [key]: { timestamp, failures, ... } }
// SQL:   row per key.

let _cooldownsHash = null;

function _hydrateCooldowns(db) {
  const fp = _resolveFilePath('cooldowns.json');
  const raw = _readJson(fp) || {};
  db.prepare('DELETE FROM cooldowns').run();
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
  const now = Date.now();
  const ins = db.prepare('INSERT INTO cooldowns (key, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO NOTHING');
  for (const [key, value] of Object.entries(raw)) {
    ins.run(String(key), JSON.stringify(value), now);
  }
}

function _resyncCooldownsIfDiverged(db) {
  const fp = _resolveFilePath('cooldowns.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_cooldownsHash != null && currentHash === _cooldownsHash) return;
  if (_cooldownsHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM cooldowns LIMIT 1').get();
    if (sqlHas) { _cooldownsHash = currentHash; return; }
  }
  _hydrateCooldowns(db);
  _cooldownsHash = currentHash;
}

function _readCooldownsFromSql(db) {
  const rows = db.prepare('SELECT key, data FROM cooldowns').all();
  const out = {};
  for (const row of rows) {
    try { out[row.key] = JSON.parse(row.data); }
    catch { /* skip malformed */ }
  }
  return out;
}

function readCooldowns() {
  const { getDb } = require('./db');
  const db = getDb();
  try { _resyncCooldownsIfDiverged(db); }
  catch { /* table may be missing on stale install */ }
  let out;
  try { out = _readCooldownsFromSql(db); }
  catch { return _readJson(_resolveFilePath('cooldowns.json')) || {}; }
  if (Object.keys(out).length === 0) {
    const fallback = _readJson(_resolveFilePath('cooldowns.json'));
    if (fallback && Object.keys(fallback).length > 0) return fallback;
    return {};
  }
  return out;
}

function applyCooldownsMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncCooldownsIfDiverged(db);
    const before = _readCooldownsFromSql(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null) ? before : next;
    if (!after || typeof after !== 'object' || Array.isArray(after)) {
      return { wrote: false, result: beforeSnap };
    }
    const afterIds = new Set(Object.keys(after));
    const beforeIds = new Set(Object.keys(beforeSnap));
    let wrote = false;
    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO cooldowns (key, data, updated_at)
      VALUES (?, ?, ?)
      ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
    `);
    const del = db.prepare('DELETE FROM cooldowns WHERE key = ?');
    for (const id of afterIds) {
      if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
        upsert.run(id, JSON.stringify(after[id]), now);
        wrote = true;
      }
    }
    for (const id of beforeIds) {
      if (!afterIds.has(id)) { del.run(id); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorCooldownsJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const obj = _readCooldownsFromSql(getDb());
    const target = filePath || _resolveFilePath('cooldowns.json');
    shared.safeWrite(target, obj);
    const h = _fileContentHash(target);
    if (h != null) _cooldownsHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── pending_rebases ───────────────────────────────────────────────────────
// Shape: [ { prId, branch, projectName, mergedItemId, queuedAt, attempts, ... }, ... ]
// SQL:   row per array entry (rowid auto-increment preserves insertion order).
//        No stable natural key, so mutations are full clear+insert.

let _pendingRebasesHash = null;

function _hydratePendingRebases(db) {
  const fp = _resolveFilePath('pending-rebases.json');
  const raw = _readJson(fp) || [];
  db.prepare('DELETE FROM pending_rebases').run();
  if (!Array.isArray(raw)) return;
  const now = Date.now();
  const ins = db.prepare('INSERT INTO pending_rebases (data, updated_at) VALUES (?, ?)');
  for (const entry of raw) {
    if (!entry || typeof entry !== 'object') continue;
    ins.run(JSON.stringify(entry), now);
  }
}

function _resyncPendingRebasesIfDiverged(db) {
  const fp = _resolveFilePath('pending-rebases.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_pendingRebasesHash != null && currentHash === _pendingRebasesHash) return;
  if (_pendingRebasesHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM pending_rebases LIMIT 1').get();
    if (sqlHas) { _pendingRebasesHash = currentHash; return; }
  }
  _hydratePendingRebases(db);
  _pendingRebasesHash = currentHash;
}

function _readPendingRebasesFromSql(db) {
  const rows = db.prepare('SELECT data FROM pending_rebases ORDER BY seq').all();
  const out = [];
  for (const row of rows) {
    try { out.push(JSON.parse(row.data)); }
    catch { /* skip malformed */ }
  }
  return out;
}

function readPendingRebases() {
  const { getDb } = require('./db');
  const db = getDb();
  try { _resyncPendingRebasesIfDiverged(db); }
  catch { /* table missing */ }
  let out;
  try { out = _readPendingRebasesFromSql(db); }
  catch { return _readJson(_resolveFilePath('pending-rebases.json')) || []; }
  if (out.length === 0) {
    const fallback = _readJson(_resolveFilePath('pending-rebases.json'));
    if (Array.isArray(fallback) && fallback.length > 0) return fallback;
    return [];
  }
  return out;
}

function applyPendingRebasesMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncPendingRebasesIfDiverged(db);
    const before = _readPendingRebasesFromSql(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null) ? before : next;
    if (!Array.isArray(after)) {
      return { wrote: false, result: beforeSnap };
    }
    let wrote = false;
    if (JSON.stringify(before) !== JSON.stringify(after)) {
      const now = Date.now();
      db.prepare('DELETE FROM pending_rebases').run();
      const ins = db.prepare('INSERT INTO pending_rebases (data, updated_at) VALUES (?, ?)');
      for (const entry of after) {
        if (!entry || typeof entry !== 'object') continue;
        ins.run(JSON.stringify(entry), now);
      }
      wrote = true;
    }
    return { wrote, result: after };
  });
}

function _mirrorPendingRebasesJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const arr = _readPendingRebasesFromSql(getDb());
    const target = filePath || _resolveFilePath('pending-rebases.json');
    shared.safeWrite(target, arr);
    const h = _fileContentHash(target);
    if (h != null) _pendingRebasesHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── cc_sessions ───────────────────────────────────────────────────────────
// Shape: [ { id, sessionId, title, _promptHash, lastActiveAt, ... }, ... ]
// SQL:   row per id (each entry has a unique `id` tab key).

let _ccSessionsHash = null;

function _hydrateCcSessions(db) {
  const fp = _resolveFilePath('cc-sessions.json');
  const raw = _readJson(fp) || [];
  db.prepare('DELETE FROM cc_sessions').run();
  if (!Array.isArray(raw)) return;
  const now = Date.now();
  const ins = db.prepare('INSERT INTO cc_sessions (id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(id) DO NOTHING');
  for (const entry of raw) {
    if (!entry || typeof entry !== 'object' || !entry.id) continue;
    ins.run(String(entry.id), JSON.stringify(entry), now);
  }
}

function _resyncCcSessionsIfDiverged(db) {
  const fp = _resolveFilePath('cc-sessions.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_ccSessionsHash != null && currentHash === _ccSessionsHash) return;
  if (_ccSessionsHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM cc_sessions LIMIT 1').get();
    if (sqlHas) { _ccSessionsHash = currentHash; return; }
  }
  _hydrateCcSessions(db);
  _ccSessionsHash = currentHash;
}

function _readCcSessionsFromSql(db) {
  const rows = db.prepare('SELECT data FROM cc_sessions ORDER BY updated_at, id').all();
  const out = [];
  for (const row of rows) {
    try { out.push(JSON.parse(row.data)); }
    catch { /* skip malformed */ }
  }
  return out;
}

function readCcSessions() {
  const { getDb } = require('./db');
  const db = getDb();
  try { _resyncCcSessionsIfDiverged(db); }
  catch { /* table missing */ }
  let out;
  try { out = _readCcSessionsFromSql(db); }
  catch { return _readJson(_resolveFilePath('cc-sessions.json')) || []; }
  if (out.length === 0) {
    const fallback = _readJson(_resolveFilePath('cc-sessions.json'));
    if (Array.isArray(fallback) && fallback.length > 0) return fallback;
    return [];
  }
  return out;
}

function applyCcSessionsMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncCcSessionsIfDiverged(db);
    const before = _readCcSessionsFromSql(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null) ? before : next;
    if (!Array.isArray(after)) {
      return { wrote: false, result: beforeSnap };
    }
    const beforeById = new Map(beforeSnap.filter(e => e && e.id).map(e => [String(e.id), e]));
    const afterById = new Map(after.filter(e => e && e.id).map(e => [String(e.id), e]));
    let wrote = false;
    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO cc_sessions (id, data, updated_at)
      VALUES (?, ?, ?)
      ON CONFLICT(id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
    `);
    const del = db.prepare('DELETE FROM cc_sessions WHERE id = ?');
    for (const [id, entry] of afterById) {
      const prior = beforeById.get(id);
      if (!prior || JSON.stringify(prior) !== JSON.stringify(entry)) {
        upsert.run(id, JSON.stringify(entry), now);
        wrote = true;
      }
    }
    for (const id of beforeById.keys()) {
      if (!afterById.has(id)) { del.run(id); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorCcSessionsJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const arr = _readCcSessionsFromSql(getDb());
    const target = filePath || _resolveFilePath('cc-sessions.json');
    shared.safeWrite(target, arr);
    const h = _fileContentHash(target);
    if (h != null) _ccSessionsHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── doc_sessions ──────────────────────────────────────────────────────────
// Shape: { [filePath]: { sessionId, lastActiveAt, turnCount, ... } }
// SQL:   row per filePath key.

let _docSessionsHash = null;

function _hydrateDocSessions(db) {
  const fp = _resolveFilePath('doc-sessions.json');
  const raw = _readJson(fp) || {};
  db.prepare('DELETE FROM doc_sessions').run();
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
  const now = Date.now();
  const ins = db.prepare('INSERT INTO doc_sessions (key, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO NOTHING');
  for (const [key, value] of Object.entries(raw)) {
    ins.run(String(key), JSON.stringify(value), now);
  }
}

function _resyncDocSessionsIfDiverged(db) {
  const fp = _resolveFilePath('doc-sessions.json');
  const currentHash = _fileContentHash(fp);
  if (currentHash == null) return;
  if (_docSessionsHash != null && currentHash === _docSessionsHash) return;
  if (_docSessionsHash == null) {
    const sqlHas = db.prepare('SELECT 1 FROM doc_sessions LIMIT 1').get();
    if (sqlHas) { _docSessionsHash = currentHash; return; }
  }
  _hydrateDocSessions(db);
  _docSessionsHash = currentHash;
}

function _readDocSessionsFromSql(db) {
  const rows = db.prepare('SELECT key, data FROM doc_sessions').all();
  const out = {};
  for (const row of rows) {
    try { out[row.key] = JSON.parse(row.data); }
    catch { /* skip malformed */ }
  }
  return out;
}

function readDocSessions() {
  const { getDb } = require('./db');
  const db = getDb();
  try { _resyncDocSessionsIfDiverged(db); }
  catch { /* table missing */ }
  let out;
  try { out = _readDocSessionsFromSql(db); }
  catch { return _readJson(_resolveFilePath('doc-sessions.json')) || {}; }
  if (Object.keys(out).length === 0) {
    const fallback = _readJson(_resolveFilePath('doc-sessions.json'));
    if (fallback && Object.keys(fallback).length > 0) return fallback;
    return {};
  }
  return out;
}

function applyDocSessionsMutation(mutator) {
  const { getDb, withTransaction } = require('./db');
  let db;
  try { db = getDb(); }
  catch (e) { throw new Error(`small-state-store: SQLite unavailable (${e.message})`); }

  return withTransaction(db, () => {
    _resyncDocSessionsIfDiverged(db);
    const before = _readDocSessionsFromSql(db);
    const beforeSnap = JSON.parse(JSON.stringify(before));
    const next = mutator(before);
    const after = (next === undefined || next === null) ? before : next;
    if (!after || typeof after !== 'object' || Array.isArray(after)) {
      return { wrote: false, result: beforeSnap };
    }
    const afterIds = new Set(Object.keys(after));
    const beforeIds = new Set(Object.keys(beforeSnap));
    let wrote = false;
    const now = Date.now();
    const upsert = db.prepare(`
      INSERT INTO doc_sessions (key, data, updated_at)
      VALUES (?, ?, ?)
      ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
    `);
    const del = db.prepare('DELETE FROM doc_sessions WHERE key = ?');
    for (const id of afterIds) {
      if (!beforeIds.has(id) || JSON.stringify(beforeSnap[id]) !== JSON.stringify(after[id])) {
        upsert.run(id, JSON.stringify(after[id]), now);
        wrote = true;
      }
    }
    for (const id of beforeIds) {
      if (!afterIds.has(id)) { del.run(id); wrote = true; }
    }
    return { wrote, result: after };
  });
}

function _mirrorDocSessionsJson(filePath) {
  try {
    const shared = require('./shared');
    const { getDb } = require('./db');
    const obj = _readDocSessionsFromSql(getDb());
    const target = filePath || _resolveFilePath('doc-sessions.json');
    shared.safeWrite(target, obj);
    const h = _fileContentHash(target);
    if (h != null) _docSessionsHash = h;
  } catch { /* mirror best-effort */ }
}

// ─── Test seam ─────────────────────────────────────────────────────────────

function _resetAllForTest() {
  const { getDb } = require('./db');
  try {
    const db = getDb();
    db.exec('DELETE FROM schedule_runs');
    db.exec('DELETE FROM pipeline_runs');
    db.exec('DELETE FROM managed_processes');
    db.exec('DELETE FROM worktree_pool');
    try { db.exec('DELETE FROM qa_runs'); } catch { /* migration not applied */ }
    try { db.exec('DELETE FROM qa_sessions'); } catch { /* migration not applied */ }
    try { db.exec('DELETE FROM pr_links'); } catch { /* migration not applied */ }
    try { db.exec('DELETE FROM cooldowns'); } catch { /* migration not applied */ }
    try { db.exec('DELETE FROM pending_rebases'); } catch { /* migration not applied */ }
    try { db.exec('DELETE FROM cc_sessions'); } catch { /* migration not applied */ }
    try { db.exec('DELETE FROM doc_sessions'); } catch { /* migration not applied */ }
  } catch { /* not initialized */ }
  _scheduleRunsHash = null;
  _pipelineRunsHash = null;
  _managedProcessesHash = null;
  _worktreePoolHash = null;
  _qaRunsHash = null;
  _qaSessionsHash = null;
  _prLinksHash = null;
  _cooldownsHash = null;
  _pendingRebasesHash = null;
  _ccSessionsHash = null;
  _docSessionsHash = null;
}

module.exports = {
  // schedule_runs
  readScheduleRuns,
  applyScheduleRunsMutation,
  _mirrorScheduleRunsJson,
  // pipeline_runs
  readPipelineRuns,
  applyPipelineRunsMutation,
  _mirrorPipelineRunsJson,
  // managed_processes
  readManagedProcesses,
  applyManagedProcessesMutation,
  _mirrorManagedProcessesJson,
  // worktree_pool
  readWorktreePool,
  applyWorktreePoolMutation,
  _mirrorWorktreePoolJson,
  // qa_runs
  readQaRuns,
  applyQaRunsMutation,
  _mirrorQaRunsJson,
  // qa_sessions
  readQaSessions,
  applyQaSessionsMutation,
  _mirrorQaSessionsJson,
  // pr_links
  readPrLinks,
  applyPrLinksMutation,
  _mirrorPrLinksJson,
  // cooldowns
  readCooldowns,
  applyCooldownsMutation,
  _mirrorCooldownsJson,
  // pending_rebases
  readPendingRebases,
  applyPendingRebasesMutation,
  _mirrorPendingRebasesJson,
  // cc_sessions
  readCcSessions,
  applyCcSessionsMutation,
  _mirrorCcSessionsJson,
  // doc_sessions
  readDocSessions,
  applyDocSessionsMutation,
  _mirrorDocSessionsJson,
  // test seam
  _resetAllForTest,
};
