/** * Explicit, verified reconcile of stranded legacy project stores into `cleo.db`. * * ## Why this exists (T12319) * * Measured 2026-09-24: ~21 projects ran on an EMPTY consolidated `cleo.db` * while their legacy `.cleo/tasks.db` / `.cleo/brain.db` still held everything * (llmtxt: 849 tasks + 1,696 observations; claude-todo: 5,330 + 5,148). Two * things kept exodus-on-open from ever copying them: a machine-wide * `CLEO_DISABLE_EXODUS_ON_OPEN=1` incident kill-switch (skip, silently), and — * with the switch off — a copy engine that aborted on real legacy data (FK * order, pre-T1408 archive reasons, pre-T877 stages, grandfathered hierarchy * rows, NULL `valid_at`). `cleo doctor superseded-store` saw the rows but * could only say "reconcile the contents first", with no command to do it. * * This module is that command's engine. It does NOT write a second copier: the * copy is {@link runExodusMigrate} (same transforms, receipts and recovery), * restricted to the project target. What it adds is the envelope: * * - **Assessment by key, not count.** For every legacy table the number of * source rows whose primary key is ABSENT from the live table. A count can * look complete while unrelated rows written into the empty shell mask real * gaps; a key cannot. * - **Additive only.** `INSERT OR IGNORE` never overwrites or duplicates a row * already in `cleo.db`; the legacy files are read-only inputs and are never * moved, archived or deleted. * - **Verified or reverted.** After the copy the assessment is re-run; any * missing key (or a live table that shrank) reverts exactly the rows this run * inserted, via their receipts, and the outcome is `refused`. * - **Idempotent.** A fully reconciled project is a no-op: nothing is written. * * @module * @task T12319 * @see ./migrate.ts — the single copy engine * @see ../../doctor/superseded-store.ts — the survey that points here */ import type { SupersededStoreReconcileResult, SupersededStoreTableCount } from '@cleocode/contracts'; import { type TargetResolver } from './runtime-targets.js'; import { type LegacyDbDescriptor } from './types.js'; /** * Account for every data-bearing legacy project table against the live store. * * Read-only on both sides: the live store is opened `readOnly` and each legacy * file is ATTACHed to that connection, which SQLite opens read-only too. * * @param liveStorePath - Absolute path of the live project `cleo.db`. * @param sources - Existing project-scope legacy sources. * @param resolveTarget - Where each legacy table lands; a reconcile passes the * runtime-read targets ({@link buildRuntimeTargetResolver}). * @returns One entry per legacy table holding rows, in copy order. * @task T12319 */ export declare function assessSupersededProjectStores(liveStorePath: string, sources: readonly LegacyDbDescriptor[], resolveTarget?: TargetResolver): SupersededStoreTableCount[]; /** * Whether any of `sources` holds at least one row in a table exodus would copy. * * Cheap by construction — stops at the first non-empty table — because it runs * on the open path whenever a consolidated store is empty and its migration is * being skipped, to decide whether that skip is stranding real data. * * @param sources - Legacy source descriptors of ONE scope. * @returns `true` when a legacy file still holds copyable rows. * @task T12319 */ export declare function legacySourcesHoldRows(sources: readonly LegacyDbDescriptor[]): boolean; /** * The unmigrated `cleo.db`'s own dead bare task-core family as exodus sources, * for exodus-on-open (T12355): the same source the reconcile reads, so the two * converge whichever runs first — without it, on-open left e.g. llmtxt's 1,972 * bare-only `task_labels` behind while a reconcile recovered them. * * @param liveStorePath - The project `cleo.db` being migrated. * @param resolveTarget - The runtime target resolver. * @param legacyTasksPath - The legacy `tasks.db`, if present (its fresher-row comparison). * @param outDir - Scratch directory the materialised sources are written to. * @returns Sources to copy BEFORE the legacy files (bare rows newer than their * legacy copy) and AFTER them (the rest of the bare family); both empty when * the bare family is not a source. * @task T12355 */ export declare function unmigratedBareSources(liveStorePath: string, resolveTarget: TargetResolver, legacyTasksPath: string | undefined, outDir: string): Promise<{ first: LegacyDbDescriptor[]; last: LegacyDbDescriptor[]; }>; /** Result of {@link rollbackSupersededReconcile}. */ export interface SupersededReconcileRollback { /** The run directory whose rows were reverted. */ readonly runDir: string; /** Rows reverted. */ readonly rowsReverted: number; /** Where the receipt now lives (renamed so later runs and checks ignore it). */ readonly receiptPath: string; } /** * Revert a reconciled run from its receipt (T13309): every row it inserted is * removed, but only when each is still exactly as the run wrote it (the * exodus receipts guard refuses the whole revert otherwise). Holds the store's * exodus lock, so no reconcile or exodus-on-open runs alongside. The receipt * is renamed, so neither a later run's earlier-recoveries scan nor the sync * check for stranded bare rows trusts it again. * * @param projectRoot - Absolute project root. * @param runDir - The run directory (`.cleo/exodus-reconcile-`), absolute * or relative to the project's `.cleo`. * @throws When `runDir` is not a reconciled run of this project, or a row the * run inserted has changed since. * @example * ```ts * await rollbackSupersededReconcile('/p', 'exodus-reconcile-20261007T020000Z'); * ``` */ export declare function rollbackSupersededReconcile(projectRoot: string, runDir: string): Promise; /** Options of {@link reconcileSupersededStores}. */ export interface ReconcileOptions { /** Assess and report without writing anything. */ readonly dryRun?: boolean; /** Copy only history rows whose keys live lacks; never write the task graph. */ readonly additive?: boolean; /** * Copy the live store's stranded bare rows into its populated prefixed * tables (T13309); see {@link bareStrandSource}. Wins over `additive`. */ readonly bareStrands?: boolean; } /** * Reconcile a project's stranded legacy stores into its live `cleo.db`. * * @param projectRoot - Absolute project root. * @param options - `dryRun` assesses and reports without writing anything; * `bareStrands` copies the live store's stranded bare rows (T13309). * @returns The receipt; `outcome: 'refused'` means the copy did not verify and * every row it inserted was reverted. * @example * ```ts * const plan = await reconcileSupersededStores('/mnt/projects/llmtxt', { dryRun: true }); * for (const t of plan.before) console.log(t.targetTable, t.missingInLive); * ``` * @task T12319 */ export declare function reconcileSupersededStores(projectRoot: string, options?: ReconcileOptions): Promise; //# sourceMappingURL=reconcile.d.ts.map