/**
* 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