/** * Docs update write path through the section CRDT merge layer (T13355 · * spec `cleodocs-rearch-design` §2 option b · D11165). * * Every `cleo docs update` carrying new inline text is projected through * this module after the legacy whole-row write lands (`docs-update.ts`): * * 1. Resolve the doc's immutable uid, adopting a legacy doc into the * conduit family on first sight ({@link ensureConduitDoc}). * 2. Split the new text into sections and resolve the previous head's * section texts from its version blob — the single-writer vs. * concurrent discriminator (T13589). * 3. Apply each section's edit to its local Loro state * (`section-merge.ts`): while the stored state equals the head's text * the input projects EXACTLY (re-seed; replaces and deletions apply * verbatim — the primitives can only append, so re-seeding is the * honest encoding); a concurrent edit that is a pure append composes * onto the stored state so both survive (T13585); a concurrent * NON-append edit is never fabricated into a merge — it is landed as * its own version row and recorded as a head conflict (T13595). * 4. Read the MERGED text back out of the merged Loro states — never out * of the input — and write its content-addressed blob so the version * row's `storage_key` resolves. * 5. Append the merged text as a DAG version row * (`appendConduitAttachmentVersion`, parent = the previous head's * version) — history is append-only. * 6. Project the merged text into the current-version row via * {@link upsertConduitDocHead}: the max-HLC head wins and a losing * concurrent head is recorded as a conflict, never silently shadowed * (typed rule `docs.head.max-hlc`). * 7. Rebase the local section states onto the landed version and rebuild * the derived contributors rollup. * * Steps 1–7 run inside ONE `BEGIN IMMEDIATE` transaction * ({@link withConduitDocWriteTransaction}, T13583): a failure after the * version append rolls everything back, so the next update cannot fork * onto a half-landed projection. The body uses only sync-driver accessors * and sync `node:fs` calls, so no real I/O yield can admit another writer * on the connection mid-transaction (the documented constraint). * * Loro owns LOCAL-ONLY section state; the syncing rows are owned by the * Stage C journal merge engine — one merge authority per table (§2). * * @module * @task T13355 * @epic T13340 * @saga T13339 */ import { type ConduitAttachmentVersionRow, type UpsertConduitDocHeadResult } from '../store/conduit-doc-history.js'; /** * TTL granted by the write path's lease acquisitions (seconds). Inside * llmtxt's 1–300 s range; long enough to cover an edit burst, short enough * that a dead session's lease dies quickly (AC4: lease state never outlives * its session without renew). */ export declare const SECTION_LEASE_TTL_SECONDS = 60; /** Input for {@link projectDocUpdateThroughSections}. */ export interface ProjectDocUpdateInput { /** The doc's live slug (or a known old slug). */ readonly slug: string; /** Revision author agent id. */ readonly authorAgentId: string; /** The full new document text. */ readonly content: string; /** * The session performing the edit. Callers that know their session (tests, * sync apply) pass it explicitly; otherwise the projection resolves the * caller's BOUND session ({@link resolveBoundSession}, gate 16 — never a * bare `getActiveSession()`). With no session the edit proceeds * UNCOORDINATED: leases are per-session state, so there is nothing to * acquire or renew under. */ readonly sessionId?: string; /** * The candidate head's HLC (journal wire form). Omit to mint one locally: * the mint ticks forward from this device's last issued HLC and never * lands at or below the stored head, so a local write always advances the * projection. Sync callers pass the remote HLC to exercise the max-HLC * rule. */ readonly hlc?: string; /** * Explicit DAG parent (sync apply). Omit to chain onto the current head's * version; `null` forces a root version. */ readonly parentVersionUid?: string | null; /** * The section texts of the version this edit was AUTHORED AGAINST * (T13674) — the write path's stale-base discriminator. Callers that * fetched a head and declared it pass its section texts (resolved and * validated before any write); every section is then merged relative to * that base, so an edit authored against a superseded head composes onto * the stored state (append) or records a conflict (non-append) instead of * re-seeding over a concurrent writer's edit. Omit (or null) to assert * single-writer semantics against the previous head — the pre-T13674 * behavior. */ readonly baseSections?: readonly string[] | null; /** Optional change summary for the version row. */ readonly changeSummary?: string; } /** Outcome of {@link projectDocUpdateThroughSections}. */ export interface ProjectDocUpdateResult { /** The doc's immutable uid (`conduit_attachments.uid`). */ readonly docUid: string; /** Whether this call adopted the doc into the conduit family. */ readonly adopted: boolean; /** The merged document text, extracted from the merged Loro states. */ readonly mergedText: string; /** Section ids whose state changed in this projection. */ readonly sectionsModified: readonly string[]; /** The appended DAG version row. */ readonly version: ConduitAttachmentVersionRow; /** The head upsert outcome (advanced vs. conflict recorded). */ readonly head: UpsertConduitDocHeadResult; /** * The head version uid this projection chained onto (`null` when the doc * had no head yet). Callers that declared a base compare it against their * base to detect a stale merge (T13674). */ readonly previousHeadVersionUid: string | null; /** * Non-append concurrent section edits recorded as conflicts (T13595): the * incoming edit could not be composed without fabricating text, so it was * landed as its own version row (append-only history preserves both) while * the head keeps the stored projection. */ readonly conflicts: readonly SectionConflict[]; /** * Advisory lease notices (T13356): sections this edit touched are leased * by ANOTHER live session. The edit proceeded anyway — leases are * advisory ("409 is a social signal, not a hard lock", llmtxt T285). */ readonly leaseNotices: readonly SectionLeaseNotice[]; } /** One recorded non-append concurrent section edit (T13595). */ export interface SectionConflict { /** The section that could not be merged. */ readonly sectionId: string; /** The version row carrying the incoming (authored) edit — the loser. */ readonly incomingVersionUid: string; /** The projection version row the head kept — the winner. */ readonly projectionVersionUid: string; } /** One advisory lease notice (T13356): the section is leased elsewhere. */ export interface SectionLeaseNotice { /** The leased section the edit touched. */ readonly sectionId: string; /** The current holder agent id. */ readonly holder: string; /** The current holder's session id. */ readonly sessionId: string; /** When the blocking lease expires without renew (ISO-8601). */ readonly expiresAt: string; } /** * Project one docs update through the section CRDT merge layer. * * The whole projection — adopt, section-state writes, version append, head * upsert, rebase, contributors rebuild — is one transaction * ({@link withConduitDocWriteTransaction}); the merged text is read back * from the merged Loro states, so concurrent same-base section edits both * survive into the version row and the head projection. * * Lease coordination (T13356, ADVISORY): before the transaction, the * projection resolves the caller's session (input, else the bound session, * gate 16) and acquires/renews a lease for every section the edit touches. * A section leased by another live session yields a {@link SectionLeaseNotice} * — the 409 equivalent as data. The edit PROCEEDS regardless: "409 is a * social signal, not a hard lock" (llmtxt T285); the CRDT layer guarantees * nothing is lost when coordination fails. Leases acquired for a projection * that then rolls back are released, so a failed edit leaves no stale claim. * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The update facts. * @returns The merge outcome. */ export declare function projectDocUpdateThroughSections(projectRoot: string, input: ProjectDocUpdateInput): Promise; /** Outcome of {@link previewDocUpdateThroughSections}. */ export interface DocUpdatePreview { /** The merged text the projection would land. */ readonly mergedText: string; /** Whether the head would advance to that text (max-HLC rule). */ readonly wouldAdvance: boolean; /** * The section conflicts the projection would record. Their version uids * name rolled-back rows: use `sectionId` only. */ readonly conflicts: readonly SectionConflict[]; } /** * Preview a docs update without committing it (T13842): runs the exact * projection — merge, adopt, version append, head upsert — inside its * transaction and rolls it back, so a caller can write the fetch-visible row * from the merged text BEFORE the projection commits. Acquires no leases. * Another writer can still land between the preview and the real * projection; callers compare the committed `mergedText` with the preview. * A content-addressed blob may remain as a harmless orphan. * * @param projectRoot - Absolute path to the CLEO project root. * @param input - The update facts, exactly as for {@link projectDocUpdateThroughSections}. * @returns The merged text, whether the head would advance, and the conflicts. */ export declare function previewDocUpdateThroughSections(projectRoot: string, input: ProjectDocUpdateInput): Promise; /** * Release every section lease this session holds for a doc — the "release * on completion" path of T13356 AC2: a session calls this when it finishes * editing the doc, so the sections are free for the next editor before the * TTL would have expired them. * * @param projectRoot - Absolute path to the CLEO project root. * @param slug - The doc's live slug (leases are keyed by it). * @param sessionId - The finishing session (only its own leases are released). * @returns The section ids released. */ export declare function releaseSectionLeases(projectRoot: string, slug: string, sessionId: string): Promise; //# sourceMappingURL=section-write-path.d.ts.map