/** * Canonical accessors for the re-keyed CleoDocs conduit history family * (T13354 · spec `cleodocs-rearch-design` §3/§10 · D11165). * * Every write to `conduit_attachment_versions` / `conduit_attachment_approvals` * / `conduit_doc_slug_aliases` / `conduit_doc_heads` / * `conduit_attachment_contributors` / `section_crdt_states` goes through the * functions here, on the `openDualScopeDb` chokepoint's drizzle handle * ({@link getDb}) — gate 3 (no raw opens) and gate 28 (no raw writes to * syncing tables; the derived contributors rebuild is raw SQL by design and * exempt per (file, table) in `scripts/lint-no-raw-table-writes.mjs`). * * Semantics implemented here: * * - Version identity is the row uid + `parent_version_uid` DAG; * `version_number` is minted as a LOCAL display ordinal (max + 1 within * the doc on this device) and is never a key. * - Approvals are append-only; {@link revokeConduitAttachmentApproval} * writes a tombstone row (`status: 'revoked'`). * - The doc head projection keeps the max-HLC head; a losing concurrent * head increments `conflict_count` and is recorded in * `last_conflict_json` (typed merge rule `docs.head.max-hlc`). * - `section_crdt_states` is local-only; nothing here syncs it, and an * apply that lands a version rebases it via * {@link rebaseSectionCrdtStates}. * * @module * @task T13354 * @epic T13340 * @saga T13339 */ import { conduitAttachmentApprovals, conduitAttachments, conduitAttachmentVersions, conduitDocHeads, sectionCrdtStates } from './schema/cleo-project/conduit.js'; import type { ConduitDocMigrationLedgerRow } from './schema/cleo-project/docs.js'; /** * Run one docs-projection write batch atomically (T13583). * * The section write path composes several accessors — section-state writes, * the version append, the head upsert, the rebase and the derived * contributors rebuild — and a failure halfway must not leave a non-head * version behind for the next update to fork onto. The drizzle handle from * {@link getDb} and the raw handle from {@link getNativeDb} are the same * underlying `DatabaseSync`, so a `BEGIN IMMEDIATE` on the raw handle * brackets every statement the accessors issue (the same pattern * `docs-update.ts` uses for its slug transfer). * * Constraint (T13583): every statement inside `fn` must resolve without a * real I/O yield — the docs projection satisfies this (sync-driver * accessors, sync `node:fs` blob reads/writes), so no other async writer can * interleave on this connection mid-transaction. A caller that awaits real * I/O inside `fn` breaks that guarantee; per-handle writer serialization is * the follow-up if such a caller appears. * * @param projectRoot - Absolute path to the CLEO project root. * @param fn - The write batch; awaited between statements on this one * connection, so no other writer can interleave inside the transaction. * @returns What `fn` returned. */ export declare function withConduitDocWriteTransaction(projectRoot: string, fn: () => Promise): Promise; /** Row shape of `conduit_attachment_versions` as read back. */ export type ConduitAttachmentVersionRow = typeof conduitAttachmentVersions.$inferSelect; /** Row shape of `conduit_attachment_approvals` as read back. */ export type ConduitAttachmentApprovalRow = typeof conduitAttachmentApprovals.$inferSelect; /** Row shape of `conduit_doc_heads` as read back. */ export type ConduitDocHeadRow = typeof conduitDocHeads.$inferSelect; /** Row shape of `section_crdt_states` as read back. */ export type SectionCrdtStateRow = typeof sectionCrdtStates.$inferSelect; /** One recorded head conflict (the losing concurrent head). */ export interface ConduitDocHeadConflict { /** The losing head's version uid. */ readonly versionUid: string; /** The losing head's HLC. */ readonly hlc: string; /** ISO-8601 instant the conflict was recorded. */ readonly recordedAt: string; /** * The winning head's version uid at recording time — both sides of the * conflict are named on the row (head `head_version_uid` + this). * (T13595) */ readonly winnerVersionUid?: string; } /** * Resolve a document slug to its immutable row uid. * * Lookup order: the live slug on `conduit_attachments` first (a live id * always wins), then the newest alias in `conduit_doc_slug_aliases` (the * old-slug lookup, T13354 AC2). * * @param projectRoot - Absolute path to the CLEO project root. * @param slug - The slug to resolve. * @returns The doc's `conduit_attachments.uid`, or `null` when unknown. */ export declare function resolveConduitDocUid(projectRoot: string, slug: string): Promise; /** * Read one `conduit_attachments` row by its immutable uid. * * @param projectRoot - Absolute path to the CLEO project root. * @param uid - The doc's `conduit_attachments.uid`. * @returns The attachment row, or `null` when unknown. * @task T13727 — `docs versions` conduit routing needs the row's live slug * for display and `--name` filtering. */ export declare function findConduitAttachmentByUid(projectRoot: string, uid: string): Promise; /** * Adopt a document into the conduit family on first sight (T13355). * * The docs write path (`docs/section-write-path.ts`) projects every doc * update through the section CRDT layer, whose history tables are keyed on * `conduit_attachments.uid`; a doc that only exists in the legacy * `attachments` registry has no conduit row yet, so this inserts one. The * row is a thin identity shell: the bytes live in the blob store, so * `content` carries an empty buffer and the sizes/hash describe nothing — * the version rows carry the real content addressing. * * `conversation_id` is the NOT NULL transport owner; docs adopted here use * the `cleodocs` sentinel (no FK — soft reference by design). Idempotent: * an existing live row (or alias-resolved uid) is returned with * `adopted: false`. * * Convergent identity (T13586): every birth fact of an adopted shell is a * function of the legacy doc, never of the adopting device — `created_at` is * the legacy registry row's `created_at` (a synced value), and * `from_agent_id` is the `cleodocs` * sentinel (the adopting author is recorded on the first version row). The * uid and birth fingerprint follow the standard minted recipe over those * facts, so two devices adopting the same legacy doc mint ONE (uid, birth_fp) * and the receiver places the peer's shell as a duplicate. A slug with no * legacy row is a new doc, not an adoption, and keeps the random mint. * * The registry holds one row per slug, and a docs update moves the slug onto * a NEW row stamped with the updating device's clock. A caller adopting * during an update therefore passes `legacyBirth`, the PRE-update row's * `created_at` (T13586 follow-up); otherwise the current slug row is read. * * @param projectRoot - Absolute path to the CLEO project root. * @param input - Slug, author, current full text of the doc, and the * pre-update legacy row's `created_at` when adopting mid-update. * @returns The doc's immutable uid and whether this call created the row. */ export declare function ensureConduitDoc(projectRoot: string, input: { slug: string; authorAgentId: string; content: Buffer; legacyBirth?: string; }): Promise<{ uid: string; adopted: boolean; }>; /** * Record an old-slug → doc-uid alias (append-only). * * The row's uid is the NATURAL identity over (slug, docUid), so two devices * recording the same alias merge to one row; a repeat insert is a no-op. * * @param projectRoot - Absolute path to the CLEO project root. * @param alias - Slug, target doc uid and an optional reason. * @returns The alias row's uid. */ export declare function recordConduitDocAlias(projectRoot: string, alias: { slug: string; docUid: string; reason?: string; }): Promise; /** Input for {@link appendConduitAttachmentVersion}. */ export interface AppendConduitAttachmentVersionInput { /** Immutable doc id (`conduit_attachments.uid`). */ readonly attachmentUid: string; /** Parent version uid in the DAG; null for the root version. */ readonly parentVersionUid: string | null; /** Revision author agent id. */ readonly authorAgentId: string; /** Content-addressed blob ref (never inline bytes for large bodies). */ readonly storageKey: string; /** SHA content hash of the body the blob ref points at. */ readonly contentHash: string; /** Uncompressed size in bytes. */ readonly originalSize: number; /** Compressed size in bytes. */ readonly compressedSize: number; /** Token count of this version. */ readonly tokens: number; /** Change type (default 'patch'). */ readonly changeType?: string; /** Optional patch text for small edits. */ readonly patchText?: string; /** Optional change summary. */ readonly changeSummary?: string; /** Section ids this version modified. */ readonly sectionsModified?: readonly string[]; /** Tokens added / removed by this version. */ readonly tokensAdded?: number; readonly tokensRemoved?: number; } /** * Append a version row to a document's history DAG. * * `version_number` is minted as (current local max within the doc) + 1 — a * display ordinal only; another device minting the same number is NOT a * conflict, because version identity is the row uid + parent linkage. The * `uid` itself comes from the schema's `defaultRowUid` (a random UUIDv7 at * insert), matching the T12341 minted-table pattern. * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The version facts. * @returns The inserted row. */ export declare function appendConduitAttachmentVersion(projectRoot: string, input: AppendConduitAttachmentVersionInput): Promise; /** * The set of content hashes the conduit doc-history family references. * * The blob sweep (`attachment-repair.ts`) only consulted the legacy * attachments tables, the publications ledger and the blobs manifest, so a * version/merged-text blob this family references could be garbage-collected * as "unreferenced" (T13596). The sweep callers consult this set; it covers * the version rows' `content_hash` (the merged-text blobs) and the adopted * docs' `content_hash`. * * @param projectRoot - Absolute path to the CLEO project root. * @returns Every `content_hash` named by `conduit_attachment_versions` or * `conduit_attachments`. */ export declare function listConduitHistorySha256Set(projectRoot: string): Promise>; /** * Read one version row by its wire identity (`uid`). * * @param projectRoot - Absolute path to the CLEO project root. * @param versionUid - The version's `uid`. * @returns The version row, or `null` when unknown. */ export declare function getConduitAttachmentVersion(projectRoot: string, versionUid: string): Promise; /** * List a document's versions, newest display ordinal first. * * @param projectRoot - Absolute path to the CLEO project root. * @param attachmentUid - The doc's immutable uid. * @returns The version rows. */ export declare function listConduitAttachmentVersions(projectRoot: string, attachmentUid: string): Promise; /** Input for {@link recordConduitAttachmentApproval}. */ export interface RecordConduitAttachmentApprovalInput { /** Immutable doc id. */ readonly attachmentUid: string; /** The reviewed version's uid. */ readonly versionUid: string; /** Section the verdict covers; omit for a whole-document verdict. */ readonly sectionId?: string; /** Reviewer agent id. */ readonly reviewerAgentId: string; /** Verdict (default 'approved'; a revoke is written as 'revoked'). */ readonly status?: string; /** Optional reviewer comment. */ readonly comment?: string; /** The version's display ordinal (display only). */ readonly versionReviewed: number; } /** * Record an approval verdict as an append-only row. * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The verdict facts. * @returns The inserted row. */ export declare function recordConduitAttachmentApproval(projectRoot: string, input: RecordConduitAttachmentApprovalInput): Promise; /** * Revoke an approval as a TOMBSTONE row (never an UPDATE or DELETE). * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The revoke facts (same shape as the original verdict). * @returns The tombstone row. */ export declare function revokeConduitAttachmentApproval(projectRoot: string, input: Omit): Promise; /** * The effective verdicts for one version: the latest row per (reviewer, * section) wins, and 'revoked' tombstones cancel the earlier verdict. * * @param projectRoot - Absolute path to the CLEO project root. * @param versionUid - The version uid. * @param sectionId - Restrict to one section when given (plus whole-doc rows). * @returns The approval rows in creation order. */ export declare function listConduitAttachmentApprovals(projectRoot: string, versionUid: string, sectionId?: string): Promise; /** * Rebuild the DERIVED contributors rollup from the versions table. * * `conduit_attachment_contributors` is not synced (spec §10): every device * recomputes it after apply. Raw set-SQL (DELETE + INSERT … SELECT … GROUP * BY) because the rebuild is a pure aggregate — the two statements are the * gate-28 exempt sites for this (file, table). * * @param projectRoot - Absolute path to the CLEO project root. * @param attachmentUid - Rebuild one doc only when given, else every doc. * @returns The number of rollup rows written. */ export declare function rebuildConduitAttachmentContributors(projectRoot: string, attachmentUid?: string): Promise; /** Input for {@link upsertConduitDocHead}. */ export interface UpsertConduitDocHeadInput { /** Immutable doc id. */ readonly attachmentUid: string; /** The candidate head version's uid. */ readonly headVersionUid: string; /** * The candidate head's HLC (journal wire form). Its lexical order IS the * (phys, ctr, replica) tuple order (`store/sync/hlc.ts`), so plain string * comparison is the max-HLC rule. */ readonly headHlc: string; } /** Outcome of a {@link upsertConduitDocHead} call. */ export interface UpsertConduitDocHeadResult { /** Whether the candidate became the head. */ readonly advanced: boolean; /** Whether a losing concurrent head was recorded as a conflict. */ readonly conflictRecorded: boolean; /** The head row after the call. */ readonly head: ConduitDocHeadRow; } /** * Move a document's current-version projection (typed rule * `docs.head.max-hlc`). * * The row keeps the MAX-HLC head. A candidate that loses to a concurrent * head is never silently shadowed: `conflict_count` increments and * `last_conflict_json` records the loser (the losing version itself survives * in `conduit_attachment_versions`). A repeat of the same head is a no-op. * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The candidate head. * @returns Whether the head advanced or a conflict was recorded. */ export declare function upsertConduitDocHead(projectRoot: string, input: UpsertConduitDocHeadInput): Promise; /** * Read a document's current-version projection row. * * @param projectRoot - Absolute path to the CLEO project root. * @param attachmentUid - The doc's immutable uid. * @returns The head row, or `null` when the doc has no projection yet. */ export declare function getConduitDocHead(projectRoot: string, attachmentUid: string): Promise; /** * Read the local Loro state of one document section. * * @param projectRoot - Absolute path to the CLEO project root. * @param attachmentUid - The doc's immutable uid. * @param sectionId - The section id. * @returns The state row, or `null` when this device has none. */ export declare function readSectionCrdtState(projectRoot: string, attachmentUid: string, sectionId: string): Promise; /** * Write the local Loro state of one document section (upsert). * * LOCAL-ONLY: this state never syncs; content moves cross-device only * through landed versions (spec §2 option b). * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The section's snapshot bytes and optional base version. */ export declare function writeSectionCrdtState(projectRoot: string, input: { attachmentUid: string; sectionId: string; snapshot: Buffer; baseVersionUid?: string | null; }): Promise; /** * Rebase every local CRDT state of a document onto a landed version. * * Called when an apply lands a new version: the local Loro state is reset * onto the landed base (`base_version_uid`) so stale local edits do not * silently diverge from the synced history (spec §10). * * @param projectRoot - Absolute path to the CLEO project root. * @param attachmentUid - The doc's immutable uid. * @param baseVersionUid - The landed version's uid. */ export declare function rebaseSectionCrdtStates(projectRoot: string, attachmentUid: string, baseVersionUid: string): Promise; /** Input for {@link importMigratedConduitDoc}. */ export interface ImportMigratedConduitDocInput { /** Doc slug (the pre-rekey primary key; still the table PK). */ readonly slug: string; /** Real provenance author when known; 'system:migration' when none. */ readonly fromAgentId: string; /** Captured content bytes (stored uncompressed; sizes equal). */ readonly content: Buffer; /** SHA-256 hex of `content`. */ readonly contentHash: string; /** Content format tag ('markdown', 'text', …). */ readonly format: string; /** Optional title. */ readonly title?: string; /** Token count (measured, never invented). */ readonly tokens: number; /** ISO-8601 doc creation instant from real provenance. */ readonly createdAt: string; /** Imported history length (also written to the version_count columns). */ readonly versionCount: number; } /** * Import one legacy doc as a `conduit_attachments` row (content-captured). * * The row uid is the natural identity over the slug: re-runs coalesce. * `conversation_id` is the honest system value 'docs:migration' — migrated * docs belong to no conduit conversation. * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The captured doc facts. * @returns The doc uid and whether this call inserted the row. * @task T13360 */ export declare function importMigratedConduitDoc(projectRoot: string, input: ImportMigratedConduitDocInput): Promise<{ uid: string; inserted: boolean; }>; /** Input for {@link importMigratedConduitVersion}. */ export interface ImportMigratedConduitVersionInput { /** Immutable doc id (`conduit_attachments.uid`). */ readonly attachmentUid: string; /** Parent version uid in the DAG (NULL for the root import). */ readonly parentVersionUid: string | null; /** Real provenance author (audit-log actor); 'system:migration' when none. */ readonly authorAgentId: string; /** Content-addressed ref to the captured bytes. */ readonly storageKey: string; /** SHA-256 hex of the version body. */ readonly contentHash: string; /** Body size in bytes. */ readonly sizeBytes: number; /** Measured token count. */ readonly tokens: number; /** Display ordinal within the doc (derived from the real event order). */ readonly versionNumber: number; /** ISO-8601 instant from real provenance (the audit event's ts). */ readonly createdAt: string; /** Honest summary of what this row is. */ readonly changeSummary: string; /** Pre-rekey display slug (legacy carry column). */ readonly slug: string; /** * Import provenance label for `origin` (free TEXT). Default `'migrated'` * (the T13360 docs migration); the T13727 history backfill passes * `'backfill-t13727'` so repair-synthesized rows never pose as T13360 * imports. Natively-written rows never come through this accessor. */ readonly origin?: string; } /** * Import one history row for a migrated doc. * * Every imported row carries an `origin` provenance label (spec §5 — no * imported row poses as native): `'migrated'` for the T13360 data migration, * or the caller's override (the T13727 backfill uses `'backfill-t13727'`). * Row identity is the NATURAL identity over (doc uid, content hash, event * timestamp) — a pure function of the version's real facts (T13638). The * derivation never touches the device-local `docs-audit.jsonl` row position, * so two devices migrating the same synced legacy doc derive the SAME uid * for the same content whatever their local audit logs hold, and two * different contents (different hashes) can never collide on one uid. The * insert is conflict-free on re-run and history can never be duplicated * (AC4). * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The version facts. * @returns The version uid and whether this call inserted the row. * @task T13360 * @task T13638 — content-derived natural uid */ export declare function importMigratedConduitVersion(projectRoot: string, input: ImportMigratedConduitVersionInput): Promise<{ uid: string; inserted: boolean; }>; /** Input for {@link recordConduitDocMigration}. */ export interface RecordConduitDocMigrationInput { /** The migrated doc's slug (one receipt per slug). */ readonly slug: string; /** Captured doc uid (NULL when the doc is dangling). */ readonly docUid: string | null; /** 'captured' or 'dangling'. */ readonly status: 'captured' | 'dangling'; /** Why (e.g. 'path-missing', 'path-ephemeral:worktree'). */ readonly reason: string; /** Legacy pointer path when one existed. */ readonly legacyPath?: string; /** Legacy content sha256 when known. */ readonly legacySha256?: string; /** How many history rows the import created. */ readonly versionsImported: number; } /** * Record the migration receipt for one doc (append-only, idempotent). * * This IS the auditable record for the dangling verdict (AC3): a dangling * doc leaves a row with status 'dangling', the legacy pointer and the * reason — never a silent import. The natural uid over the slug makes a * re-run coalesce onto the same row. * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The receipt facts. * @task T13360 */ export declare function recordConduitDocMigration(projectRoot: string, input: RecordConduitDocMigrationInput): Promise<{ uid: string; inserted: boolean; }>; /** The migration receipt for one doc, or null when not migrated. */ export declare function findConduitDocMigration(projectRoot: string, slug: string): Promise; /** All migration receipts, slug ascending (the audit view). */ export declare function listConduitDocMigrations(projectRoot: string): Promise; //# sourceMappingURL=conduit-doc-history.d.ts.map