/** * T13360 — honest migration of legacy docs into the re-keyed store * (spec `cleodocs-rearch-design` §5, D11165). * * Imports every legacy CleoDocs document (`docs_attachments` rows — the * D11-collapse twin of the legacy `attachments` registry) into the T13354 * re-keyed conduit history family, inside the T13448 migration safety * harness: * * backup → BEGIN IMMEDIATE → before-fingerprints → step → verify → * COMMIT (only when every check passed) / ROLLBACK * * Honesty contract (the spec's words are the ACs): * * - **Every imported history row is labeled `origin: 'migrated'`** — no * imported row poses as native (AC1). * - **No fabricated authors, no invented version numbers** — authors come * from the legacy docs audit log's real `actor`; when no provenance * exists the row is system-attributed (`system:migration`), the AC's * sanctioned "system attribution", never an invented human (AC2). * Version ordinals are derived from the real audit-event order. * - **Dangling-pointer docs are content-captured or marked dangling** — * a doc whose recorded path is missing or ephemeral (worktree/tmp) gets * its bytes recovered from the content-addressed blob store when * possible (captured); otherwise it is recorded in * `conduit_doc_migration_ledger` with `status: 'dangling'`, the legacy * pointer and the reason — never silently imported (AC3). * - **Idempotent** — every write carries a NATURAL uid (doc slug; (doc * uid, content hash, event timestamp); ledger slug), so a re-run derives * the same uids and `ON CONFLICT DO NOTHING` turns it into a verified * no-op; docs already in the ledger are skipped. The second run's * harness verification proves zero deltas and unchanged hashes (AC4). * * The migration is exposed as `cleo docs migrate` (dispatch op * `docs.migrate`); it takes its own pre-step VACUUM INTO backup through the * harness and needs no `--force` (it is additive — nothing is deleted). * * Cross-device identity (T13638): the audit log (`docs-audit.jsonl`) is a * DEVICE-LOCAL file — it never syncs. Version identity therefore CANNOT * derive from a row's position in it (`evt-` was v1 and diverged across * devices). Every imported version's natural uid is instead a pure function * of the version's real facts — (doc uid, content hash, event timestamp) — * so two devices migrating the same synced legacy doc derive IDENTICAL uids * for the same content whatever their local audit logs hold, and two * different contents can never collide on one uid. The doc uid, the ledger * receipt, and the head projection were already device-independent. * * Existing installs: a store that already ran the t13360 import keeps its * position-derived version uids — the ledger receipt makes the migration * one-shot per doc, so the content-derived derivation applies to newly * migrated rows only (accepted mixed state, per the task's (a) option; no * re-key migration, since the spec does not mandate one). * * @module * @task T13360 * @task T13633 — review round 2: leaf HLC constant, native-conflict count, * cross-device caveat * @task T13638 — version uids derived from (doc uid, content hash, event * timestamp), never from the device-local audit position * @epic T13340 * @saga T13339 */ import type { DatabaseSync } from 'node:sqlite'; import { type MigrationRunReport, type MigrationStep, type MigrationStepReport } from '../store/migration-safety.js'; /** * The honest system attribution for migrated rows with no real author * provenance (spec §5 "system attribution"). Never an invented human name. */ export declare const MIGRATION_SYSTEM_AUTHOR = "system:migration"; /** One history row to import for one doc. */ interface PlannedVersion { /** Real audit actor, or {@link MIGRATION_SYSTEM_AUTHOR}. */ readonly authorAgentId: string; /** SHA-256 hex of this version's body. */ readonly contentHash: string; /** Body size in bytes. */ readonly sizeBytes: number; /** Measured token count. */ readonly tokens: number; /** Real provenance instant (audit event ts, or the row's created_at). */ readonly createdAt: string; /** Honest summary of what this row is. */ readonly changeSummary: string; } /** The import decision for one legacy doc. */ interface PlannedDoc { readonly slug: string; readonly status: 'captured' | 'dangling'; readonly reason: string; readonly legacyPath: string | null; readonly legacySha256: string; /** Captured bytes (null when dangling). */ readonly content: Buffer | null; readonly format: string; readonly title: string | null; readonly createdAt: string; readonly versions: readonly PlannedVersion[]; /** Doc-row author (author of the last imported version). */ readonly fromAgentId: string; } /** Statically-declared per-table row deltas (fed to the harness verifier). */ export interface LegacyMigrationPlan { /** The single harness step executing this plan. */ readonly step: MigrationStep; /** Rows the step will add per table (all zero on a re-run). */ readonly expectedDelta: Readonly>; /** Planned doc imports (for reporting/tests). */ readonly docs: readonly PlannedDoc[]; readonly counts: { readonly toCapture: number; readonly toMarkDangling: number; /** Docs already receipted by an earlier run (idempotency skips). */ readonly toSkip: number; /** * Legacy slugs that already exist NATIVELY in `conduit_attachments` — the * migration never clobbers a native doc, and these are counted * explicitly (not silently lumped into `toSkip`) so the operator can * reconcile them by hand (T13633). */ readonly nativeConflicts: number; readonly versions: number; }; } /** * Derive the import plan for every legacy doc — the SAME derivation runs in * `planLegacyDocMigration` (to declare the harness's expected deltas) and * inside the step (inside the transaction), so the deltas match by * construction and any divergence fails verification and rolls back. * * Read-only: never writes; reads the store, the blob files and the audit * log. * * @internal Exported for tests. */ export declare function deriveLegacyDocMigrationPlan(projectRoot: string): Promise<{ docs: PlannedDoc[]; skipped: number; nativeConflicts: number; expectedDelta: Record; }>; /** * Plan the migration: derive the work, declare the harness's expected deltas, * and build the single harness step. Call immediately before * {@link runLegacyDocMigration} — the plan and the step re-derive from the * same state, and the harness fails verification (rolling back) if a * concurrent write lands in between. * * @param projectRoot - Absolute path to the CLEO project root. * @returns The plan (step + declared deltas + per-doc decisions). * @task T13360 */ export declare function planLegacyDocMigration(opts: { projectRoot: string; }): Promise; /** Result wrapper around the harness run report. */ export interface LegacyMigrationRunResult { /** The harness report (backup receipt, per-table verification, transaction). */ readonly report: MigrationRunReport; /** Counts from the plan that built the step. */ readonly counts: LegacyMigrationPlan['counts']; } /** * Run the legacy docs migration inside the T13448 harness: pre-step VACUUM * INTO backup → BEGIN IMMEDIATE → before-fingerprints → import → verify → * COMMIT, or ROLLBACK on any failure (the failed step is never visible). * Additive only — nothing is deleted, so no `force` is required. * * @param opts.projectRoot - Absolute path to the CLEO project root. * @param opts.db - Live native handle on the project store (chokepoint-opened). * @param opts.cwd - Invocation directory (the restore path's worktree guard). * @param opts.onStepReport - Optional per-step report sink. * @returns The harness report plus plan counts. * @task T13360 */ export declare function runLegacyDocMigration(opts: { projectRoot: string; db: DatabaseSync; cwd: string; onStepReport?: (report: MigrationStepReport) => void; }): Promise; export {}; //# sourceMappingURL=legacy-doc-migration.d.ts.map