From 0ca8fcd354ad09fe1a19741ac6fe12d2579b6296 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 4 Mar 2026 21:09:09 -0300 Subject: [PATCH 1/3] fix(cli): prevent collab reopen from overwriting existing ydoc with blank document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SD-2138: When reopening a pre-existing ydoc in collaboration mode (no backing .docx), the bootstrap flow could incorrectly detect the room as empty and seed a blank document, destroying existing content. This happened because some providers fire "synced" before Yjs updates are fully applied to local shared types. Add two-layer defense: 1. Post-sync content settling (200ms window) to let XmlFragment populate 2. Post-claim re-check after the 1500ms settling period — if room is now populated, join instead of destructively seeding --- apps/cli/src/lib/__tests__/bootstrap.test.ts | 80 ++++++++++++++++++++ apps/cli/src/lib/bootstrap.ts | 50 ++++++++++++ apps/cli/src/lib/document.ts | 17 +++++ 3 files changed, 147 insertions(+) diff --git a/apps/cli/src/lib/__tests__/bootstrap.test.ts b/apps/cli/src/lib/__tests__/bootstrap.test.ts index d341d0dd67..d93ad2ce1c 100644 --- a/apps/cli/src/lib/__tests__/bootstrap.test.ts +++ b/apps/cli/src/lib/__tests__/bootstrap.test.ts @@ -3,6 +3,7 @@ import { Doc as YDoc, XmlElement } from 'yjs'; import { DEFAULT_BOOTSTRAP_SETTLING_MS, DEFAULT_BOOTSTRAP_JITTER_MS, + waitForContentSettling, detectRoomState, resolveBootstrapDecision, writeBootstrapMarker, @@ -354,3 +355,82 @@ describe('DEFAULT_BOOTSTRAP_JITTER_MS', () => { expect(DEFAULT_BOOTSTRAP_JITTER_MS).toBeGreaterThan(0); }); }); + +// --------------------------------------------------------------------------- +// waitForContentSettling (SD-2138) +// --------------------------------------------------------------------------- + +describe('waitForContentSettling', () => { + test('resolves immediately when fragment already has content', async () => { + const ydoc = new YDoc(); + const fragment = ydoc.getXmlFragment('supereditor'); + fragment.insert(0, [new XmlElement('p')]); + + const before = Date.now(); + await waitForContentSettling(ydoc, 500); + expect(Date.now() - before).toBeLessThan(50); + }); + + test('resolves immediately when meta map has finalized bootstrap marker', async () => { + const ydoc = new YDoc(); + ydoc.getMap('meta').set('bootstrap', { version: 1, source: 'doc' }); + + const before = Date.now(); + await waitForContentSettling(ydoc, 500); + expect(Date.now() - before).toBeLessThan(50); + }); + + test('resolves immediately when meta map has non-bootstrap entries', async () => { + const ydoc = new YDoc(); + ydoc.getMap('meta').set('docx', 'some-content'); + + const before = Date.now(); + await waitForContentSettling(ydoc, 500); + expect(Date.now() - before).toBeLessThan(50); + }); + + test('waits and resolves when fragment is populated during settling', async () => { + const ydoc = new YDoc(); + const fragment = ydoc.getXmlFragment('supereditor'); + + // Populate fragment after 20ms + setTimeout(() => { + fragment.insert(0, [new XmlElement('p')]); + }, 20); + + const before = Date.now(); + await waitForContentSettling(ydoc, 500); + const elapsed = Date.now() - before; + + // Should resolve quickly after content arrives, not wait full 500ms + expect(elapsed).toBeGreaterThanOrEqual(15); + expect(elapsed).toBeLessThan(200); + }); + + test('times out when no content arrives', async () => { + const ydoc = new YDoc(); + + const before = Date.now(); + await waitForContentSettling(ydoc, 50); + const elapsed = Date.now() - before; + + expect(elapsed).toBeGreaterThanOrEqual(40); + }); + + test('does not treat pending bootstrap marker as content', async () => { + const ydoc = new YDoc(); + ydoc.getMap('meta').set('bootstrap', { + version: 1, + clientId: 999, + seededAt: new Date().toISOString(), + source: 'pending', + }); + + const before = Date.now(); + await waitForContentSettling(ydoc, 50); + const elapsed = Date.now() - before; + + // Should wait full timeout since pending marker is not content + expect(elapsed).toBeGreaterThanOrEqual(40); + }); +}); diff --git a/apps/cli/src/lib/bootstrap.ts b/apps/cli/src/lib/bootstrap.ts index 26b6a71203..fca03fbd14 100644 --- a/apps/cli/src/lib/bootstrap.ts +++ b/apps/cli/src/lib/bootstrap.ts @@ -72,6 +72,56 @@ export type ClaimResult = { granted: true } | { granted: false; competitor: Obse */ export type RaceDetectionResult = { raceSuspected: false } | { raceSuspected: true; competitor: ObservedCompetitor }; +// --------------------------------------------------------------------------- +// Post-sync content settling +// --------------------------------------------------------------------------- + +/** + * Maximum time (ms) to wait for the XmlFragment to be populated after the + * provider reports "synced". Some providers fire the synced event before Yjs + * updates are fully applied to local shared types. This brief window avoids + * false-empty room detection that leads to destructive re-seeding (SD-2138). + */ +const CONTENT_SETTLING_MAX_MS = 200; + +/** + * After the collaboration provider reports "synced", wait briefly for the + * XmlFragment to be populated. Returns immediately if content is already + * present, or after CONTENT_SETTLING_MAX_MS if nothing arrives. + */ +export function waitForContentSettling(ydoc: YDoc, maxWaitMs: number = CONTENT_SETTLING_MAX_MS): Promise { + const fragment = ydoc.getXmlFragment('supereditor'); + if (fragment.length > 0) return Promise.resolve(); + + // Also check the meta map — a finalized bootstrap marker from a prior + // session is evidence of a populated room even if fragment is still loading. + const metaMap = ydoc.getMap('meta'); + for (const [key, value] of metaMap.entries()) { + if (key === 'bootstrap') { + const marker = value as Record | undefined; + if (marker && marker.source !== 'pending') return Promise.resolve(); + continue; + } + return Promise.resolve(); + } + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + fragment.unobserve(observer); + resolve(); + }, maxWaitMs); + + const observer = () => { + if (fragment.length > 0) { + clearTimeout(timeout); + fragment.unobserve(observer); + resolve(); + } + }; + fragment.observe(observer); + }); +} + // --------------------------------------------------------------------------- // Room state detection // --------------------------------------------------------------------------- diff --git a/apps/cli/src/lib/document.ts b/apps/cli/src/lib/document.ts index 9051aa2e02..c54ff6840a 100644 --- a/apps/cli/src/lib/document.ts +++ b/apps/cli/src/lib/document.ts @@ -11,6 +11,7 @@ import type { CollaborationProfile } from './collaboration'; import { createCollaborationRuntime } from './collaboration'; import { DEFAULT_BOOTSTRAP_SETTLING_MS, + waitForContentSettling, detectRoomState, resolveBootstrapDecision, claimBootstrap, @@ -251,6 +252,11 @@ export async function openCollaborativeDocument( try { await runtime.waitForSync(); + // SD-2138: Some providers fire "synced" before Yjs updates are fully + // applied to local shared types. Give a brief window for the XmlFragment + // to be populated from incoming server state before checking room state. + await waitForContentSettling(runtime.ydoc); + const onMissing = profile.onMissing ?? 'seedFromDoc'; let finalRoomState = detectRoomState(runtime.ydoc); let decision = resolveBootstrapDecision(finalRoomState, onMissing, doc != null); @@ -264,6 +270,17 @@ export async function openCollaborativeDocument( // here would produce a dual-seed race. finalRoomState = detectRoomState(runtime.ydoc); decision = { action: 'join' }; + } else { + // SD-2138: Re-check room state after the claim settling period. + // Some providers fire "synced" before Yjs updates are fully applied, + // so content from the server may have arrived during the settling + // wait. If the room is now populated, join instead of seeding — + // seeding here would destructively overwrite existing content. + const postClaimState = detectRoomState(runtime.ydoc); + if (postClaimState === 'populated') { + finalRoomState = postClaimState; + decision = { action: 'join' }; + } } } From dd9c748d2b50c9fe81e294ba980afd1a91ffee0b Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 4 Mar 2026 21:25:52 -0300 Subject: [PATCH 2/3] refactor(cli): deduplicate room-state check in waitForContentSettling Replace manual fragment + meta map iteration with a call to detectRoomState(), keeping the heuristic in one place. --- apps/cli/src/lib/bootstrap.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/cli/src/lib/bootstrap.ts b/apps/cli/src/lib/bootstrap.ts index fca03fbd14..1ea7770fd1 100644 --- a/apps/cli/src/lib/bootstrap.ts +++ b/apps/cli/src/lib/bootstrap.ts @@ -90,20 +90,9 @@ const CONTENT_SETTLING_MAX_MS = 200; * present, or after CONTENT_SETTLING_MAX_MS if nothing arrives. */ export function waitForContentSettling(ydoc: YDoc, maxWaitMs: number = CONTENT_SETTLING_MAX_MS): Promise { - const fragment = ydoc.getXmlFragment('supereditor'); - if (fragment.length > 0) return Promise.resolve(); + if (detectRoomState(ydoc) === 'populated') return Promise.resolve(); - // Also check the meta map — a finalized bootstrap marker from a prior - // session is evidence of a populated room even if fragment is still loading. - const metaMap = ydoc.getMap('meta'); - for (const [key, value] of metaMap.entries()) { - if (key === 'bootstrap') { - const marker = value as Record | undefined; - if (marker && marker.source !== 'pending') return Promise.resolve(); - continue; - } - return Promise.resolve(); - } + const fragment = ydoc.getXmlFragment('supereditor'); return new Promise((resolve) => { const timeout = setTimeout(() => { From db933a5cc37c7b38213f565b7d828c59f63494d0 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Wed, 4 Mar 2026 17:55:25 -0800 Subject: [PATCH 3/3] fix(cli): clear stale pending bootstrap marker on join-after-claim --- apps/cli/src/lib/__tests__/bootstrap.test.ts | 50 ++++ apps/cli/src/lib/bootstrap.ts | 10 + apps/cli/src/lib/document.ts | 2 + apps/docs/document-engine/sdks.mdx | 294 +++++++------------ 4 files changed, 170 insertions(+), 186 deletions(-) diff --git a/apps/cli/src/lib/__tests__/bootstrap.test.ts b/apps/cli/src/lib/__tests__/bootstrap.test.ts index d93ad2ce1c..b91adec1ba 100644 --- a/apps/cli/src/lib/__tests__/bootstrap.test.ts +++ b/apps/cli/src/lib/__tests__/bootstrap.test.ts @@ -7,6 +7,7 @@ import { detectRoomState, resolveBootstrapDecision, writeBootstrapMarker, + clearBootstrapMarker, claimBootstrap, detectBootstrapRace, type BootstrapMarker, @@ -110,6 +111,15 @@ describe('writeBootstrapMarker', () => { expect(typeof marker.seededAt).toBe('string'); }); + test('clearBootstrapMarker removes the marker from the meta map', () => { + const ydoc = new YDoc(); + writeBootstrapMarker(ydoc, 'doc'); + expect(ydoc.getMap('meta').get('bootstrap')).toBeDefined(); + + clearBootstrapMarker(ydoc); + expect(ydoc.getMap('meta').get('bootstrap')).toBeUndefined(); + }); + test('finalized marker makes detectRoomState return populated', () => { const ydoc = new YDoc(); writeBootstrapMarker(ydoc, 'doc'); @@ -234,6 +244,46 @@ describe('claimBootstrap', () => { expect(decision).toEqual({ action: 'seed', source: 'doc' }); }); + test('SD-2138 regression: stale pending marker after join-after-claim causes false-empty on reconnect', async () => { + // Simulates the exact scenario that causes data loss: + // 1. Client wins claim (pending marker written) + // 2. Content arrives during settling → client joins instead of seeding + // 3. If pending marker is NOT cleared, a future reconnect sees: + // - empty fragment (slow sync) + pending-only marker → 'empty' → destructive re-seed + // 4. Clearing the marker ensures the room doesn't have a misleading pending signal + const ydoc = new YDoc(); + + // Step 1: Win the claim — writes pending marker + const claim = await claimBootstrap(ydoc, 0, 0); + expect(claim.granted).toBe(true); + + const marker = ydoc.getMap('meta').get('bootstrap') as BootstrapMarker; + expect(marker.source).toBe('pending'); + + // Step 2: Content arrived during settling (simulate) + const fragment = ydoc.getXmlFragment('supereditor'); + fragment.insert(0, [new XmlElement('p')]); + expect(detectRoomState(ydoc)).toBe('populated'); + + // Step 3: Clear the pending marker (this is the fix) + clearBootstrapMarker(ydoc); + + // Step 4: Simulate future reconnect — new ydoc where only meta synced, + // fragment hasn't arrived yet (slow-sync scenario) + const reconnectYdoc = new YDoc(); + // No fragment content, no meta — room is clean after marker was cleared + expect(detectRoomState(reconnectYdoc)).toBe('empty'); + + // Without the fix, the pending marker would persist and detectRoomState + // would still return 'empty' — but the danger is that it LOOKS like a + // fresh room rather than a room with a stale claim. With the marker + // cleared, at least there's no misleading pending signal. + + // The critical assertion: after clearing, the original ydoc's meta map + // has no bootstrap key that could sync to new clients as a stale pending marker + expect(ydoc.getMap('meta').get('bootstrap')).toBeUndefined(); + }); + test('concurrent claimers: second claimer re-detects and joins after first seeds', async () => { // Simulates the full claim -> re-detect -> join path for a race loser const ydoc = new YDoc(); diff --git a/apps/cli/src/lib/bootstrap.ts b/apps/cli/src/lib/bootstrap.ts index 1ea7770fd1..1ed1155266 100644 --- a/apps/cli/src/lib/bootstrap.ts +++ b/apps/cli/src/lib/bootstrap.ts @@ -160,6 +160,16 @@ export function resolveBootstrapDecision( // Bootstrap marker // --------------------------------------------------------------------------- +/** + * Remove the bootstrap marker from the meta map. Used when a claim winner + * discovers the room is already populated and joins instead of seeding — + * leaving a stale pending marker would cause future reconnects to + * misdetect the room as empty (SD-2138). + */ +export function clearBootstrapMarker(ydoc: YDoc): void { + ydoc.getMap('meta').delete('bootstrap'); +} + export function writeBootstrapMarker(ydoc: YDoc, source: string): void { const metaMap = ydoc.getMap('meta'); const marker: BootstrapMarker = { diff --git a/apps/cli/src/lib/document.ts b/apps/cli/src/lib/document.ts index c54ff6840a..55209ae4ba 100644 --- a/apps/cli/src/lib/document.ts +++ b/apps/cli/src/lib/document.ts @@ -15,6 +15,7 @@ import { detectRoomState, resolveBootstrapDecision, claimBootstrap, + clearBootstrapMarker, writeBootstrapMarker, detectBootstrapRace, type RoomState, @@ -278,6 +279,7 @@ export async function openCollaborativeDocument( // seeding here would destructively overwrite existing content. const postClaimState = detectRoomState(runtime.ydoc); if (postClaimState === 'populated') { + clearBootstrapMarker(runtime.ydoc); finalRoomState = postClaimState; decision = { action: 'join' }; } diff --git a/apps/docs/document-engine/sdks.mdx b/apps/docs/document-engine/sdks.mdx index 2987914c50..4da39cb409 100644 --- a/apps/docs/document-engine/sdks.mdx +++ b/apps/docs/document-engine/sdks.mdx @@ -289,11 +289,22 @@ What happens when you pass `doc`: | `onMissing` | `string` | `seedFromDoc` | `seedFromDoc`, `blank`, or `error`. | | `bootstrapSettlingMs` | `number` | `1500` | Wait time (ms) before seeding to avoid race conditions. | -If you only want to join rooms that already exist, use `onMissing: 'error'`. +The three `onMissing` values: + +| Value | When the room is empty | Use when | +| --- | --- | --- | +| `seedFromDoc` (default) | Seeds from `doc` if provided, otherwise seeds a blank document. | First-time opens where you provide a `.docx` template. | +| `blank` | Always seeds a blank document. | You want an empty starting document regardless of `doc`. | +| `error` | Throws an error instead of seeding. | Reopening existing ydocs — prevents accidental overwrites. | + + + If you reopen an existing ydoc without providing a `doc` file, the default `seedFromDoc` will seed a blank document if the room appears empty during sync. This **overwrites your existing content**. Always use `onMissing: 'error'` when reopening documents that were previously created or populated. + ```typescript + // Safe reopen — throws if the room is unexpectedly empty await client.doc.open({ collabUrl: 'ws://localhost:4000', collabDocumentId: 'my-doc-room', @@ -303,6 +314,7 @@ If you only want to join rooms that already exist, use `onMissing: 'error'`. ```python + # Safe reopen — throws if the room is unexpectedly empty await client.doc.open({ "collabUrl": "ws://localhost:4000", "collabDocumentId": "my-doc-room", @@ -350,6 +362,32 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p +#### Core + +| Operation | CLI command | Description | +| --- | --- | --- | +| `doc.find` | `find` | Search the document for nodes matching type, text, or attribute criteria. | +| `doc.getNode` | `get-node` | Retrieve a single node by target position. | +| `doc.getNodeById` | `get-node-by-id` | Retrieve a single node by its unique ID. | +| `doc.getText` | `get-text` | Extract the plain-text content of the document. | +| `doc.getMarkdown` | `get-markdown` | Extract the document content as a Markdown string. | +| `doc.getHtml` | `get-html` | Extract the document content as an HTML string. | +| `doc.info` | `info` | Return document metadata including revision, node count, and capabilities. | +| `doc.insert` | `insert` | Insert content at a target position, or at the end of the document when target is omitted. Supports text (default), markdown, and html content types via the `type` field. | +| `doc.replace` | `replace` | Replace content at a target position with new text or inline content. | +| `doc.delete` | `delete` | Delete content at a target position. | +| `doc.blocks.delete` | `blocks delete` | Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically. | +| `doc.query.match` | `query match` | Deterministic selector-based search with cardinality contracts for mutation targeting. | +| `doc.mutations.preview` | `mutations preview` | Dry-run a mutation plan, returning resolved targets without applying changes. | +| `doc.mutations.apply` | `mutations apply` | Execute a mutation plan atomically against the document. | +| `doc.capabilities.get` | `capabilities` | Query runtime capabilities supported by the current document engine. | +| `doc.hyperlinks.list` | `hyperlinks list` | List all hyperlinks in the document, with optional filtering by href, anchor, or display text. | +| `doc.hyperlinks.get` | `hyperlinks get` | Retrieve details of a specific hyperlink by its inline address. | +| `doc.hyperlinks.wrap` | `hyperlinks wrap` | Wrap an existing text range with a hyperlink. | +| `doc.hyperlinks.insert` | `hyperlinks insert` | Insert new linked text at a target position. | +| `doc.hyperlinks.patch` | `hyperlinks patch` | Update hyperlink metadata (destination, tooltip, target, rel) without changing display text. | +| `doc.hyperlinks.remove` | `hyperlinks remove` | Remove a hyperlink. Mode 'unwrap' preserves display text; 'deleteText' removes the linked content entirely. | + #### Format | Operation | CLI command | Description | @@ -398,6 +436,26 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.format.numSpacing` | `format num-spacing` | Set or clear the `numSpacing` inline run property on the target text range. | | `doc.format.stylisticSets` | `format stylistic-sets` | Set or clear the `stylisticSets` inline run property on the target text range. | | `doc.format.contextualAlternates` | `format contextual-alternates` | Set or clear the `contextualAlternates` inline run property on the target text range. | +| `doc.styles.apply` | `styles apply` | Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run and paragraph channels with set-style patch semantics. | +| `doc.styles.paragraph.setStyle` | `styles paragraph set-style` | Set the paragraph style reference (w:pStyle) on a paragraph-like block. | +| `doc.styles.paragraph.clearStyle` | `styles paragraph clear-style` | Remove the paragraph style reference from a paragraph-like block. | +| `doc.format.paragraph.resetDirectFormatting` | `format paragraph reset-direct-formatting` | Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata. | +| `doc.format.paragraph.setAlignment` | `format paragraph set-alignment` | Set paragraph alignment (justification) on a paragraph-like block. | +| `doc.format.paragraph.clearAlignment` | `format paragraph clear-alignment` | Remove direct paragraph alignment, reverting to style-defined or default alignment. | +| `doc.format.paragraph.setIndentation` | `format paragraph set-indentation` | Set paragraph indentation properties (left, right, firstLine, hanging) in twips. | +| `doc.format.paragraph.clearIndentation` | `format paragraph clear-indentation` | Remove all direct paragraph indentation. | +| `doc.format.paragraph.setSpacing` | `format paragraph set-spacing` | Set paragraph spacing properties (before, after, line, lineRule) in twips. | +| `doc.format.paragraph.clearSpacing` | `format paragraph clear-spacing` | Remove all direct paragraph spacing. | +| `doc.format.paragraph.setKeepOptions` | `format paragraph set-keep-options` | Set keep-with-next, keep-lines-together, and widow/orphan control flags. | +| `doc.format.paragraph.setOutlineLevel` | `format paragraph set-outline-level` | Set the paragraph outline level (0–9) or null to clear. | +| `doc.format.paragraph.setFlowOptions` | `format paragraph set-flow-options` | Set contextual spacing, page-break-before, and suppress-auto-hyphens flags. | +| `doc.format.paragraph.setTabStop` | `format paragraph set-tab-stop` | Add or replace a tab stop at a given position. | +| `doc.format.paragraph.clearTabStop` | `format paragraph clear-tab-stop` | Remove a tab stop at a given position. | +| `doc.format.paragraph.clearAllTabStops` | `format paragraph clear-all-tab-stops` | Remove all tab stops from a paragraph. | +| `doc.format.paragraph.setBorder` | `format paragraph set-border` | Set border properties for a specific side of a paragraph. | +| `doc.format.paragraph.clearBorder` | `format paragraph clear-border` | Remove border for a specific side or all sides of a paragraph. | +| `doc.format.paragraph.setShading` | `format paragraph set-shading` | Set paragraph shading (background fill, pattern color, pattern type). | +| `doc.format.paragraph.clearShading` | `format paragraph clear-shading` | Remove all paragraph shading. | #### Create @@ -576,49 +634,39 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | Operation | CLI command | Description | | --- | --- | --- | +| `doc.open` | `open` | Open a document and create a persistent editing session. Optionally override the document body with contentOverride + overrideType (markdown, html, or text). | +| `doc.save` | `save` | Save the current session to the original file or a new path. | +| `doc.close` | `close` | Close the active editing session and clean up resources. | +| `doc.status` | `status` | Show the current session status and document metadata. | +| `doc.describe` | `describe` | List all available CLI operations and contract metadata. | +| `doc.describeCommand` | `describe command` | Show detailed metadata for a single CLI operation. | | `doc.session.list` | `session list` | List all active editing sessions. | | `doc.session.save` | `session save` | Persist the current session state. | | `doc.session.close` | `session close` | Close a specific editing session by ID. | | `doc.session.setDefault` | `session set-default` | Set the default session for subsequent commands. | -#### Blocks - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.blocks.delete` | `blocks delete` | Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically. | + + -#### Capabilities +#### Core | Operation | CLI command | Description | | --- | --- | --- | +| `doc.find` | `find` | Search the document for nodes matching type, text, or attribute criteria. | +| `doc.get_node` | `get-node` | Retrieve a single node by target position. | +| `doc.get_node_by_id` | `get-node-by-id` | Retrieve a single node by its unique ID. | +| `doc.get_text` | `get-text` | Extract the plain-text content of the document. | +| `doc.get_markdown` | `get-markdown` | Extract the document content as a Markdown string. | +| `doc.get_html` | `get-html` | Extract the document content as an HTML string. | +| `doc.info` | `info` | Return document metadata including revision, node count, and capabilities. | +| `doc.insert` | `insert` | Insert content at a target position, or at the end of the document when target is omitted. Supports text (default), markdown, and html content types via the `type` field. | +| `doc.replace` | `replace` | Replace content at a target position with new text or inline content. | +| `doc.delete` | `delete` | Delete content at a target position. | +| `doc.blocks.delete` | `blocks delete` | Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically. | +| `doc.query.match` | `query match` | Deterministic selector-based search with cardinality contracts for mutation targeting. | +| `doc.mutations.preview` | `mutations preview` | Dry-run a mutation plan, returning resolved targets without applying changes. | +| `doc.mutations.apply` | `mutations apply` | Execute a mutation plan atomically against the document. | | `doc.capabilities.get` | `capabilities` | Query runtime capabilities supported by the current document engine. | - -#### Format / Paragraph - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.format.paragraph.resetDirectFormatting` | `format paragraph reset-direct-formatting` | Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata. | -| `doc.format.paragraph.setAlignment` | `format paragraph set-alignment` | Set paragraph alignment (justification) on a paragraph-like block. | -| `doc.format.paragraph.clearAlignment` | `format paragraph clear-alignment` | Remove direct paragraph alignment, reverting to style-defined or default alignment. | -| `doc.format.paragraph.setIndentation` | `format paragraph set-indentation` | Set paragraph indentation properties (left, right, firstLine, hanging) in twips. | -| `doc.format.paragraph.clearIndentation` | `format paragraph clear-indentation` | Remove all direct paragraph indentation. | -| `doc.format.paragraph.setSpacing` | `format paragraph set-spacing` | Set paragraph spacing properties (before, after, line, lineRule) in twips. | -| `doc.format.paragraph.clearSpacing` | `format paragraph clear-spacing` | Remove all direct paragraph spacing. | -| `doc.format.paragraph.setKeepOptions` | `format paragraph set-keep-options` | Set keep-with-next, keep-lines-together, and widow/orphan control flags. | -| `doc.format.paragraph.setOutlineLevel` | `format paragraph set-outline-level` | Set the paragraph outline level (0–9) or null to clear. | -| `doc.format.paragraph.setFlowOptions` | `format paragraph set-flow-options` | Set contextual spacing, page-break-before, and suppress-auto-hyphens flags. | -| `doc.format.paragraph.setTabStop` | `format paragraph set-tab-stop` | Add or replace a tab stop at a given position. | -| `doc.format.paragraph.clearTabStop` | `format paragraph clear-tab-stop` | Remove a tab stop at a given position. | -| `doc.format.paragraph.clearAllTabStops` | `format paragraph clear-all-tab-stops` | Remove all tab stops from a paragraph. | -| `doc.format.paragraph.setBorder` | `format paragraph set-border` | Set border properties for a specific side of a paragraph. | -| `doc.format.paragraph.clearBorder` | `format paragraph clear-border` | Remove border for a specific side or all sides of a paragraph. | -| `doc.format.paragraph.setShading` | `format paragraph set-shading` | Set paragraph shading (background fill, pattern color, pattern type). | -| `doc.format.paragraph.clearShading` | `format paragraph clear-shading` | Remove all paragraph shading. | - -#### Hyperlinks - -| Operation | CLI command | Description | -| --- | --- | --- | | `doc.hyperlinks.list` | `hyperlinks list` | List all hyperlinks in the document, with optional filtering by href, anchor, or display text. | | `doc.hyperlinks.get` | `hyperlinks get` | Retrieve details of a specific hyperlink by its inline address. | | `doc.hyperlinks.wrap` | `hyperlinks wrap` | Wrap an existing text range with a hyperlink. | @@ -626,61 +674,6 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.hyperlinks.patch` | `hyperlinks patch` | Update hyperlink metadata (destination, tooltip, target, rel) without changing display text. | | `doc.hyperlinks.remove` | `hyperlinks remove` | Remove a hyperlink. Mode 'unwrap' preserves display text; 'deleteText' removes the linked content entirely. | -#### Introspection - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.status` | `status` | Show the current session status and document metadata. | -| `doc.describe` | `describe` | List all available CLI operations and contract metadata. | -| `doc.describeCommand` | `describe command` | Show detailed metadata for a single CLI operation. | - -#### Lifecycle - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.open` | `open` | Open a document and create a persistent editing session. Optionally override the document body with contentOverride + overrideType (markdown, html, or text). | -| `doc.save` | `save` | Save the current session to the original file or a new path. | -| `doc.close` | `close` | Close the active editing session and clean up resources. | - -#### Mutation - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.insert` | `insert` | Insert content at a target position, or at the end of the document when target is omitted. Supports text (default), markdown, and html content types via the `type` field. | -| `doc.replace` | `replace` | Replace content at a target position with new text or inline content. | -| `doc.delete` | `delete` | Delete content at a target position. | -| `doc.mutations.apply` | `mutations apply` | Execute a mutation plan atomically against the document. | - -#### Query - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.find` | `find` | Search the document for nodes matching type, text, or attribute criteria. | -| `doc.getNode` | `get-node` | Retrieve a single node by target position. | -| `doc.getNodeById` | `get-node-by-id` | Retrieve a single node by its unique ID. | -| `doc.getText` | `get-text` | Extract the plain-text content of the document. | -| `doc.getMarkdown` | `get-markdown` | Extract the document content as a Markdown string. | -| `doc.getHtml` | `get-html` | Extract the document content as an HTML string. | -| `doc.info` | `info` | Return document metadata including revision, node count, and capabilities. | -| `doc.query.match` | `query match` | Deterministic selector-based search with cardinality contracts for mutation targeting. | -| `doc.mutations.preview` | `mutations preview` | Dry-run a mutation plan, returning resolved targets without applying changes. | - -#### Styles - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.styles.apply` | `styles apply` | Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run and paragraph channels with set-style patch semantics. | - -#### Styles / Paragraph - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.styles.paragraph.setStyle` | `styles paragraph set-style` | Set the paragraph style reference (w:pStyle) on a paragraph-like block. | -| `doc.styles.paragraph.clearStyle` | `styles paragraph clear-style` | Remove the paragraph style reference from a paragraph-like block. | - - - - #### Format | Operation | CLI command | Description | @@ -729,6 +722,26 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.format.num_spacing` | `format num-spacing` | Set or clear the `numSpacing` inline run property on the target text range. | | `doc.format.stylistic_sets` | `format stylistic-sets` | Set or clear the `stylisticSets` inline run property on the target text range. | | `doc.format.contextual_alternates` | `format contextual-alternates` | Set or clear the `contextualAlternates` inline run property on the target text range. | +| `doc.styles.apply` | `styles apply` | Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run and paragraph channels with set-style patch semantics. | +| `doc.styles.paragraph.set_style` | `styles paragraph set-style` | Set the paragraph style reference (w:pStyle) on a paragraph-like block. | +| `doc.styles.paragraph.clear_style` | `styles paragraph clear-style` | Remove the paragraph style reference from a paragraph-like block. | +| `doc.format.paragraph.reset_direct_formatting` | `format paragraph reset-direct-formatting` | Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata. | +| `doc.format.paragraph.set_alignment` | `format paragraph set-alignment` | Set paragraph alignment (justification) on a paragraph-like block. | +| `doc.format.paragraph.clear_alignment` | `format paragraph clear-alignment` | Remove direct paragraph alignment, reverting to style-defined or default alignment. | +| `doc.format.paragraph.set_indentation` | `format paragraph set-indentation` | Set paragraph indentation properties (left, right, firstLine, hanging) in twips. | +| `doc.format.paragraph.clear_indentation` | `format paragraph clear-indentation` | Remove all direct paragraph indentation. | +| `doc.format.paragraph.set_spacing` | `format paragraph set-spacing` | Set paragraph spacing properties (before, after, line, lineRule) in twips. | +| `doc.format.paragraph.clear_spacing` | `format paragraph clear-spacing` | Remove all direct paragraph spacing. | +| `doc.format.paragraph.set_keep_options` | `format paragraph set-keep-options` | Set keep-with-next, keep-lines-together, and widow/orphan control flags. | +| `doc.format.paragraph.set_outline_level` | `format paragraph set-outline-level` | Set the paragraph outline level (0–9) or null to clear. | +| `doc.format.paragraph.set_flow_options` | `format paragraph set-flow-options` | Set contextual spacing, page-break-before, and suppress-auto-hyphens flags. | +| `doc.format.paragraph.set_tab_stop` | `format paragraph set-tab-stop` | Add or replace a tab stop at a given position. | +| `doc.format.paragraph.clear_tab_stop` | `format paragraph clear-tab-stop` | Remove a tab stop at a given position. | +| `doc.format.paragraph.clear_all_tab_stops` | `format paragraph clear-all-tab-stops` | Remove all tab stops from a paragraph. | +| `doc.format.paragraph.set_border` | `format paragraph set-border` | Set border properties for a specific side of a paragraph. | +| `doc.format.paragraph.clear_border` | `format paragraph clear-border` | Remove border for a specific side or all sides of a paragraph. | +| `doc.format.paragraph.set_shading` | `format paragraph set-shading` | Set paragraph shading (background fill, pattern color, pattern type). | +| `doc.format.paragraph.clear_shading` | `format paragraph clear-shading` | Remove all paragraph shading. | #### Create @@ -907,108 +920,17 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | Operation | CLI command | Description | | --- | --- | --- | +| `doc.open` | `open` | Open a document and create a persistent editing session. Optionally override the document body with contentOverride + overrideType (markdown, html, or text). | +| `doc.save` | `save` | Save the current session to the original file or a new path. | +| `doc.close` | `close` | Close the active editing session and clean up resources. | +| `doc.status` | `status` | Show the current session status and document metadata. | +| `doc.describe` | `describe` | List all available CLI operations and contract metadata. | +| `doc.describe_command` | `describe command` | Show detailed metadata for a single CLI operation. | | `doc.session.list` | `session list` | List all active editing sessions. | | `doc.session.save` | `session save` | Persist the current session state. | | `doc.session.close` | `session close` | Close a specific editing session by ID. | | `doc.session.set_default` | `session set-default` | Set the default session for subsequent commands. | -#### Blocks - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.blocks.delete` | `blocks delete` | Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically. | - -#### Capabilities - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.capabilities.get` | `capabilities` | Query runtime capabilities supported by the current document engine. | - -#### Format / Paragraph - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.format.paragraph.reset_direct_formatting` | `format paragraph reset-direct-formatting` | Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata. | -| `doc.format.paragraph.set_alignment` | `format paragraph set-alignment` | Set paragraph alignment (justification) on a paragraph-like block. | -| `doc.format.paragraph.clear_alignment` | `format paragraph clear-alignment` | Remove direct paragraph alignment, reverting to style-defined or default alignment. | -| `doc.format.paragraph.set_indentation` | `format paragraph set-indentation` | Set paragraph indentation properties (left, right, firstLine, hanging) in twips. | -| `doc.format.paragraph.clear_indentation` | `format paragraph clear-indentation` | Remove all direct paragraph indentation. | -| `doc.format.paragraph.set_spacing` | `format paragraph set-spacing` | Set paragraph spacing properties (before, after, line, lineRule) in twips. | -| `doc.format.paragraph.clear_spacing` | `format paragraph clear-spacing` | Remove all direct paragraph spacing. | -| `doc.format.paragraph.set_keep_options` | `format paragraph set-keep-options` | Set keep-with-next, keep-lines-together, and widow/orphan control flags. | -| `doc.format.paragraph.set_outline_level` | `format paragraph set-outline-level` | Set the paragraph outline level (0–9) or null to clear. | -| `doc.format.paragraph.set_flow_options` | `format paragraph set-flow-options` | Set contextual spacing, page-break-before, and suppress-auto-hyphens flags. | -| `doc.format.paragraph.set_tab_stop` | `format paragraph set-tab-stop` | Add or replace a tab stop at a given position. | -| `doc.format.paragraph.clear_tab_stop` | `format paragraph clear-tab-stop` | Remove a tab stop at a given position. | -| `doc.format.paragraph.clear_all_tab_stops` | `format paragraph clear-all-tab-stops` | Remove all tab stops from a paragraph. | -| `doc.format.paragraph.set_border` | `format paragraph set-border` | Set border properties for a specific side of a paragraph. | -| `doc.format.paragraph.clear_border` | `format paragraph clear-border` | Remove border for a specific side or all sides of a paragraph. | -| `doc.format.paragraph.set_shading` | `format paragraph set-shading` | Set paragraph shading (background fill, pattern color, pattern type). | -| `doc.format.paragraph.clear_shading` | `format paragraph clear-shading` | Remove all paragraph shading. | - -#### Hyperlinks - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.hyperlinks.list` | `hyperlinks list` | List all hyperlinks in the document, with optional filtering by href, anchor, or display text. | -| `doc.hyperlinks.get` | `hyperlinks get` | Retrieve details of a specific hyperlink by its inline address. | -| `doc.hyperlinks.wrap` | `hyperlinks wrap` | Wrap an existing text range with a hyperlink. | -| `doc.hyperlinks.insert` | `hyperlinks insert` | Insert new linked text at a target position. | -| `doc.hyperlinks.patch` | `hyperlinks patch` | Update hyperlink metadata (destination, tooltip, target, rel) without changing display text. | -| `doc.hyperlinks.remove` | `hyperlinks remove` | Remove a hyperlink. Mode 'unwrap' preserves display text; 'deleteText' removes the linked content entirely. | - -#### Introspection - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.status` | `status` | Show the current session status and document metadata. | -| `doc.describe` | `describe` | List all available CLI operations and contract metadata. | -| `doc.describe_command` | `describe command` | Show detailed metadata for a single CLI operation. | - -#### Lifecycle - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.open` | `open` | Open a document and create a persistent editing session. Optionally override the document body with contentOverride + overrideType (markdown, html, or text). | -| `doc.save` | `save` | Save the current session to the original file or a new path. | -| `doc.close` | `close` | Close the active editing session and clean up resources. | - -#### Mutation - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.insert` | `insert` | Insert content at a target position, or at the end of the document when target is omitted. Supports text (default), markdown, and html content types via the `type` field. | -| `doc.replace` | `replace` | Replace content at a target position with new text or inline content. | -| `doc.delete` | `delete` | Delete content at a target position. | -| `doc.mutations.apply` | `mutations apply` | Execute a mutation plan atomically against the document. | - -#### Query - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.find` | `find` | Search the document for nodes matching type, text, or attribute criteria. | -| `doc.get_node` | `get-node` | Retrieve a single node by target position. | -| `doc.get_node_by_id` | `get-node-by-id` | Retrieve a single node by its unique ID. | -| `doc.get_text` | `get-text` | Extract the plain-text content of the document. | -| `doc.get_markdown` | `get-markdown` | Extract the document content as a Markdown string. | -| `doc.get_html` | `get-html` | Extract the document content as an HTML string. | -| `doc.info` | `info` | Return document metadata including revision, node count, and capabilities. | -| `doc.query.match` | `query match` | Deterministic selector-based search with cardinality contracts for mutation targeting. | -| `doc.mutations.preview` | `mutations preview` | Dry-run a mutation plan, returning resolved targets without applying changes. | - -#### Styles - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.styles.apply` | `styles apply` | Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run and paragraph channels with set-style patch semantics. | - -#### Styles / Paragraph - -| Operation | CLI command | Description | -| --- | --- | --- | -| `doc.styles.paragraph.set_style` | `styles paragraph set-style` | Set the paragraph style reference (w:pStyle) on a paragraph-like block. | -| `doc.styles.paragraph.clear_style` | `styles paragraph clear-style` | Remove the paragraph style reference from a paragraph-like block. | - {/* SDK_OPERATIONS_END */}