Skip to content

Commit 2a804f7

Browse files
authored
fix(document-api): add document diff API and fix tracked diff replay in CLI host session (#2418)
* fix(document-api): add document diff API and fix tracked diff replay in CLI host session * chore: update docs * fix: diff comment fingerprinting and payload aliasing
1 parent 09677dc commit 2a804f7

47 files changed

Lines changed: 3499 additions & 51 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/cli/src/cli/operation-hints.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ export const SUCCESS_VERB: Record<CliExposedOperationId, string> = {
208208
'images.setPosition': 'set position',
209209
'images.setAnchorOptions': 'set anchor options',
210210
'images.setZOrder': 'set z-order',
211+
212+
// Diff
213+
'diff.capture': 'captured snapshot',
214+
'diff.compare': 'compared documents',
215+
'diff.apply': 'applied diff',
211216
};
212217

213218
// ---------------------------------------------------------------------------
@@ -235,7 +240,10 @@ export type OutputFormat =
235240
| 'documentInfo'
236241
| 'receipt'
237242
| 'plain'
238-
| 'void';
243+
| 'void'
244+
| 'diffSnapshot'
245+
| 'diffPayload'
246+
| 'diffApplyResult';
239247

240248
export const OUTPUT_FORMAT: Record<CliExposedOperationId, OutputFormat> = {
241249
get: 'plain',
@@ -375,6 +383,11 @@ export const OUTPUT_FORMAT: Record<CliExposedOperationId, OutputFormat> = {
375383
'images.setPosition': 'plain',
376384
'images.setAnchorOptions': 'plain',
377385
'images.setZOrder': 'plain',
386+
387+
// Diff
388+
'diff.capture': 'diffSnapshot',
389+
'diff.compare': 'diffPayload',
390+
'diff.apply': 'diffApplyResult',
378391
};
379392

380393
// ---------------------------------------------------------------------------
@@ -537,6 +550,11 @@ export const RESPONSE_ENVELOPE_KEY: Record<CliExposedOperationId, string | null>
537550
'headerFooters.parts.list': 'result',
538551
'headerFooters.parts.create': 'result',
539552
'headerFooters.parts.delete': 'result',
553+
554+
// Diff
555+
'diff.capture': 'snapshot',
556+
'diff.compare': 'diff',
557+
'diff.apply': 'result',
540558
};
541559

542560
// ---------------------------------------------------------------------------
@@ -577,6 +595,7 @@ export type OperationFamily =
577595
| 'create'
578596
| 'blocks'
579597
| 'query'
598+
| 'diff'
580599
| 'general';
581600

582601
export const OPERATION_FAMILY: Record<CliExposedOperationId, OperationFamily> = {
@@ -717,4 +736,9 @@ export const OPERATION_FAMILY: Record<CliExposedOperationId, OperationFamily> =
717736
'images.setPosition': 'images',
718737
'images.setAnchorOptions': 'images',
719738
'images.setZOrder': 'images',
739+
740+
// Diff
741+
'diff.capture': 'diff',
742+
'diff.compare': 'diff',
743+
'diff.apply': 'diff',
720744
};

apps/cli/src/cli/operation-set.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const REFERENCE_GROUP_TO_CATEGORY: Record<string, CliCategory> = {
105105
toc: 'toc',
106106
images: 'images',
107107
history: 'history',
108+
diff: 'core',
108109
};
109110

110111
function deriveCategoryFromDocApi(docApiId: OperationId): CliCategory {

apps/cli/src/lib/document.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import { readFile, writeFile } from 'node:fs/promises';
22
import { createHash } from 'node:crypto';
33
import { Editor } from 'superdoc/super-editor';
44
import { BLANK_DOCX_BASE64 } from '@superdoc/super-editor/blank-docx';
5-
import { getDocumentApiAdapters } from '@superdoc/super-editor/document-api-adapters';
65
import { markdownToPmDoc } from '@superdoc/super-editor/markdown';
76

8-
import { createDocumentApi, type DocumentApi } from '@superdoc/document-api';
7+
import type { DocumentApi } from '@superdoc/document-api';
98
import { createCliDomEnvironment } from './dom-environment';
109
import type { CollaborationProfile } from './collaboration';
1110
import { createCollaborationRuntime } from './collaboration';
@@ -218,9 +217,6 @@ export async function openDocument(
218217
}
219218
}
220219

221-
const adapters = getDocumentApiAdapters(editor);
222-
const docApi = createDocumentApi(adapters);
223-
Object.defineProperty(editor, 'doc', { value: docApi, configurable: true, writable: true });
224220
const editorWithDoc = editor as EditorWithDoc;
225221

226222
return {

apps/cli/src/lib/error-mapping.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,27 @@ function tryMapPlanEngineError(
324324
// Per-family error mappers (dispatch by family)
325325
// ---------------------------------------------------------------------------
326326

327+
function mapDiffError(operationId: CliExposedOperationId, error: unknown, code: string | undefined): CliError {
328+
const message = extractErrorMessage(error);
329+
const details = extractErrorDetails(error);
330+
331+
if (code === 'INVALID_INPUT') {
332+
return new CliError('INVALID_INPUT', message, { operationId, details });
333+
}
334+
if (code === 'CAPABILITY_UNSUPPORTED') {
335+
return new CliError('CAPABILITY_UNSUPPORTED', message, { operationId, details });
336+
}
337+
if (code === 'PRECONDITION_FAILED') {
338+
return new CliError('PRECONDITION_FAILED', message, { operationId, details });
339+
}
340+
if (code === 'CAPABILITY_UNAVAILABLE') {
341+
return new CliError('CAPABILITY_UNAVAILABLE', message, { operationId, details });
342+
}
343+
344+
if (error instanceof CliError) return error;
345+
return new CliError('COMMAND_FAILED', message, { operationId, details });
346+
}
347+
327348
const FAMILY_MAPPERS: Record<
328349
OperationFamily,
329350
(operationId: CliExposedOperationId, error: unknown, code: string | undefined) => CliError
@@ -338,6 +359,7 @@ const FAMILY_MAPPERS: Record<
338359
create: mapCreateError,
339360
blocks: mapBlocksError,
340361
query: mapQueryError,
362+
diff: mapDiffError,
341363
general: (operationId, error, code) => {
342364
// Plan-engine errors pass through with original code and structured details
343365
const planEngineError = tryMapPlanEngineError(operationId, error, code);

apps/cli/src/lib/errors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export type CliErrorCode =
4242
| 'PAGE_NUMBERS_NOT_MATERIALIZED'
4343
| 'CAPABILITY_UNAVAILABLE'
4444
| 'INVALID_TARGET'
45-
| 'AMBIGUOUS_TARGET';
45+
| 'AMBIGUOUS_TARGET'
46+
| 'CAPABILITY_UNSUPPORTED';
4647

4748
/**
4849
* Intersection type for errors thrown by document-api adapter operations.

apps/cli/src/lib/output-formatters.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,46 @@ function formatDocumentInfo(result: unknown, ctx: FormatContext): string {
187187
return lines.join('\n');
188188
}
189189

190+
// ---------------------------------------------------------------------------
191+
// Diff formatters
192+
// ---------------------------------------------------------------------------
193+
194+
function formatDiffSnapshot(result: unknown, ctx: FormatContext): string {
195+
const record = asRecord(result);
196+
if (!record) return `Revision ${ctx.revision}: captured snapshot`;
197+
const fingerprint = hasNonEmptyString(record.fingerprint) ? record.fingerprint : '<unknown>';
198+
const coverage = asRecord(record.coverage);
199+
const components = coverage
200+
? Object.entries(coverage)
201+
.filter(([, v]) => v === true)
202+
.map(([k]) => k)
203+
.join(', ')
204+
: 'body';
205+
const payloadSize = record.payload ? JSON.stringify(record.payload).length : 0;
206+
return `Revision ${ctx.revision}: captured snapshot\n fingerprint: ${fingerprint}\n coverage: ${components}\n payload size: ${payloadSize} bytes`;
207+
}
208+
209+
function formatDiffPayload(result: unknown, ctx: FormatContext): string {
210+
const record = asRecord(result);
211+
if (!record) return `Revision ${ctx.revision}: compared documents`;
212+
const summary = asRecord(record.summary);
213+
const changed = asArray(summary?.changedComponents).filter(hasNonEmptyString);
214+
const baseFp = hasNonEmptyString(record.baseFingerprint) ? record.baseFingerprint : '<unknown>';
215+
const targetFp = hasNonEmptyString(record.targetFingerprint) ? record.targetFingerprint : '<unknown>';
216+
const changedStr = changed.length > 0 ? changed.join(', ') : 'none';
217+
return `Revision ${ctx.revision}: compared documents\n base: ${baseFp}\n target: ${targetFp}\n changed: ${changedStr}`;
218+
}
219+
220+
function formatDiffApplyResult(result: unknown, ctx: FormatContext): string {
221+
const record = asRecord(result);
222+
if (!record) return `Revision ${ctx.revision}: applied diff`;
223+
const ops = safeNumber(record.appliedOperations, 0);
224+
const summary = asRecord(record.summary);
225+
const changed = asArray(summary?.changedComponents).filter(hasNonEmptyString);
226+
const changedStr = changed.length > 0 ? changed.join(', ') : 'none';
227+
return `Revision ${ctx.revision}: applied diff (${ops} operations)\n changed: ${changedStr}`;
228+
}
229+
190230
// ---------------------------------------------------------------------------
191231
// Dispatch
192232
// ---------------------------------------------------------------------------
@@ -200,6 +240,9 @@ const FORMAT_DISPATCH: Partial<Record<OutputFormat, Formatter>> = {
200240
listResult: (result, ctx) => formatListResult(result, ctx),
201241
trackChangeList: (result, ctx) => formatTrackChangeList(result, ctx),
202242
documentInfo: (result, ctx) => formatDocumentInfo(result, ctx),
243+
diffSnapshot: (result, ctx) => formatDiffSnapshot(result, ctx),
244+
diffPayload: (result, ctx) => formatDiffPayload(result, ctx),
245+
diffApplyResult: (result, ctx) => formatDiffApplyResult(result, ctx),
203246
};
204247

205248
/**

apps/docs/docs.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@
109109
"document-api/available-operations"
110110
]
111111
},
112-
"document-engine/sdks",
112+
{
113+
"group": "SDKs",
114+
"pages": ["document-engine/sdks", "document-engine/sdk-diffing"]
115+
},
113116
"document-engine/cli",
114117
{
115118
"group": "AI Agents",

apps/docs/document-api/available-operations.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Use the tables below to see what operations are available and where each one is
2424
| Core | 13 | 0 | 13 | [Reference](/document-api/reference/core/index) |
2525
| Create | 6 | 0 | 6 | [Reference](/document-api/reference/create/index) |
2626
| Cross-References | 5 | 0 | 5 | [Reference](/document-api/reference/cross-refs/index) |
27+
| Diff | 3 | 0 | 3 | [Reference](/document-api/reference/diff/index) |
2728
| Fields | 5 | 0 | 5 | [Reference](/document-api/reference/fields/index) |
2829
| Footnotes | 6 | 0 | 6 | [Reference](/document-api/reference/footnotes/index) |
2930
| Format | 44 | 1 | 45 | [Reference](/document-api/reference/format/index) |
@@ -161,6 +162,9 @@ Use the tables below to see what operations are available and where each one is
161162
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.crossRefs.insert(...)</code></span> | [`crossRefs.insert`](/document-api/reference/cross-refs/insert) |
162163
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.crossRefs.rebuild(...)</code></span> | [`crossRefs.rebuild`](/document-api/reference/cross-refs/rebuild) |
163164
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.crossRefs.remove(...)</code></span> | [`crossRefs.remove`](/document-api/reference/cross-refs/remove) |
165+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.diff.capture(...)</code></span> | [`diff.capture`](/document-api/reference/diff/capture) |
166+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.diff.compare(...)</code></span> | [`diff.compare`](/document-api/reference/diff/compare) |
167+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.diff.apply(...)</code></span> | [`diff.apply`](/document-api/reference/diff/apply) |
164168
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.fields.list(...)</code></span> | [`fields.list`](/document-api/reference/fields/list) |
165169
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.fields.get(...)</code></span> | [`fields.get`](/document-api/reference/fields/get) |
166170
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.fields.insert(...)</code></span> | [`fields.insert`](/document-api/reference/fields/insert) |

apps/docs/document-api/reference/_generated-manifest.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@
126126
"apps/docs/document-api/reference/cross-refs/rebuild.mdx",
127127
"apps/docs/document-api/reference/cross-refs/remove.mdx",
128128
"apps/docs/document-api/reference/delete.mdx",
129+
"apps/docs/document-api/reference/diff/apply.mdx",
130+
"apps/docs/document-api/reference/diff/capture.mdx",
131+
"apps/docs/document-api/reference/diff/compare.mdx",
132+
"apps/docs/document-api/reference/diff/index.mdx",
129133
"apps/docs/document-api/reference/fields/get.mdx",
130134
"apps/docs/document-api/reference/fields/index.mdx",
131135
"apps/docs/document-api/reference/fields/insert.mdx",
@@ -948,8 +952,15 @@
948952
"operationIds": ["ranges.resolve"],
949953
"pagePath": "apps/docs/document-api/reference/ranges/index.mdx",
950954
"title": "Ranges"
955+
},
956+
{
957+
"aliasMemberPaths": [],
958+
"key": "diff",
959+
"operationIds": ["diff.capture", "diff.compare", "diff.apply"],
960+
"pagePath": "apps/docs/document-api/reference/diff/index.mdx",
961+
"title": "Diff"
951962
}
952963
],
953964
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
954-
"sourceHash": "068dea933bf1591112019534c6bae48f811dc8d65c42f6cb94f365548028ea77"
965+
"sourceHash": "bf7e9b493d8ab9e84c2d5875b5aa7fe0e74e8504ec3e258e463af5e529b16e92"
955966
}

0 commit comments

Comments
 (0)