/** * Split-brain import: bring the rows only one copy of a project store has * into another copy, under new ids, without touching a single pre-existing * row (T12329). * * ## The failure this repairs * * A project directory copied to a second path keeps its `project-info.json` * and its `.cleo/cleo.db`. From then on, both copies write under one * projectId. Both task allocators continue from the same counter, so the * same `T####` names different work in each copy. Brain observations * written in one copy never reach the other. Measured on cleocode, whose * checkout was rsynced to `/home/keatonhoskins/cleocode` on 2026-09-14: * T12188–T12202 exist in both stores with different titles, and 11 * observations exist only in `/home`. * * ## What counts as a source-only row * * The copies were one store until they diverged. The divergence point `T0` is * the newest `created_at` among tasks present in BOTH stores with the SAME * `created_at`. A source row absent from the target is imported only if it was * written after `T0`. A source row written before `T0` was present in the * target at divergence, so its absence there means the target deleted it (for * example by session GC). Resurrecting it would undo the target's own history, * so such rows are counted and reported, never imported. * * A task whose id exists in both stores with a different `created_at` is a * COLLISION: the same id holds different work. Every imported task gets a newly * minted id: `max(sequence counter, max stored id) + k`, the same lower bound * `allocateNextTaskId` uses. The live allocator therefore continues past the * imported ids with no counter write. Child rows follow their task (acceptance * criteria, AC history, dependencies, relations, labels, work history, commit * links, audit rows). Brain rows and sessions keep their ids, which are * time+random and absent in the target by construction. A brain or session id * that collides is reported as unresolved, not imported. * * ## What is deliberately not imported (reported with counts) * * - telemetry: usage, retrieval and token logs; * - derived data the runtime rebuilds: the brain page graph and embeddings; * - parent-owned child-projection AC rows on a parent that is NOT imported. A * rebuild rewrites the parent's whole AC set, and this import never modifies * a pre-existing row. The runtime rebuild owns it; * - rows in any other table that reference an imported task. They are counted * per table so coverage is never silently partial. * * ## Provenance * * Every imported entity gets a `tasks_audit_log` row: action * `split_brain_import`, the original id, the new id, the source store path and * label. Every imported task also carries a note naming its original id. The * returned receipt lists the full id map. * * The target is written in ONE `BEGIN IMMEDIATE` transaction, or not at all * (`dryRun`, the default in the CLI). The source is opened read-only. * * @task T12329 */ import { parseStoreTimestamp } from './row-identity.js'; export { parseStoreTimestamp }; /** Options for {@link importSplitBrain}. */ export interface SplitBrainImportOptions { /** Store that holds the extra rows. Opened read-only. */ readonly sourcePath: string; /** Store that receives them. Opened read-only when `dryRun`. */ readonly targetPath: string; /** Plan only; write nothing. */ readonly dryRun?: boolean; /** Human label for the source, recorded in provenance (e.g. its checkout path). */ readonly sourceLabel?: string; /** * Override the detected divergence point (ISO 8601). Only rows written * after it are candidates. */ readonly divergedAfter?: string; /** * Task under which provenance rows for non-task entities are recorded * (`tasks_audit_log.task_id` is NOT NULL). It must exist in the target. * Required to apply an import that includes non-task rows. */ readonly provenanceTaskId?: string; } /** One imported (or planned) entity. */ export interface SplitBrainMapping { /** Table the row lives in. */ readonly table: string; /** Id in the source store. */ readonly originalId: string; /** Id in the target store (equal to `originalId` when kept). */ readonly newId: string; /** Why it is imported. */ readonly reason: 'collision' | 'source-only'; } /** Rows found in the source but not imported, with the reason. */ export interface SplitBrainSkip { /** Table. */ readonly table: string; /** Row count. */ readonly count: number; /** Why these rows are not imported. */ readonly reason: string; } /** Receipt of {@link importSplitBrain}. */ export interface SplitBrainReport { /** `true` when nothing was written. */ readonly dryRun: boolean; /** Source store path. */ readonly sourcePath: string; /** Target store path. */ readonly targetPath: string; /** Divergence point used (ISO 8601). */ readonly divergedAfter: string; /** Tasks identical in both stores (same id and created_at). */ readonly sharedTasks: number; /** Imported (or, in a dry run, planned) entities with their id mapping. */ readonly mappings: readonly SplitBrainMapping[]; /** Rows written (or planned) per table, including child rows. */ readonly rowsByTable: Readonly>; /** Rows found in the source that are not imported, with reasons. */ readonly skipped: readonly SplitBrainSkip[]; /** Rows that could not be imported safely, with reasons. */ readonly unresolved: readonly SplitBrainSkip[]; } /** * Import the source-only rows of a split-brain store pair into the target. * * @param options - Source, target, dry-run flag and provenance label. * @returns The receipt: divergence point, id map, per-table counts, and * every skipped or unresolved row group with its reason. * @throws {Error} When the stores share no task (they are not two copies of * one store), or when the target write fails. A failed write is rolled * back entirely. * * @example * ```ts * const plan = importSplitBrain({ sourcePath: home, targetPath: live, dryRun: true }); * ``` */ export declare function importSplitBrain(options: SplitBrainImportOptions): SplitBrainReport; /** Per-table result of {@link verifyPreexistingRows}. */ export interface PreservedRowsCheck { /** Table name. */ readonly table: string; /** Rows in the snapshot taken before the import. */ readonly before: number; /** Rows after the import. */ readonly after: number; /** Snapshot rows with no byte-identical row after the import (must be 0). */ readonly missingOrChanged: number; } /** * Prove that an import only ADDED rows: every row of every ordinary table in * the pre-import snapshot still exists, byte-identical, in the post-import * store. * * FTS virtual tables and their shadow tables are excluded. They are derived, * and an FTS5 index legitimately rewrites its segments on insert. * * @param beforePath - Snapshot of the target taken before the import. * @param afterPath - The target after the import. * @returns One entry per table; every `missingOrChanged` must be 0. */ export declare function verifyPreexistingRows(beforePath: string, afterPath: string): PreservedRowsCheck[]; //# sourceMappingURL=split-brain-import.d.ts.map