/** * 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 { DatabaseSync } from 'node:sqlite'; import type { OperationExecutionContext } from '@cleocode/contracts/jobs'; import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite'; import { type ExodusAbortDetail, ExodusAbortWriteUnsafeError } 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; } /** * Obtain the native connection owned by an already-open dual-scope handle. * * Validates the driver's runtime client instead of casting the schema-typed * Drizzle database, whose public type does not expose `$client`. * * @param handle - Exact cached or dedicated handle whose connection is required. * @returns Its native SQLite connection; ownership remains with the handle. * @throws When the driver exposes no native SQLite connection. * @remarks Dedicated migration handles retain independent connection ownership. * @example * ```ts * const native = getDualScopeNativeDb(handle); * native.prepare('SELECT 1').get(); * ``` */ export declare function getDualScopeNativeDb(handle: DualScopeDbHandle): DatabaseSync; /** * Options for {@link openDualScopeDbAtPath}. * * @task T11782 (FIX D — dedicated migrate connection) */ export interface OpenDualScopeAtPathOptions { /** Captured operation lifetime; defaults to the existing async routing scope when present. */ readonly execution?: OperationExecutionContext; /** * 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; /** * How the physical open treats sync (journal spec §1.5, T13336). `live` * runs the capture open pass (triggers match `sync.capture`) and the * replica bind (`syncOpenPass`). `off` runs neither: no capture * trigger is installed, verified or dropped, and the store is never bound * or rebound, so nothing it writes is sealed as a new replica's. * * Defaults to `off` for a dedicated handle and for any path that is not a * canonical store (`/.cleo/cleo.db`, or `/cleo.db` for the * global scope), and to `live` otherwise. A cached handle keeps the mode of * the open that created it. */ readonly syncMode?: StoreSyncMode; } /** How a chokepoint open treats sync: see {@link OpenDualScopeAtPathOptions.syncMode}. */ export type StoreSyncMode = 'live' | 'off'; /** * The sync mode of a physical open (T13336): the caller's, else `off` for a * dedicated handle or a non-canonical path, else `live`. */ export declare function resolveStoreSyncMode(scope: DualScope, dbPath: string, options?: Pick): StoreSyncMode; export { ExodusAbortWriteUnsafeError }; /** * 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; /** * Refuse a write through `nativeDb` while that store still owes its legacy * migration: exodus-on-open was deferred or aborted while legacy rows wait * (T13158, T13167). * * The production write chokepoints call this first (the task accessor's write * transaction, session creation, {@link insertIdempotent}, * {@link upsertIdempotent}), so the caller gets the typed * {@link ExodusAbortWriteUnsafeError} and its remedy rather than a wrapped * SQLite trigger error. The guard is per connection, so one store's refusal * never refuses another. Once the scope's anchor table has rows (the migration * or a reconcile ran, in this process or another) the guard lifts and writes * proceed. The temp triggers remain the backstop for every other write path. * It first waits out another process's genesis or restore marker * ({@link awaitStoreWritable}, T12343). * * @param nativeDb - The connection about to be written. * @throws {ExodusAbortWriteUnsafeError} When that store still owes its migration. * @example * ```ts * await assertExodusWriteSafe(nativeDb); * ``` * @task T13158 * @task T13167 */ export declare function assertExodusWriteSafe(nativeDb: DatabaseSync): Promise; /** * Refuse a cloud vault placeholder as a project directory (T13006, T13021): * it names a project that lives on another machine, so resolving it (raw, or * already resolved against a working directory) would open or create a store * under the caller's directory. * * @param cwd - A project directory, as given or resolved. * @throws {CleoError} `NOT_FOUND` for a placeholder. */ export declare function assertNotVaultRemotePath(cwd: string | undefined): void; /** * Resolve the absolute path to the dual-scope `cleo.db` for the given scope. * * - `project`: `resolveCleoDir(cwd)` + `'cleo.db'` (falls under `/.cleo/`); a cloud * vault placeholder `cwd` (a project on another machine) is refused with `E_NOT_FOUND` * - `global`: `getCleoHome()` + `'cleo.db'` (falls under XDG data home `/cleo/`). * @param scope - Project or global storage ownership. * @param cwd - Explicit project root when resolving project storage. * @param capturedGlobalHome - Previously captured global home, independent of later environment changes. * @returns Canonical consolidated store path. * @throws {CleoError} `NOT_FOUND` when `cwd` is a cloud vault placeholder (T13006). * @remarks Omitting the captured home preserves ambient global routing. A project * resolution never uses the global home override. Capture ownership before awaiting. * @example * ```ts * const path = resolveDualScopeDbPath('global', undefined, capturedHome); * ``` */ export declare function resolveDualScopeDbPath(scope: DualScope, cwd?: string, capturedGlobalHome?: 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'`. * @param options - Optional dedicated handle and captured operation lifetime. * @throws Error if the operation expires or a nested call replaces its captured context. * @remarks Cancellation guards asynchronous continuations and publication, but cannot * preempt synchronous SQLite work. A cancelled initializer never closes a published * handle which another caller may already use. * @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, options?: OpenDualScopeAtPathOptions): Promise>; export declare function openDualScopeDb(scope: 'global', cwd?: string, options?: OpenDualScopeAtPathOptions): Promise>; /** * Assert a chokepoint handle has `foreign_keys = 1`, at open, after the * schema pass; never per transaction (NEW-6, T12786). Every chokepoint * handle is ON: `applyPerfPragmas` sets it outside vitest, and `node:sqlite` * opens with foreign keys enabled when the pragma is skipped (vitest), so a * test runs on the same mode production does. * * @throws {ForeignKeysNotRestoredError} `E_STORE_FK_OFF` when it is off. */ export declare function assertHandleForeignKeys(nativeDb: DatabaseSync): void; /** * 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. * @param exodusCwd - Internal canonical root enabling migration-on-open checks. * @param options - Optional dedicated handle and captured operation lifetime. * @throws Error if opening, migration, cancellation, or initializer ownership checks fail. * @remarks Scoped cancellation is cooperative; synchronous SQLite work can overrun * the deadline. Live waiters may retry after their initializing caller is cancelled. * @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; } /** * Run one domain establishment with the runtime store's captured file ownership. * @typeParam T - Result produced by the domain establishment callback. * @param store - Canonical project or global store whose immutable identity is required. * @param native - Existing domain connection; its main file must expose the same lease grant. * @param establish - Callback admitted under required cross-process schema exclusion. * @returns The callback result after ownership checks and owned lease-handle cleanup. * @remarks The lease handle is independent of the callback connection, allowing * deliberate callback closure. File/epoch checkpoints do not preempt synchronous * work or fence arbitrary uncooperative callback writes. * @example * ```ts * const value = await withStoreSchemaOwnership(store, native, async () => { * return establishSchema(native); * }); * ``` */ export declare function withStoreSchemaOwnership(store: ProjectStore | GlobalStore, native: DatabaseSync, establish: () => Promise): Promise; /** * 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