import Database from 'better-sqlite3'; /** * **6, and this is the deliberate exception to AD-11's one-bump-per-release.** * * That rule governs *additive* DDL: Story 2.2 took R1's increment, and 3.1 * appended `content_digests` to `V5_TABLES` without touching the version, * because a table an older binary does not know about is a table it does not * read. Story 3.5 is a different class of change — it **rewrites the meaning of * existing values**. `direction` moves from `'spent'|'saved'` to * `'injected'|'saved'|'unrealized'|'estimated'`, and a pre-3.5 binary filtering * on `direction = 'spent'` then reports `Spent 0 / Saved 0 / Efficiency 0%`: * measured, and confidently wrong rather than absent. * * That is exactly the condition P-5's guard exists for, and this repository's * standing position is that a wrong answer is worse than a refusal — the same * argument that narrowed the downgrade guard to `<` so an old binary can never * silently rewrite a newer store. Without the bump, `NewerSchemaError` cannot * fire and the protection is unreachable for the one change that needs it. * * Cost, stated: any binary built before this story now refuses a store that has * been opened by it — loudly for user commands, silently and exit-0 for hooks * (AD-12). On this machine that is the `dist/`-lags-a-branch-switch case the * project docs already warn about, and a refusal there is the outcome we want. */ export declare const SCHEMA_VERSION = 6; /** * P-5: an older binary opening a store written by a newer one must refuse * clearly rather than corrupt it. * * A distinct class rather than a bare `Error` because the ambient paths have to * tell this apart from an ordinary open failure: AD-12 says a hook degrades to * silence, but a user running `cortex status` deserves the message. Callers * discriminate with `instanceof`, so the wording can change without breaking * them. * * The fix deliberately never says "run a cortex command" — that is what * `doctor` already refuses to say for this case, because the command that * "fixes" it is the one that rewrites the version down. */ /** * A store this build must not operate on. The ambient paths catch this base * class, not each subclass, so a new unopenable condition degrades to hook * silence automatically instead of escaping as an unhandled throw the first * time it fires (AD-12). */ export declare class UnopenableStoreError extends Error { } /** * The stored `schema_version` is present but not a version. * * Refusing rather than repairing, because `getSchemaVersion` parses with * `Number.parseInt` and reports an unparseable value as `0` — indistinguishable * from a fresh store. Measured: a store holding `schema_version = 'v6'` was * opened, rewritten to `'5'`, run through the v1→v2 migration path, and had its * `created_at` overwritten. That is exactly the "silently rewrote it down, * destroying the evidence" outcome `NewerSchemaError` exists to prevent, * reached through a corrupt value instead of a newer one. An *absent* row still * means a fresh store and still opens. */ export declare class CorruptSchemaVersionError extends UnopenableStoreError { readonly rawValue: string; constructor(rawValue: string); } export declare class NewerSchemaError extends UnopenableStoreError { readonly storeVersion: number; readonly binaryVersion: number; constructor(storeVersion: number, binaryVersion: number); } /** * Open (or create) the SQLite database with WAL mode, foreign keys, and busy timeout. */ export declare function openDatabase(dbPath: string): Database.Database; /** * Open an existing database read-only, for callers that must observe the store * without changing it. * * Deliberately not `openDatabase`: that one creates the file when it is missing * and sets `journal_mode = WAL`, which is itself a write and throws on a * read-only connection. A diagnostic that opened the store the normal way would * create an empty database for a user who has none, and — via * `ensureCortexSchema` — migrate the schema it was asked to report on, so a * version mismatch could never be observed. * * Throws when the file does not exist (`fileMustExist`), so absence is a * distinguishable outcome rather than a silently fresh database. */ export declare function openDatabaseReadOnly(dbPath: string): Database.Database; /** Default ceiling before a mid-session checkpoint is worth running (FR-25). */ export declare const DEFAULT_WAL_MAX_BYTES: number; /** One page. A ceiling below this would checkpoint on every single call. */ export declare const MIN_WAL_MAX_BYTES = 4096; export interface WalCheckpointResult { /** A reader held the file, so the WAL could not be reclaimed. Not an error. */ busy: boolean; /** Frames left in the WAL afterwards. */ log: number; /** Frames moved into the main database. */ checkpointed: number; } /** `-wal`. */ export declare function walPath(dbPath: string): string; /** * Size of the write-ahead log, or 0 when there is none. * * `statSync`, deliberately, not a query: opening the database to ask would * *create* the sidecar it is measuring — story 2.3's finding — and this is * called on paths that must stay cheap. */ export declare function walSizeBytes(dbPath: string): number; /** Size of the main database file, or 0 when it does not exist. */ export declare function databaseSizeBytes(dbPath: string): number; /** * Checkpoint the WAL and return the space to the filesystem. * * **`TRUNCATE`, not the passive checkpoint SQLite runs on its own.** Measured * on SQLite 3.51.3: `wal_autocheckpoint` is 1000 pages by default and does bound * the WAL, but a passive checkpoint only resets it for reuse — the file stays * parked at its high-water mark (4,128,272 bytes before and after). Only * `TRUNCATE` shrinks it, and shrinking it is the whole of FR-25's footprint * claim. * * **It never waits, and that is not a detail.** `TRUNCATE` is RESTART plus a * truncate, and RESTART invokes SQLite's busy handler until no other connection * is inside a transaction. `openDatabase` sets `busy_timeout = 5000`, so * inheriting it made every checkpoint a potential five-second stall — measured * at 5518 ms against a concurrent reader and 5560 ms against a writer, both * returning `busy` with the WAL unchanged. Cortex checkpoints only on hook and * command paths, where blocking is the one thing it must not do, so the busy * timeout is dropped to zero for the duration: 7 ms instead of 5518 ms, and the * frames still move (`{busy:1, checkpointed:242}`). * * Never throws. A `busy` result is ordinary rather than a failure — it means * another Cortex process was mid-transaction, most often the spool flush that * `cortex-capture.sh` launches detached past its size threshold. */ export declare function checkpointWal(db: Database.Database, options?: { waitMs?: number; }): WalCheckpointResult | null; /** * The configured mid-session ceiling. * * Parsed with `Number`, not `parseInt`. `gc`'s neighbouring `envNumber` uses * `parseInt`, which succeeds on a *prefix* — `4e6` becomes 4, silently turning a * 4 MB ceiling into a 4-byte one that checkpoints on every call. Same reasoning * as `resolvePageLimit` in story 2.1. * * Rejected as well as non-finite, zero and negative: fractions and anything * below one page. `Number` accepts `0.5` and `0x10` happily, and a sub-page * ceiling means every call checkpoints — cheap now that checkpoints do not * block, but still pointless I/O on every hook. */ export declare function resolveWalMaxBytes(env?: NodeJS.ProcessEnv): number; /** * Checkpoint only when the WAL has crossed its ceiling (FR-25 AC #2). * * Callers must be off the tool-call path: `PostToolUse` is pure bash and spawns * no Node (N-4), so nothing on the hot path can reach this — the constraint is * kept by where it is *called*, and the call sites are the spool flush and * `end-of-turn`. Returns null when nothing was done. */ export declare function maybeCheckpointWal(db: Database.Database, dbPath: string, env?: NodeJS.ProcessEnv): WalCheckpointResult | null; /** * Apply latest tables and indexes. Idempotent (IF NOT EXISTS). * For existing databases, use ensureCortexSchema() to run migrations as well. */ export declare function applySchema(db: Database.Database): void; /** * Initialize meta keys for a fresh database. */ export declare function initializeMeta(db: Database.Database, rootPath: string, schemaVersion?: number): void; export declare function getSchemaVersion(db: Database.Database): number; export interface EnsureSchemaResult { previousVersion: number; currentVersion: number; migrated: boolean; fresh: boolean; } export declare function ensureCortexSchema(db: Database.Database, rootPath: string): EnsureSchemaResult; export declare function getMetaValue(db: Database.Database, key: string): string | undefined; export declare function setMetaValue(db: Database.Database, key: string, value: string): void; //# sourceMappingURL=schema.d.ts.map