/** * Dual-scope SQLite DB open chokepoint for the SG-DB-SUBSTRATE-V2 consolidated schema. * * ## Overview (D1″ lifecycle split · T11246/E3 + T11247/E4) * * The owner-ratified D1″ decision (2026-05-30) collapses the CLEO SQLite fleet * into exactly **two `cleo.db` files per machine view**: * * - **Project scope** — `/.cleo/cleo.db` * Contains every project-tier domain: `tasks_*` / `brain_*` (project-local * memory) / `conduit_*` / `docs_*` / `telemetry_*` / lifecycle / provenance / * chain / playbooks / agents (87 tables / 903 columns, T11360 count). * * - **Global scope** — `$XDG_DATA_HOME/cleo/cleo.db` * Contains every cross-project domain: `nexus_*` / `skills_*` / * `signaldock_*` / `brain_*` (global cross-project memory) * (49 tables / 555 columns, T11361 count). * * ## Lifecycle * * `openDualScopeDb` is the **single chokepoint** for all opens of the * consolidated schema. It: * 1. Resolves the DB file path from scope + `cwd` (project) or `getCleoHome()` * (global). * 2. Opens a `node:sqlite` `DatabaseSync` handle. * 3. Applies the canonical pragma set from `specs/sqlite-pragmas.json` via * {@link applyPerfPragmas}. * 4. Runs the drizzle-kit migrate step against the scope-appropriate * migrations folder (`drizzle-cleo-project` or `drizzle-cleo-global`). * 5. Returns a cached, typed `NodeSQLiteDatabase` handle. * Subsequent calls for the same (scope, cwd) return the cached handle. * * ## Note on co-existence with legacy openCleoDb * * During the E3/E4 → E6 exodus transition, `openCleoDb` (the existing * 8-role chokepoint) and `openDualScopeDb` (this module) co-exist. `openCleoDb` * will be updated by E3 to delegate to this function for the consolidated * schema. Until the E6 store rewrite, individual store modules still open their * own legacy DBs via `openCleoDb`. The E6 milestone removes the legacy opens. * * @module * @task T11512 (E4-T1) * @task T11513 (E4-T2 — idempotent write helpers in this same file) * @epic T11247 (E4) * @saga T11242 (SG-DB-SUBSTRATE-V2) * @adr ADR-068, ADR-069 * @see packages/core/src/store/schema/cleo-project/index.ts — project schema * @see packages/core/src/store/schema/cleo-global/index.ts — global schema * @see packages/core/migrations/drizzle-cleo-project — project migrations * @see packages/core/migrations/drizzle-cleo-global — global migrations */ import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite'; import { type ExodusAbortDetail } from './exodus/abort-events.js'; import { type WriterLeaseIdentity } from './writer-lease.js'; /** * The two canonical scopes for the consolidated dual-scope `cleo.db` substrate. * * - `'project'` — per-project DB at `/.cleo/cleo.db` * - `'global'` — per-user DB at `$XDG_DATA_HOME/cleo/cleo.db` */ export type DualScope = 'project' | 'global'; /** Typed Drizzle handle for the project-scope `cleo.db`. */ export type CleoProjectDb = NodeSQLiteDatabase; /** Typed Drizzle handle for the global-scope `cleo.db`. */ export type CleoGlobalDb = NodeSQLiteDatabase; /** * Handle returned by {@link openDualScopeDb}. * * `TScope extends DualScope` narrows `db` to the correct schema type: * - `openDualScopeDb('project')` → `DualScopeDbHandle<'project'>` with `db: CleoProjectDb` * - `openDualScopeDb('global')` → `DualScopeDbHandle<'global'>` with `db: CleoGlobalDb` */ export interface DualScopeDbHandle { /** The Drizzle ORM handle typed against the consolidated schema for `scope`. */ readonly db: TScope extends 'project' ? CleoProjectDb : CleoGlobalDb; /** The scope this handle was opened against. */ readonly scope: TScope; /** Absolute path to the underlying SQLite file. */ readonly dbPath: string; /** * Immutable writer-lease identity bound to this exact handle at construction * (T12042). The chokepoint write primitives derive scope + dbPath from this * identity via {@link resolveDbIdentity} — a caller cannot pair file-A * identity with file-B DB. Frozen and normalized. */ readonly identity: WriterLeaseIdentity; /** * Whether the underlying native `DatabaseSync` connection is still * open. Reflects `nativeDb.isOpen` and is `false` after `close()`. */ readonly isOpen: boolean; /** * Set ONLY when the exodus-on-open data-continuity gate ABORTED the first-open * auto-migration for this scope (T11828 · DHQ-059). When present, the handle is * live and the consolidated `cleo.db` is internally consistent but EMPTY — the * user's real data is still in the legacy fleet, which was kept as the source * of truth. A read-only caller may safely ignore this marker; a MUTATING caller * MUST treat its write as not-durable-against-source and react (see * {@link assertWriteDurable}). `undefined` on every normal (migrated / skipped / * fresh-install) open. */ readonly exodusAbort?: ExodusAbortDetail; /** * Close the underlying native handle and evict this entry from the * singleton cache. Safe to call multiple times (idempotent). */ close(): void; } /** * Options for {@link openDualScopeDbAtPath}. * * @task T11782 (FIX D — dedicated migrate connection) */ export interface OpenDualScopeAtPathOptions { /** * When `true`, open a DEDICATED, NON-cached connection — a second SQLite * handle to the same file, independent of the singleton `_cache`. Used by the * exodus migrate engine so its copy + rollback transactions are isolated from * the caller's cached handle (and any concurrent task INSERTs sharing it). The * returned handle's `close()` closes only the native connection and never * mutates the cache; the caller MUST close it to avoid a descriptor leak. * * @default false */ readonly dedicated?: boolean; } /** * Thrown by {@link assertWriteDurable} when a MUTATING caller is about to write * through a {@link DualScopeDbHandle} whose first-open exodus auto-migration * ABORTED (T11828 · DHQ-059). * * The consolidated `cleo.db` is internally consistent but EMPTY: the user's real * data is still in the legacy fleet (kept as the source of truth). Writing here * would land in a DB that does not reflect that data, so the write is NOT durable * against the source of truth. Read paths never raise this — they intentionally * skip {@link assertWriteDurable} and operate on the empty-but-consistent DB. * * Self-contained (mirrors `BackupRecoverError`) rather than a `CleoError` subclass * so the store layer does not need a new numeric `ExitCode` in `@cleocode/contracts` * for a condition that is surfaced structurally on the handle. * * @task T11828 * @epic T11833 * @saga T11242 * @public */ export declare class ExodusAbortWriteUnsafeError extends Error { /** Stable string error code for envelope `codeName` / log correlation. */ readonly codeName: "E_EXODUS_ABORT_WRITE_UNSAFE"; /** The structured abort detail carried by the handle. */ readonly detail: ExodusAbortDetail; /** Remediation hint surfaced to the operator. */ readonly fix: string; /** * @param detail - The {@link ExodusAbortDetail} stamped on the handle. */ constructor(detail: ExodusAbortDetail); } /** * Assert that a {@link DualScopeDbHandle} is safe to WRITE through. * * Call this at the head of a MUTATING code path (insert/update/delete) that holds * a dual-scope handle. If the handle carries an {@link DualScopeDbHandle.exodusAbort} * marker — i.e. the first-open auto-migration aborted and the consolidated DB is * empty with legacy kept as source — this throws {@link ExodusAbortWriteUnsafeError} * so the write is rejected with a non-zero signal rather than silently landing in * a DB that does not hold the user's data. * * READ-only callers MUST NOT call this — they are expected to operate on the * empty-but-consistent consolidated DB without error, exactly as before T11828. * * @param handle - The handle returned by {@link openDualScopeDb}. * @throws {ExodusAbortWriteUnsafeError} When `handle.exodusAbort` is set. * * @example * ```ts * const h = await openDualScopeDb('project', cwd); * assertWriteDurable(h); // throws if a prior exodus-on-open aborted * await h.db.insert(table).values(row); * ``` * * @task T11828 (DHQ-059) * @epic T11833 * @saga T11242 * @public */ export declare function assertWriteDurable(handle: DualScopeDbHandle): void; /** * Resolve the absolute path to the dual-scope `cleo.db` for the given scope. * * - `project`: `resolveCleoDir(cwd)` + `'cleo.db'` (falls under `/.cleo/`) * - `global`: `getCleoHome()` + `'cleo.db'` (falls under XDG data home `/cleo/`) */ export declare function resolveDualScopeDbPath(scope: DualScope, cwd?: string): string; /** * Open (or re-use) the consolidated dual-scope `cleo.db` for the given scope. * * @param scope - `'project'` for the per-project DB; `'global'` for the * per-user cross-project DB. * @param cwd - Optional working directory used to resolve the project root for * the `'project'` scope. Ignored for `'global'`. * @returns A typed {@link DualScopeDbHandle} wrapping the Drizzle ORM instance * bound to the consolidated schema for the requested scope. The handle is * cached per (scope, dbPath) — subsequent calls return the same instance. * * @example * ```ts * const proj = await openDualScopeDb('project', process.cwd()); * const global = await openDualScopeDb('global'); * ``` * * @task T11512 * @epic T11247 (E4) * @saga T11242 */ export declare function openDualScopeDb(scope: 'project', cwd?: string): Promise>; export declare function openDualScopeDb(scope: 'global', cwd?: string): Promise>; /** * Open (or re-use) a consolidated dual-scope `cleo.db` at an EXPLICIT path, * bypassing the scope→path resolver. * * This is the path-aware sibling of {@link openDualScopeDb}. Production callers * MUST prefer {@link openDualScopeDb}, which resolves the canonical path from * `cwd` / `getCleoHome()`. The explicit-path form exists for two cases: * * 1. Tests that materialise an isolated consolidated `cleo.db` under a * `mkdtemp` directory (e.g. the skills-db `{ path }` override, E6-L5), * without having to monkey-patch `getCleoHome()`. * 2. Domain modules whose legacy lifecycle API accepted an explicit on-disk * path and must keep that contract while still flowing every open through * the single dual-scope chokepoint (so DB Open Guard Gate 3 stays green). * * The handle is cached per (scope, dbPath) exactly like {@link openDualScopeDb}; * a test path and the canonical path are distinct cache keys and never collide. * * @param scope - The consolidated schema scope (`'project'` | `'global'`). * @param dbPath - The absolute path to the consolidated `cleo.db` file. The * parent directory is created if absent. * @returns A typed {@link DualScopeDbHandle} bound to the scope's schema. * * @task T11525 (E6-L5) * @epic T11249 (E6) * @saga T11242 */ export declare function openDualScopeDbAtPath(scope: 'project', dbPath: string, exodusCwd?: string, options?: OpenDualScopeAtPathOptions): Promise>; export declare function openDualScopeDbAtPath(scope: 'global', dbPath: string, exodusCwd?: string, options?: OpenDualScopeAtPathOptions): Promise>; /** * Reset cached dual-scope handles. Primarily for use in tests between test * cases and by domain `closeDb()`/`resetDbState()` paths. Closes the targeted * open handles before evicting them from the cache. * * ## Scope filter (E6-L4 · T11524) * * Pass `scope` to evict ONLY that scope's entries. This matters because the * `'project'` and `'global'` scopes now share this cache: the tasks/brain/conduit * domains hold the project-scope `cleo.db`, while nexus/signaldock/skills hold the * global-scope `cleo.db`. A project-domain reset (`closeDb`/`resetDbState` in * sqlite.ts) must NOT close the global handle out from under an in-flight nexus * query — and vice-versa. When `scope` is omitted, ALL entries are evicted (the * coordinated full teardown used by `closeAllDatabases` and test global resets). * * @param scope - When provided, only entries opened against this scope are * closed + evicted. When omitted, every cached handle is reset. * @internal */ export declare function _resetDualScopeDbCache(scope?: DualScope): void; /** * A typed handle to a single project's consolidated `cleo.db`, keyed by canonical * database path. Obtained from {@link CleoRuntime.openProject}. * * Each {@link ProjectStore} wraps ONE shared {@link DualScopeDbHandle} — closing * it affects only this project; other projects and the global scope are untouched. * * ## Lifecycle identity * * Every store carries an opaque identity tag. Its `close()` is stale-safe: it only * evicts the registry entry when that entry still belongs to THIS store (not a * replacement opened after close). An old stale `close()` is a no-op. * * @task T12036 (E6-L12) * @epic T11249 (E6) * @saga T11242 */ export interface ProjectStore { /** The literal scope discriminator. */ readonly scope: 'project'; /** Absolute on-disk path to this project's `cleo.db`. */ readonly dbPath: string; /** * Immutable writer-lease identity bound to this store's underlying * {@link DualScopeDbHandle} at construction (T12042). */ readonly identity: WriterLeaseIdentity; /** The typed Drizzle ORM handle for the project-scope consolidated schema. */ readonly db: CleoProjectDb; /** * Whether the underlying native `DatabaseSync` connection is still * open. `false` after `close()`. Delegates to * {@link DualScopeDbHandle.isOpen}. */ readonly isOpen: boolean; /** * Set when the exodus-on-open auto-migration aborted for this project * (T11828 · DHQ-059). `undefined` on a normal open. */ readonly exodusAbort?: ExodusAbortDetail; /** * Close this project's handle and evict it from the runtime registry * if-and-only-if the registry still points to this store. Safe to call * multiple times (idempotent). Does NOT affect the global scope or other * open projects. */ close(): void; } /** * A typed handle to the global consolidated `cleo.db`, keyed by canonical * database path. Obtained from {@link CleoRuntime.openGlobal}. * * The {@link GlobalStore} wraps the shared dual-scope chokepoint handle — * closing it only disposes the global entry, never any project. * * ## Lifecycle identity * * Same stale-safe semantics as {@link ProjectStore}: an old `close()` after a * reopen is a no-op. * * @task T12036 (E6-L12) * @epic T11249 (E6) * @saga T11242 */ export interface GlobalStore { /** The literal scope discriminator. */ readonly scope: 'global'; /** Absolute on-disk path to the global `cleo.db`. */ readonly dbPath: string; /** * Immutable writer-lease identity bound to this store's underlying * {@link DualScopeDbHandle} at construction (T12042). */ readonly identity: WriterLeaseIdentity; /** The typed Drizzle ORM handle for the global-scope consolidated schema. */ readonly db: CleoGlobalDb; /** * Whether the underlying native `DatabaseSync` connection is still * open. `false` after `close()`. Delegates to * {@link DualScopeDbHandle.isOpen}. */ readonly isOpen: boolean; /** * Set when the exodus-on-open auto-migration aborted for the global scope * (T11828 · DHQ-059). `undefined` on a normal open. */ readonly exodusAbort?: ExodusAbortDetail; /** * Close the global handle and evict it from the runtime registry * if-and-only-if the registry still points to this store. Safe to call * multiple times (idempotent). */ close(): void; } /** * The CleoRuntime store registry — the explicit composition root that owns * project and global database entries keyed by canonical database path. * * Created via {@link createCleoRuntime}. Each entry in the registry is a * {@link ProjectStore} or {@link GlobalStore} that wraps a shared consolidated * {@link DualScopeDbHandle} obtained from the dual-scope chokepoint * ({@link openDualScopeDbAtPath}). The registry provides: * * - **Path-keyed identity** — entries are keyed by `${scope}::${canonical * dbPath}` (scoped composite key), not cwd or "last opened project". * Equivalent path spellings are normalized via `path.resolve` before keying * so `/path/./to/cleo.db` and `/path/to/cleo.db` single-flight together. * - **Single-flight** — concurrent `openProject(p)` or `openGlobal()` calls * for the same key share one initialization. If the entry is closed during * that initialization, the acquired handle is closed and the openers reject. * - **Scoped disposal** — closing a project never closes another project or * the global scope. `closeAll()` disposes every entry. Each store's `close()` * is stale-safe: only evicts if the registry still references that exact * store instance. * - **Cache-hit liveness** — when the underlying `DatabaseSync` was externally * closed, the registry evicts and reopens (mirrors the dual-scope chokepoint * pattern). * * ## Cross-runtime sharing * * Two separate {@link CleoRuntime} instances (created by independent calls to * {@link createCleoRuntime}) share the **process-global** dual-scope handle * cache (`_cache` in this module). A project opened by runtime A returns a * store whose `close()` closes that shared handle — runtime B's store for the * same path will observe `isOpen === false` on its store, and the * **cache-hit liveness** check in this runtime will reacquire a fresh handle * on the next `openProject`/`openGlobal`. This is by design: the runtime * registry owns **entry lifecycle**, not the underlying connection. * * For connection-level isolation, pass `{ dedicated: true }` to * {@link CleoRuntime.openProject} or {@link CleoRuntime.openGlobal}. This * opens a second `DatabaseSync` connection to the same file (WAL allows * concurrent connections), bypasses the shared chokepoint cache, and returns * a store whose `close()` closes only that dedicated connection. Dedicated * entries are NOT keyed or tracked by the runtime registry — they are * returned directly and the caller is responsible for closing them. * * @task T12036 (E6-L12) * @epic T11249 (E6) * @saga T11242 */ /** * Options for {@link CleoRuntime.openProject} and * {@link CleoRuntime.openGlobal}. * * @task T12036 (E6-L12) */ export interface CleoRuntimeOpenOptions { /** * When `true`, open a DEDICATED, NON-cached connection — a second SQLite * handle to the same file, independent of the singleton `_cache`. The * returned store is NOT tracked in the runtime registry and its `close()` * closes only that dedicated connection. Used for isolation between * concurrent consumers (e.g. a snapshot worker, a migration engine). * * @default false */ readonly dedicated?: boolean; /** * The project working directory that this open was resolved FROM. * * Forwarded verbatim to {@link openDualScopeDbAtPath} as its `exodusCwd` * argument, which is what arms the exodus-on-open legacy-fleet * auto-migration (E6 · T11553). {@link openDualScopeDb} passes its own * `cwd` for exactly this reason; a runtime open that omits it would * silently DISABLE auto-migration for users still carrying legacy * standalone `tasks.db` / `brain.db` files. * * Domain ports that replace a `getDb(cwd)`-style facade MUST forward the * caller's `cwd` here so behaviour is identical to the facade they retire. * Leave `undefined` for explicit-path opens (test fixtures, snapshots) * that must never auto-migrate. * * @task T12037 (E6-L13) */ readonly exodusCwd?: string; } export interface CleoRuntime { /** * Open (or reuse) the project-scope `cleo.db` at the given path. * * The path may be relative or absolute — it is normalized via * `path.resolve()` before keying. The path MUST resolve to a project's * canonical `cleo.db` file (use {@link resolveDualScopeDbPath} to compute * it from a project root directory). * * Concurrent calls for the same normalized path single-flight — all callers * receive the same {@link ProjectStore} instance unless `dedicated: true` * was passed. * * @param dbPath - Absolute or relative path to the project's `cleo.db`. * Normalized via `path.resolve()` before keying. * @param options - Optional open mode (e.g. `{ dedicated: true }`). * @returns A typed {@link ProjectStore} bound to the requested path. */ openProject(dbPath: string, options?: CleoRuntimeOpenOptions): Promise; /** * Open (or reuse) the global-scope `cleo.db`. The path is resolved * internally via {@link getCleoHome}. * * Concurrent calls single-flight — all callers receive the same * {@link GlobalStore} instance unless `dedicated: true`. * * @param options - Optional open mode (e.g. `{ dedicated: true }`). * @returns A typed {@link GlobalStore} bound to the global scope. */ openGlobal(options?: CleoRuntimeOpenOptions): Promise; /** * Open (or reuse) a global-scope `cleo.db` at an EXPLICIT path, bypassing the * `getCleoHome()` resolver. * * The path-aware sibling of {@link openGlobal}, mirroring * {@link openProject}. Exists for domains whose lifecycle API accepts an * explicit on-disk path — notably the skills registry's test-sandbox * `{ path }` override — so those opens still flow through ONE registry * instead of a private cache. * * Keyed as `global::`, so a sandbox file and the canonical * global `cleo.db` are distinct entries that never collide. * * @param dbPath - Absolute or relative path to a global-scope `cleo.db`. * Normalized via `path.resolve()` before keying. * @param options - Optional open mode (e.g. `{ dedicated: true }`). * @returns A typed {@link GlobalStore} bound to the requested path. * * @task T12039 (E6-L15) */ openGlobalAt(dbPath: string, options?: CleoRuntimeOpenOptions): Promise; /** * Close and evict a single project entry from the registry. The * underlying dual-scope handle is closed (evicted from the chokepoint * cache) and this project is removed from the registry map. Other * projects and the global scope are unaffected. * * If the project is mid-initialization, the in-flight open is cancelled: * its acquired handle is closed and its promise rejects. * * Idempotent — a path not in the registry is a no-op. * * @param dbPath - The path previously passed to {@link openProject}. * Normalized internally. */ closeProject(dbPath: string): void; /** * Close and evict every entry in the registry. Disposes all project * handles and the global handle if open. Cancels any in-flight opens. * Safe to call multiple times. */ closeAll(): void; /** * The set of composite registry keys (`scope::normalizedDbPath`) * currently tracked by this runtime. The key format matches the * internal scope-qualified cache key and distinguishes a project * path from a global path that happens to share the same spelling. * * Read-only snapshot — concurrent opens/closes may race the snapshot. */ readonly openPaths: ReadonlySet; } /** * Override the dual-scope opener function used by the given * {@link CleoRuntime} instance. Tests inject a custom opener to return a * pre-closed handle, exercising the post-await liveness and bounded- * reacquisition code paths. * * The injected function receives the same `(scope, dbPath)` signature as * {@link openDualScopeDbAtPath} and must return a * {@link DualScopeDbHandle}. Only non-dedicated opens ( * {@link CleoRuntime.openProject} and {@link CleoRuntime.openGlobal} * without `{ dedicated: true }`) are affected. Dedicated opens always * call {@link openDualScopeDbAtPath} directly — a custom opener can * never turn a dedicated store into a cached/shared store. * * Pass `undefined` to restore the default (`openDualScopeDbAtPath`). * * @param runtime - The runtime instance to configure. * @param fn - The opener to use, or `undefined` to reset. * * @internal Test seam; imported directly from this module, not the barrel. */ export declare function setRuntimeOpenFn(runtime: CleoRuntime, fn: typeof openDualScopeDbAtPath | undefined): void; /** * Create a new {@link CleoRuntime} store registry. * * The returned runtime is the explicit composition root for project and * global database entries. Use {@link CleoRuntime.openProject} to open a * project's `cleo.db` at a canonical path, and {@link CleoRuntime.openGlobal} * for the global scope. Closing a project via {@link CleoRuntime.closeProject} * or the store's own `close()` disposes only that entry. * * ## Cross-runtime behavior * * Two separate runtime instances SHARE the **process-global dual-scope * handle cache** (`_cache`). Closing a store in one runtime closes the * shared `DatabaseSync` — another runtime referencing the same path * will observe a closed connection. The **cache-hit liveness** check in * this runtime reacquires a fresh handle on the next `openProject`/ * `openGlobal`. This is by design: the runtime owns **entry lifecycle**, * not the underlying connection. Use a dedicated connection for isolation. * * @returns A fresh {@link CleoRuntime} instance with an empty registry. * * @example * ```ts * import { createCleoRuntime, resolveDualScopeDbPath } from '@cleocode/core/db'; * * const runtime = createCleoRuntime(); * const projectA = await runtime.openProject(resolveDualScopeDbPath('project', '/path/to/projA')); * const projectB = await runtime.openProject(resolveDualScopeDbPath('project', '/path/to/projB')); * // projectA and projectB are distinct, independent handles. * projectA.close(); // Only closes projectA; projectB and any global handle are intact. * ``` * * @task T12036 (E6-L12) * @epic T11249 (E6) * @saga T11242 */ export declare function createCleoRuntime(): CleoRuntime; import type { InferInsertModel } from 'drizzle-orm'; import type { SQLiteTableWithColumns, TableConfig } from 'drizzle-orm/sqlite-core'; /** * Attempt to insert `row` into `table`. If a row with the same value for * `keyColumn` already exists (UNIQUE conflict), the insert is silently skipped. * * Wraps Drizzle v1's `.onConflictDoNothing()` to provide a type-safe, * retry-safe idempotent insert for tables that carry an `idempotency_key` * column or any other UNIQUE column. * * @param db - The Drizzle database handle (project or global scope). * @param table - The Drizzle table reference from the consolidated schema. * @param row - The row data to insert (all required columns). * @param _keyColumn - The column name to conflict on (informational; the * conflict resolution is applied table-wide via `.onConflictDoNothing()`). * Pass the column name as a hint for documentation purposes. * @returns The number of rows actually inserted (0 or 1). * * Refuses the write (throws {@link ExodusAbortWriteUnsafeError}) when a prior * exodus-on-open aborted in this process (T11828 · DHQ-059) — these helpers are * the consolidated-schema MUTATION primitives, so the guard is write-only and * never affects read paths. * * Derives the writer-lease identity from the DB handle itself via * {@link resolveDbIdentity} — the identity was registered at * {@link DualScopeDbHandle} construction, so a caller cannot pair file-A * identity with file-B DB (T12042). * * @example * ```ts * import { tasksTasksTable } from '@cleocode/core/store/schema/cleo-project'; * const inserted = await insertIdempotent(db, tasksTasksTable, newTask, 'idempotencyKey'); * ``` * * @task T11513 (E4-T2) * @task T11828 (write-side exodus-abort guard) * @task T12042 (E6-L12b — exact DB-bound identity) * @epic T11247 (E4) * @saga T11242 */ export declare function insertIdempotent>(db: NodeSQLiteDatabase, table: TTable, row: InferInsertModel, _keyColumn: string): Promise; /** * Upsert `row` into `table`, updating all non-key columns when a row with * the same `keyColumn` value already exists. * * Wraps Drizzle v1's `.onConflictDoUpdate()` for retry-safe upsert semantics. * * @param db - The Drizzle database handle. * @param table - The Drizzle table reference. * @param row - The row data to insert or update. * @param keyColumn - The conflict-target column name (must be a UNIQUE or * PRIMARY KEY column on the table). * @param conflictTarget - The column reference used as the `.target` for * `.onConflictDoUpdate()`. Pass the Drizzle column reference (e.g. * `table.idempotencyKey`). * @param set - The columns to update on conflict. If omitted, all columns * in `row` are used as the update set. * @returns The number of rows inserted or updated (always 1). * * Refuses the write (throws {@link ExodusAbortWriteUnsafeError}) when a prior * exodus-on-open aborted in this process (T11828 · DHQ-059) — write-only guard. * * Derives the writer-lease identity from the DB handle itself via * {@link resolveDbIdentity} (T12042 — exact DB-bound identity, no fallback). * * @example * ```ts * await upsertIdempotent(db, tasksTasksTable, updatedTask, 'idempotencyKey', * tasksTasksTable.idempotencyKey); * ``` * * @task T11513 (E4-T2) * @task T11828 (write-side exodus-abort guard) * @task T12042 (E6-L12b — exact DB-bound identity) * @epic T11247 (E4) * @saga T11242 */ export declare function upsertIdempotent>(db: NodeSQLiteDatabase, table: TTable, row: InferInsertModel, /** The conflict-target column name (informational hint for callers). */ _keyColumn: string, conflictTarget: any, set?: Partial>): Promise; //# sourceMappingURL=dual-scope-db.d.ts.map