import type { SyncJournal, V3JournalState } from "../types.js"; import { AreaLedger, type AreaLedgerDelta } from "./area-ledger.js"; import { AreaResolver, type AreaId } from "./area-resolver.js"; import { StateStore, type StateStoreMaintenanceOptions, type StateStoreMaintenanceResult } from "./state-store.js"; export declare const AREA_LAYOUT_STATE_FORMAT = 1; export declare const DEFAULT_MIGRATION_MAX_RECORD_BYTES: number; export declare const DEFAULT_MIGRATION_MAX_ROWS_PER_RECORD = 256; /** * How long a post-cutover bridge keeps its reverse-shadow mirror open after a * mutation. Opening the legacy store reads and sha256-verifies its newest * snapshot (tens of MB on a large vault), so releasing it after every delta * turned a burst of 16k queued delete-intent writes into 16k snapshot reads. * A short linger amortizes a burst over one open while an idle runner still * pins one journal in memory, not two. */ export declare const AREA_LEGACY_MIRROR_LINGER_MS = 5000; export type LayoutPhase = "legacy-authoritative" | "copying" | "catching-up" | "cutover-ready" | "area-authoritative"; export interface SourceCursor { generation: number; sequence: number; } /** Durable control state. This is deliberately a small, checksummed file. */ export interface LayoutState { format: typeof AREA_LAYOUT_STATE_FORMAT; routerVersion: number; phase: LayoutPhase; sourceScope: string; sourceGeneration: number; sourceSequence: number; sourceWalBytes: number; perAreaCursor: Record; /** Durable ownership of the source-generation pin across CLI processes. */ sourcePinPath?: string; activeLayoutEpoch?: string; activeCommitWatermark?: number; legacyMirrorWatermark?: number; rollbackSafe: boolean; } export interface AreaLedgerMigrationOptions { rootDir: string; journalSlug: string; resolver: AreaResolver; /** The live legacy v3 store. It remains the only writer authority before cutover. */ legacyStore?: StateStore; /** Opens the reverse-shadow store only for an active post-cutover mutation. */ openLegacyStore?: () => StateStore; maxRecordBytes?: number; maxRowsPerRecord?: number; } export interface MigrationParity { rowCount: number; rowDigest: string; unionDigest: string; tombstoneDigest: string; metadataDigest: string; } export interface MigrationRunResult { state: LayoutState; projectedRows: number; maxProjectedRecordBytes: number; } export interface AreaLedgerMigrationTestHooks { /** Throws to model a process death after the named durable transition. */ afterDurablePhase?: (phase: string) => void; beforeManifestCommit?: () => void; afterManifestCommit?: () => void; /** Observes a post-cutover bridge construction without intercepting filesystem APIs. */ afterOpen?: (migration: AreaLedgerMigration) => void; /** Controls the cutover wall clock without changing production behavior. */ now?: () => number; /** Observes every canonical serialization sized by the projection (bytes). */ onCanonicalized?: (utf8Bytes: number) => void; } export declare function setAreaLedgerMigrationTestHooksForTest(hooks: AreaLedgerMigrationTestHooks | undefined): void; /** The path is derived from the legacy scope, never from a mutable current pointer. */ export declare function areaLayoutDirectory(rootDir: string, journalSlug: string): string; export declare function areaLayoutManifestPath(rootDir: string, journalSlug: string): string; /** * Read the last valid durable control state. A corrupt primary never makes * area directories authoritative; a valid committed backup prevents a torn * primary from silently reverting an already-cut-over outpost to legacy. */ export declare function readAreaLayoutState(rootDir: string, journalSlug: string): LayoutState | undefined; export declare function resolveAreaLayoutAuthority(rootDir: string, journalSlug: string): "legacy" | "areas"; /** V3-only binaries must not be allowed to read a stale pre-cutover journal. */ export declare function assertV3OnlyPinAllowed(rootDir: string, journalSlug: string): void; export declare class AreaLedgerMigration { private readonly options; private readonly maxRecordBytes; private readonly maxRowsPerRecord; private readonly areaIds; /** * Reopened at cutover, so it is not `readonly`: see {@link openLedger} for * why the migration-window thresholds must not outlive the migration. */ private ledger; private pin; private projectedRows; private maxProjectedRecordBytes; private legacyStore; private legacyStoreRelease; constructor(options: AreaLedgerMigrationOptions); /** * Open the ledger this bridge works through. * * `relaxed` raises the rotation thresholds far above StateStore's ordinary * limits. That is correct for exactly one situation: a projection pass * building the area copy writes a long run of deliberately small records, * and folding after every 16 MB of them would put the copy in a compaction * loop for no benefit. Normal maintenance compacts each area afterwards. * * It must be scoped to that situation, which is what this argument is for. * Once the manifest commits, the bridge is no longer a migration tool: it * becomes the authoritative access path for every read and write against the * vault, because `openAreaMigrationIfAuthoritative` returns it and caches it. * A threshold left raised there is not a temporary concession -- it is a * permanent property of a live store, and it disables compaction rather than * deferring it. * * That is how a controller ended up with a 224 MB `@meta` WAL of 47,291 * records sitting under a 256 MB threshold. Nothing was contended or stuck; * `needsCompaction()` was simply false, and every process opening the vault * replayed all 224 MB before doing any work. `maintainArea` already reopens * with ordinary limits for exactly this reason, but it only covers scheduled * maintenance of named data areas -- never `@meta`, which is not one of the * layout's areas at all, and never the bridge's own long-lived ledger. */ private openLedger; /** Phase 1 followed by an ordinary Phase 2 tail pass. Safe to resume indefinitely. */ copyAndCatchUp(): MigrationRunResult; /** Phase 3. The caller must already have acquired coordinator maintenance admission. */ cutOver(cutoverBudgetMs?: number): LayoutState; /** * The first keyed session after cutover must borrow this single packed * aggregate instead of repacking every area row into its own projection. * This runs at the authority transition, outside the session-open path. */ private primeSharedJournalRows; /** * Apply an area-authoritative mutation and mirror it into a fresh v3 * generation. A crash after the area append and before this marker leaves * `rollbackSafe=false`, which is intentionally observable to the launcher. */ reverseShadow(delta: AreaLedgerDelta, options?: { releaseLegacyStoreImmediately?: boolean; }): LayoutState; /** Compose the area union into a new v3 generation before an old binary may run. */ composeBackForV3Rollback(): LayoutState; /** * Explicitly abandon an uncommitted copy/catch-up attempt. Area directories * remain non-authoritative, and its durable source pin is retired so pruning * is not held hostage by an abandoned operator run. */ abandon(): LayoutState; verifyParity(legacy: SyncJournal): MigrationParity; /** Verify only the rows and metadata touched by one reverse-shadow delta. */ private verifyDeltaParity; dispose(): void; /** The post-cutover journal adapter uses this instead of opening legacy state. */ readAreaJournal(): SyncJournal; /** * Internal cached aggregate for JournalStore. The ledger owns this object; * callers use its revision to avoid rebuilding a keyed projection unchanged. */ readAreaJournalCached(): ReturnType; /** * Detached keyed snapshot for a scoped pass. Unlike the aggregate reader, * this materializes only the requested rows so callers can mutate their * planning snapshot without retaining or corrupting the ledger cache. */ readAreaJournalScoped(keys: readonly string[], deleteScopeRoots?: readonly string[]): SyncJournal; /** Revision immediately after a local append, before an aggregate is rebuilt. */ currentAreaJournalRevision(): number; hasAreaJournalChanged(revision: number | undefined): boolean; /** Test/diagnostic surface for the lazy per-area StateStore cache. */ openedAreaStoresForTest(): readonly string[]; /** The scheduler's bounded maintenance units, in deterministic order. */ maintenanceAreaIds(): readonly string[]; /** Format-preserving rotation/recovery for one area; never crosses areas. */ maintainArea(areaId: string, maintenance?: StateStoreMaintenanceOptions): { repair: StateStoreMaintenanceResult; rotated: boolean; }; private loadOrInitialize; private withSource; private projectSnapshot; private appendProjection; private replayTail; private applyLegacyRecord; private reconcile; private isTailWarm; private sourceIsAheadOfCursor; private releaseSourcePin; private getLegacyStore; private releaseLegacyStore; /** A full sync pass owns its packed projection and cannot retain a v3 mirror between checkpoints. */ private dropLegacyStore; private requireAreaAuthority; private persist; } //# sourceMappingURL=area-ledger-migration.d.ts.map