/** * Write guard for a store that still owes its legacy migration (T13158, T13167). * * Exodus-on-open migrates the legacy `tasks.db` / `brain.db` / `nexus.db` rows * into the consolidated `cleo.db`. When that migration is DEFERRED (the governor * could not admit it) or ABORTED (parity failure, assessment failure, plan * mismatch, a completion marker contradicted by legacy rows), the open still * returns a live handle on the EMPTY store so the command can run. A write * through it used to strand the legacy data for good: a row in the scope's * anchor table makes the store "populated", and on-open never migrates a * populated store. A row in any other table the migration fills can collide * with a legacy row's key, and the migration's copy keeps the new row. * * {@link installExodusWriteGuard} closes that window. Each consolidated table * the migration would fill gets a `TEMP` trigger that refuses INSERTs with * `E_EXODUS_DEFERRED_WRITE_UNSAFE` or `E_EXODUS_ABORT_WRITE_UNSAFE` and the * remedy, while the anchor table is empty. A temp trigger lives only on its * connection and is never persisted, so later opens and the migration's own * dedicated connections never see it. Reads, and writes to tables the migration * does not fill, are untouched. The guard is registered per connection, so the * typed write checks ({@link activeExodusWriteGuard}) and the handle's * `exodusAbort` marker ({@link peekExodusWriteGuard}) read the same state. * * @task T13158 * @task T13167 */ import type { DatabaseSync } from 'node:sqlite'; import type { DualScope } from '../dual-scope-db.js'; import { type ExodusAbortDetail } from './abort-events.js'; /** Stable error code a write refusal carries while the migration is deferred. */ export declare const EXODUS_DEFERRED_WRITE_CODE = "E_EXODUS_DEFERRED_WRITE_UNSAFE"; /** Stable error code a write refusal carries after the migration aborted. */ export declare const EXODUS_ABORT_WRITE_CODE = "E_EXODUS_ABORT_WRITE_UNSAFE"; /** The legacy data a pending migration still owes this scope. */ export interface PendingExodusTargets { /** Legacy sources of the scope that hold copyable rows (`tasks`, `brain`, …). */ readonly sources: readonly string[]; /** Consolidated tables a migration of those sources fills. */ readonly tables: readonly string[]; /** * For each table, the sentinels of the sources that fill it: per source, the * first table in copy order that has legacy rows. Each source is copied in * ONE transaction, so its sentinel holding rows means that source's copy * committed (T13171). */ readonly sentinels: Readonly>; } /** * The legacy sources of `scope` that still hold copyable rows, and every * consolidated table a migration of them would fill. * * Reads the legacy files read-only. Called only on a deferred open of an empty * store whose legacy sources exist, never on a normal open. * * @param scope - The scope whose migration was deferred. * @param cwd - Working directory used to resolve the project root. * @returns The pending sources and target tables; both empty when the legacy * files hold no copyable rows (nothing to protect). */ export declare function pendingExodusTargets(scope: DualScope, cwd: string | undefined): Promise; /** An active guard on one connection. */ export interface ExodusWriteGuard { /** The scope's anchor table: its first row means the migration has run. */ readonly anchor: string; /** Tables carrying a guard trigger. */ readonly tables: readonly string[]; /** * Per table, the sentinel tables whose rows mean every source filling it * has committed its copy ({@link PendingExodusTargets.sentinels}). A table * stays guarded until all of them hold rows, so no table accepts writes while * its own copy is pending (T13171). A table without sentinels waits on the * anchor. Fails closed: if a source's sentinel table is skipped or only * partly copied while its other tables commit, that source's tables stay * guarded on this connection until the process restarts (a later open * assesses afresh). */ readonly sentinels?: Readonly>; /** Legacy sources still holding the rows the migration owes. */ readonly sources: readonly string[]; /** * The scope's exodus completion marker. Its appearance means the cutover * sealed (in any process), even when the legacy held no rows for the anchor * table, so the guard lifts. */ readonly markerPath: string | null; /** * The refusal detail the typed write checks raise and the handle reports as * `exodusAbort`. Its `kind` says whether the migration is deferred or aborted. */ detail: ExodusAbortDetail; } /** * Refuse INSERTs into `guard.tables` on this connection with * `: ` while that table's own copy is pending (any of its * sentinels, else the scope's anchor, is empty), and register the guard. The code follows `guard.detail.kind`: `E_EXODUS_DEFERRED_WRITE_UNSAFE` * or `E_EXODUS_ABORT_WRITE_UNSAFE`. * * Each trigger fires only while one of its table's sentinels (or the anchor) * is empty: once the copy that fills the table commits (in another process, or * the migration itself), the trigger stops refusing on its own, so a * long-lived process recovers without a restart, and a table whose copy has * not committed yet keeps refusing even after the anchor has rows (T13171). Tables absent from the consolidated schema (another scope's) are * skipped. A guard already on the connection is replaced (a deferred migration * that then aborts changes kind and message). * * @param nativeDb - The handle's native connection. * @param guard - Anchor, candidate tables, sources, completion marker, detail. * @param message - Why the write is refused and how to fix it. * @returns The registered guard (its `tables` are the ones now guarded). */ export declare function installExodusWriteGuard(nativeDb: DatabaseSync, guard: ExodusWriteGuard, message: string): ExodusWriteGuard; /** * The guard registered on `nativeDb`, without checking whether it should lift. * Backs the handle's `exodusAbort` marker, read on every access. * * @param nativeDb - A store connection. * @returns The registered guard, if any. */ export declare function peekExodusWriteGuard(nativeDb: DatabaseSync): ExodusWriteGuard | undefined; /** * The guard active on `nativeDb`, or `undefined`. A guard whose anchor table * has rows (the migration ran, here or in another process), or a deferral whose * scope's completion marker exists, is lifted here and reported as absent. * * @param nativeDb - A store connection. * @returns The active guard, if the store is still waiting for its migration. */ export declare function activeExodusWriteGuard(nativeDb: DatabaseSync): ExodusWriteGuard | undefined; /** * Remove the guard from `nativeDb`: drop its triggers, forget it, and clear the * process record it set. Safe when no guard is active. * * @param nativeDb - A store connection. */ export declare function liftExodusWriteGuard(nativeDb: DatabaseSync): void; //# sourceMappingURL=write-guard.d.ts.map