import type Database from 'better-sqlite3'; import { type WalCheckpointResult } from '../db/schema.js'; import { type ResolveStoreIdentityOptions, type StoreIdentity } from './identity.js'; /** * Moving a store without losing it (FR-24 AC #3/#4, Risk R-4). * * The copy is `VACUUM INTO`, never a file copy. Measured on this machine * (better-sqlite3 12.x / SQLite 3.51.3): copying `.cortex.db` alone out of a * live WAL store produced a database in which the table did not exist — * everything was in the `-wal` sidecar the copy left behind. That is R-4 * ("loses or orphans an existing user's memory") reachable in three obvious * lines. `VACUUM INTO` folds the WAL in, emits one clean file with no sidecars, * works from a read-only connection, and refuses to overwrite an existing * destination — a race guard SQLite gives us for free. * * Nothing here ever deletes a project-root database. AC #3 requires the * original to survive "until the user confirms removal", so no path in this * module touches it; removal is the user's own step. `adoptStore` does remove * the store it has just copied and verified — that is what makes adoption a * move rather than a duplication — and that is the only deletion in the file. */ /** Tables whose row counts must agree before a copy is trusted. */ export declare const VERIFIED_TABLES: readonly ["memory_items", "notes", "sessions", "events"]; export type MigrationAction = 'none' | 'migrated' | 'destination-exists' | 'deferred-to-adoption' | 'failed'; export interface MigrationOutcome { action: MigrationAction; sourcePath: string | null; targetPath: string; /** Whether the copy passed integrity, schema-version and row-count checks. */ verified: boolean; /** Set when `action` is `failed`, or when a migration was skipped for cause. */ reason: string | null; /** Always true when a migration ran: AC #3 leaves the original in place. */ originalRetained: boolean; } export interface CopyVerification { ok: boolean; reason: string | null; } /** * Verify a freshly written copy against its source. * * Both sides are read through their own connections. The destination is opened * fresh rather than reusing the connection that wrote it, because a count taken * through the writing connection verifies that connection's view, not the file. */ export declare function verifyStoreCopy(sourcePath: string, targetPath: string): CopyVerification; export interface MigrateOptions { /** Injected for tests; defaults to `Date.now()`. */ now?: number; /** * Injected for tests; defaults to `verifyStoreCopy`. * * A seam rather than a natural fixture because there is no way to construct a * source that `VACUUM INTO` copies successfully and verification then * rejects — SQLite either produces a faithful copy or fails outright. Without * it, replacing the whole verification call with `{ ok: true }` is a mutation * no test can catch: the failure it models is "the verdict is computed and * then ignored", which is about wiring, not about any input. */ verify?: (sourcePath: string, targetPath: string) => CopyVerification; } /** * Migrate a project-root database into the per-project store directory. * * Idempotent (N-8): once the destination exists this is a no-op, so the second * of two concurrent sessions finds the winner's store and uses it. Never throws * — an ambient caller runs inside a hook where AD-12 requires silence. */ export declare function migrateLegacyStore(identity: StoreIdentity, options?: MigrateOptions): MigrationOutcome; export interface AdoptionCandidate { storeDir: string; dbPath: string; /** `meta.root_path` — where this store believes its repository lives. */ recordedPath: string | null; rootCommitOid: string | null; sizeBytes: number; } /** * Stores that look like this repository under a path that no longer exists. * * AC #4's repair anchor. Only consulted when there is no store at the computed * path — a repository that still resolves to its own store is not lost. * Detection is ambient and silent; acting on it is `cortex adopt`. */ export declare function findAdoptionCandidates(identity: StoreIdentity): AdoptionCandidate[]; export interface AdoptionOutcome { action: 'adopted' | 'destination-exists' | 'failed'; candidate: AdoptionCandidate; targetPath: string; reason: string | null; } /** * Attach an orphaned store to this repository's computed path. * * **`VACUUM INTO` and verify, exactly as migration does — never a rename.** * `fs.renameSync` moves `cortex.db` alone and leaves `-wal`/`-shm` behind, and a * store whose last writer did not close cleanly keeps its rows there. Measured: * a store killed mid-write read 50 rows in place and `no such table` after a * rename of the main file. That is total, silent loss on the one command whose * entire purpose is to rescue memory — and unrecoverable afterwards, because the * abandoned directory no longer holds a `cortex.db` for the scan to find. * * It is still a *move*: the source is removed once the copy is verified, because * leaving it would keep it matching as a candidate and `doctor` would go on * offering an adoption already performed. Verification is what makes the removal * safe, and it precedes it. The sidecars go with it — leaving them orphans a * `-wal` next to no database. */ export declare function adoptStore(identity: StoreIdentity, candidate: AdoptionCandidate): AdoptionOutcome; /** * Persist the AD-10 repair anchor and the recorded checkout path. * * Written on open when absent or stale. `root_path` is deliberately the * worktree toplevel rather than the process cwd: it has to stop existing when * the repository moves, which is the entire signal AC #4 keys on. */ export declare function recordStoreIdentityMeta(db: Database.Database, identity: StoreIdentity): void; export interface ResolvedProjectStore { identity: StoreIdentity; dbPath: string; migration: MigrationOutcome; } /** Drop the memo. Tests that change `CORTEX_HOME` or the filesystem need this. */ export declare function clearProjectStoreCache(): void; /** * The single entry point every transport uses to find its database. * * Four scattered derivations is how three of them drift; this replaces all of * them. Resolution, directory creation and one-time migration happen here so a * caller only has to `openDatabase(dbPath)`. */ export declare function resolveProjectStore(startDir: string, options?: ResolveStoreIdentityOptions): ResolvedProjectStore; export interface OpenedProjectStore extends ResolvedProjectStore { db: Database.Database; } /** * Resolve, migrate, open, and record the repair anchor — in that order. * * The four steps belong together because the third and fourth are only correct * as a pair: a store opened without `recordStoreIdentityMeta` never gets a * `root_commit_oid`, and AC #4's adoption matches on exactly that column. So a * caller that resolves and opens by hand produces a working store that can * never be recovered after a move — a failure invisible until the day it * matters. Leaving that to three transports to each remember is the same shape * as the scattered path derivations this story replaced. */ export declare function openProjectStore(startDir: string, options?: ResolveStoreIdentityOptions): OpenedProjectStore; /** * Close every store this process opened, checkpointing each (FR-25 AC #1). * * Wired to process exit in the CLI and hook transports, whose lifetime *is* the * command, and called on shutdown by the MCP server. Closing after each * individual command instead would break the in-process test suite, where one * vitest process runs many commands and win32 refuses to remove a file whose * handle is still open — the hazard story 2.5 hit and had to split a test for. */ export declare function closeAllProjectStores(): WalCheckpointResult[]; export declare function installStoreCloseOnExit(): void; export declare function closeProjectStore(db: Database.Database): WalCheckpointResult | null; /** Meta keys carrying what happened to the migration, for `doctor` to read. */ export declare const MIGRATED_FROM_KEY = "migrated_from"; export declare const MIGRATION_FAILED_KEY = "migration_failed"; /** * Marks a store Cortex created only because it had to open *something*, while * an orphaned store matching this repository was waiting to be adopted. */ export declare const ADOPTION_PENDING_KEY = "adoption_pending"; /** Does this store record that an adoption is still outstanding? */ export declare function hasPendingAdoption(dbPath: string): boolean; //# sourceMappingURL=store-migration.d.ts.map