/** * The repair diff (journal spec §4.4, §4.3 item 3; S3d, T12987). * * A table is `suspect:` when something may have written it uncaptured: a * bracketed rewriter (exodus, the lineage carry-forward, the identity fill), * a quarantined capture, or a migration pass over rows that were already out * of baseline. Later migrations skip every row of a suspect table when they * re-baseline `chash` (T13202), so this pass is the only place those rows are * checked again, and it re-checks EVERY row of the table, never only the * captured ones. * * For each suspect table it compares the live rows with `_sync_row_meta`: * - a live row whose meta `chash` differs: U, carrying the full after-image * (the before-image is unknown; only its hash was kept); * - a live row with no meta, or with a tombstone: I; * - a live meta row with no live row (an orphan): D. * * Rows held by a rebase (`held = 1`, §3.5 Rule 5) are skipped, so a held * insert never produces a spurious D. The ops are written as captures of one * `repair` frame per table and sealed by the sealer (`via: 'repair'`), so row * meta, the ledger and `chash` have one writer. * * A table whose row meta was never baselined (no `baseline:` key) * cannot tell a pre-sync row from an uncaptured insert: both lack meta. Such * rows are baselined instead (meta with the §1.2 genesis HLC, no op) and the * ledger is set to the verified count; nothing is pushed before the genesis * checkpoint, which carries them. From then on a row without meta is an * uncaptured insert. Once the stream has started ({@link streamStarted}) a * row without meta is journaled as an I in every table, baselined or not, * and the table is then marked baselined (T13217): never absorbed silently. * * The suspect key is cleared only after verification: no live capture of the * table remains, a rescan finds nothing to repair, and the ledger equals * `count(*) + held`. A key re-marked while the repair ran is kept. * * @task T12987 * @module store/sync/repair */ import type { DatabaseSync } from 'node:sqlite'; import type { TableScope } from '@cleocode/contracts'; import { type CaptureTableDef } from './capture.js'; import { type SealerRowView } from './sealer.js'; /** `_sync_meta` key prefix of a table whose row meta covers every live row. */ export declare const BASELINE_KEY_PREFIX = "baseline:"; /** How many uids a report lists per kind (the counts are always complete). */ export declare const REPAIR_SAMPLE_SIZE = 20; /** A live row: its uid and its local key `rk` (as the capture triggers write it). */ export interface RepairRow { readonly uid: string; readonly rk: string; } /** The repair diff of one suspect table. */ export interface RepairPlan { readonly table: string; /** Row meta covers every live row, so a row without meta is an uncaptured insert. */ readonly baselined: boolean; /** Live rows to emit as I. */ readonly inserts: readonly RepairRow[]; /** Live rows whose `chash` differs: U. */ readonly updates: readonly RepairRow[]; /** Live meta rows without a live row: D. */ readonly deletes: readonly string[]; /** Live rows without meta in a table never baselined: baselined, no op. */ readonly unbaselined: readonly RepairRow[]; /** Rows a rebase holds (skipped). */ readonly held: number; /** Live rows with no uid: the identity fill must run first. */ readonly unidentified: number; /** Why the table was not diffed, or null. */ readonly skipped: string | null; } /** What the repair did to one table. */ export interface RepairTableResult { readonly table: string; readonly baselined: boolean; readonly counts: { readonly inserts: number; readonly updates: number; readonly deletes: number; readonly baselinedRows: number; readonly held: number; readonly unidentified: number; }; /** Up to {@link REPAIR_SAMPLE_SIZE} uids per kind. */ readonly sample: { readonly inserts: readonly string[]; readonly updates: readonly string[]; readonly deletes: readonly string[]; }; /** The repair frame written, or null (dry run, nothing to emit, skipped). */ readonly frame: string | null; /** The suspect key was cleared. */ readonly cleared: boolean; /** Why the table stays suspect, or null. */ readonly reason: string | null; } /** What {@link repairSuspectTables} did. */ export interface RepairReport { /** Why nothing ran, or null. */ readonly refused: string | null; readonly dryRun: boolean; readonly tables: readonly RepairTableResult[]; /** Transactions and ops the sealer wrote while sealing pending and repair captures. */ readonly sealed: { readonly txns: number; readonly ops: number; }; } /** Options of {@link repairSuspectTables}. */ export interface RepairOptions { readonly scope: TableScope; /** Plan only: write nothing, seal nothing, clear nothing. */ readonly dryRun?: boolean; /** Restrict to these suspect tables (default: every suspect table). */ readonly tables?: readonly string[]; readonly env?: NodeJS.ProcessEnv; /** The replica to seal as (default: the store's bound replica). */ readonly replica?: string; /** Seal while `sync.seal` is unreleased (tests and Gate B only). */ readonly allowUnreleased?: boolean; readonly now?: () => number; } /** * Whether this store's stream has started (T13217): a genesis cut was * recorded (a `_sync_meta` key starting with {@link GENESIS_CUT_KEY_PREFIX}, * §2.11 §10), a sealed transaction was carried by a segment or folded into a * checkpoint (`state` `segmented` or `folded`; `inherited` is another * replica's and does not count), or undo is on (it turns on with the genesis * cut, C1). After that, a checkpoint may already have left the device, so a * row without meta can no longer be assumed to be in it. * * The `undo_enabled` clause never decides the repair diff, which refuses to * run while undo is on; it is there for other callers of this function. */ export declare function streamStarted(db: DatabaseSync): boolean; /** * Diff one table against its row meta, every row. Read-only. * * @param db - The store. * @param scope - Its scope. * @param table - A sync-set table. * @param view - From {@link sealerRowView}, shared across tables. */ export declare function planRepair(db: DatabaseSync, scope: TableScope, table: string, view?: SealerRowView): RepairPlan; /** `x`'s local key matches the bound key values ({@link keyValues}). */ export declare function keyWhere(def: CaptureTableDef): string; /** The key values a row's `rk` encodes, in key-column order. */ export declare function keyValues(rk: string): Array; /** The column a pre-sync row's genesis HLC is read from (§1.2), or null. */ export declare function genesisColumn(scope: TableScope, table: string): string | null; /** * A pre-sync row's genesis physical time (§1.2): its {@link genesisColumn} * value clamped to `[0, now]`, else 0. * * @param at - The row's genesis column value. * @param now - Wall clock (ms). * @returns The physical time (ms). */ export declare function genesisPhys(at: unknown, now: number): number; /** * Baseline one table's row meta (§1.2 "genesis of an existing row"; §1.6 * "one live meta row per live sync-set row"): every live row with a uid and * no meta gets meta with its genesis HLC and no op, the ledger is set to * `count(*) + held`, and `baseline:
` is recorded, atomically. The * repair diff calls it for a table never baselined; T12342's first-enable * init is meant to call it for every sync-set table, so row meta keeps one * initializer. * * Runs in the caller's transaction when there is one, else in its own * `BEGIN IMMEDIATE`. * * @param db - The store. * @param scope - Its scope. * @param table - A sync-set table with a uid column. * @param replica - The replica the baseline meta names as origin. * @param now - Wall milliseconds (the genesis HLC is clamped to it). * @returns The rows baselined, or null when the table is not in the sync set. */ export declare function baselineRowMeta(db: DatabaseSync, scope: TableScope, table: string, replica: string, now?: number): number | null; /** * Seal what is pending, then run the repair diff over every suspect table * (§4.4; S3d, T12987): emit each table's repair ops in a `repair` frame, seal * them, then verify and clear the suspect key. The pending seal runs even * when no table is suspect (the NEW-8 step before a migration pass). A dry * run only plans. * * Prerequisites (§4.4): the sealer's preconditions hold, a replica is bound, * the table's capture triggers are present, and no capture of the table is * left waiting after the pending seal. While undo is on, repair captures * write their own undo ({@link repairUndo}, T13212). * * @param db - The store; must not be inside a transaction. * @param opts - {@link RepairOptions}. */ export declare function repairSuspectTables(db: DatabaseSync, opts: RepairOptions): RepairReport; //# sourceMappingURL=repair.d.ts.map