Skip to content

Commit 6221580

Browse files
committed
fix: document-api improvements, plan mode, query.match, mutations
1 parent a093891 commit 6221580

169 files changed

Lines changed: 12304 additions & 18904 deletions

File tree

Some content is hidden

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

apps/cli/scripts/export-sdk-contract.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,7 @@ const INTENT_NAMES = {
5858
'doc.insert': 'insert_content',
5959
'doc.replace': 'replace_content',
6060
'doc.delete': 'delete_content',
61-
'doc.format.bold': 'format_bold',
62-
'doc.format.italic': 'format_italic',
63-
'doc.format.underline': 'format_underline',
64-
'doc.format.strikethrough': 'format_strikethrough',
61+
'doc.format.apply': 'format_apply',
6562
'doc.create.paragraph': 'create_paragraph',
6663
'doc.create.heading': 'create_heading',
6764
'doc.lists.list': 'list_lists',
@@ -72,23 +69,14 @@ const INTENT_NAMES = {
7269
'doc.lists.outdent': 'outdent_list',
7370
'doc.lists.restart': 'restart_list_numbering',
7471
'doc.lists.exit': 'exit_list',
75-
'doc.comments.add': 'add_comment',
76-
'doc.comments.edit': 'edit_comment',
77-
'doc.comments.reply': 'reply_to_comment',
78-
'doc.comments.move': 'move_comment',
79-
'doc.comments.resolve': 'resolve_comment',
80-
'doc.comments.remove': 'remove_comment',
81-
'doc.comments.setInternal': 'set_comment_internal',
82-
'doc.comments.setActive': 'set_comment_active',
83-
'doc.comments.goTo': 'go_to_comment',
72+
'doc.comments.create': 'create_comment',
73+
'doc.comments.patch': 'patch_comment',
74+
'doc.comments.delete': 'delete_comment',
8475
'doc.comments.get': 'get_comment',
8576
'doc.comments.list': 'list_comments',
8677
'doc.trackChanges.list': 'list_tracked_changes',
8778
'doc.trackChanges.get': 'get_tracked_change',
88-
'doc.trackChanges.accept': 'accept_tracked_change',
89-
'doc.trackChanges.reject': 'reject_tracked_change',
90-
'doc.trackChanges.acceptAll': 'accept_all_tracked_changes',
91-
'doc.trackChanges.rejectAll': 'reject_all_tracked_changes',
79+
'doc.review.decide': 'review_decide',
9280
'doc.query.match': 'query_match',
9381
'doc.mutations.preview': 'preview_mutations',
9482
'doc.mutations.apply': 'apply_mutations',
@@ -100,6 +88,7 @@ const INTENT_NAMES = {
10088

10189
function loadDocApiContract(): {
10290
contractVersion: string;
91+
$defs?: Record<string, unknown>;
10392
operations: Record<string, Record<string, unknown>>;
10493
} {
10594
const raw = readFileSync(CONTRACT_JSON_PATH, 'utf-8');
@@ -200,6 +189,7 @@ function buildSdkContract() {
200189
return {
201190
contractVersion: docApiContract.contractVersion,
202191
sourceHash,
192+
...(docApiContract.$defs ? { $defs: docApiContract.$defs } : {}),
203193
cli: {
204194
package: cliPkg.name,
205195
// Envelope meta.version is contract-version-based today, so minVersion must match that domain.
@@ -228,9 +218,13 @@ function main() {
228218
let existing: string;
229219
try {
230220
existing = readFileSync(OUTPUT_PATH, 'utf-8');
231-
} catch {
232-
console.error(`--check: ${OUTPUT_PATH} does not exist. Run without --check to generate.`);
233-
process.exit(1);
221+
} catch (error) {
222+
const fsError = error as NodeJS.ErrnoException;
223+
if (fsError?.code === 'ENOENT') {
224+
console.error(`--check: ${OUTPUT_PATH} does not exist. Run without --check to generate.`);
225+
process.exit(1);
226+
}
227+
throw error;
234228
}
235229

236230
if (existing === json) {

apps/cli/src/__tests__/cli.test.ts

Lines changed: 35 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ async function firstTextRange(args: string[]): Promise<TextRange> {
9595
const envelope = parseJsonOutput<
9696
SuccessEnvelope<{
9797
result: {
98-
context?: Array<{ textRanges?: TextRange[] }>;
98+
items?: Array<{ context?: { textRanges?: TextRange[] } }>;
9999
};
100100
}>
101101
>(result);
102102

103-
const range = envelope.data.result.context?.[0]?.textRanges?.[0];
103+
const range = envelope.data.result.items?.[0]?.context?.textRanges?.[0];
104104
if (!range) {
105105
throw new Error('Expected at least one text range from find result.');
106106
}
@@ -130,12 +130,12 @@ async function firstListItemAddress(args: string[]): Promise<ListItemAddress> {
130130
const envelope = parseJsonOutput<
131131
SuccessEnvelope<{
132132
result: {
133-
matches: ListItemAddress[];
133+
items: Array<{ address: ListItemAddress }>;
134134
};
135135
}>
136136
>(result);
137137

138-
const address = envelope.data.result.matches[0];
138+
const address = envelope.data.result.items[0]?.address;
139139
if (!address) {
140140
throw new Error('Expected at least one list item address from lists.list result.');
141141
}
@@ -279,11 +279,11 @@ describe('superdoc CLI', () => {
279279
expect(result.stdout).not.toContain('<doc> Document path or stdin');
280280
});
281281

282-
test('describe command doc.insert includes --block-id and --offset flags', async () => {
282+
test('describe command doc.insert includes --target and --text flags', async () => {
283283
const result = await runCli(['describe', 'command', 'doc.insert', '--output', 'pretty']);
284284
expect(result.code).toBe(0);
285-
expect(result.stdout).toContain('--block-id');
286-
expect(result.stdout).toContain('--offset');
285+
expect(result.stdout).toContain('--target');
286+
expect(result.stdout).toContain('--text');
287287
});
288288

289289
test('call executes an operation from canonical input payload', async () => {
@@ -564,16 +564,16 @@ describe('superdoc CLI', () => {
564564
SuccessEnvelope<{
565565
result: {
566566
total: number;
567-
matches: Array<{ kind: string; nodeType: string }>;
567+
items: Array<{ address: { kind: string; nodeType: string } }>;
568568
};
569569
}>
570570
>(result);
571571

572572
expect(envelope.ok).toBe(true);
573573
expect(envelope.command).toBe('find');
574574
expect(envelope.data.result.total).toBeGreaterThan(0);
575-
expect(envelope.data.result.matches[0].kind).toBe('inline');
576-
expect(envelope.data.result.matches[0].nodeType).toBe('run');
575+
expect(envelope.data.result.items[0].address.kind).toBe('inline');
576+
expect(envelope.data.result.items[0].address.nodeType).toBe('run');
577577
});
578578

579579
test('find rejects legacy query.include payloads', async () => {
@@ -608,14 +608,16 @@ describe('superdoc CLI', () => {
608608
const envelope = parseJsonOutput<
609609
SuccessEnvelope<{
610610
result: {
611-
context?: Array<{
612-
textRanges?: Array<{ kind: 'text'; blockId: string; range: { start: number; end: number } }>;
611+
items?: Array<{
612+
context?: {
613+
textRanges?: Array<{ kind: 'text'; blockId: string; range: { start: number; end: number } }>;
614+
};
613615
}>;
614616
};
615617
}>
616618
>(result);
617619

618-
const firstContext = envelope.data.result.context?.[0];
620+
const firstContext = envelope.data.result.items?.[0]?.context;
619621
expect(firstContext).toBeDefined();
620622
expect(firstContext?.textRanges?.length).toBeGreaterThan(0);
621623
});
@@ -627,12 +629,12 @@ describe('superdoc CLI', () => {
627629
const findEnvelope = parseJsonOutput<
628630
SuccessEnvelope<{
629631
result: {
630-
matches: Array<Record<string, unknown>>;
632+
items: Array<{ address: Record<string, unknown> }>;
631633
};
632634
}>
633635
>(findResult);
634636

635-
const address = findEnvelope.data.result.matches[0];
637+
const address = findEnvelope.data.result.items[0]?.address;
636638
expect(address).toBeDefined();
637639

638640
const getNodeResult = await runCli(['get-node', SAMPLE_DOC, '--address-json', JSON.stringify(address)]);
@@ -651,11 +653,11 @@ describe('superdoc CLI', () => {
651653
const findEnvelope = parseJsonOutput<
652654
SuccessEnvelope<{
653655
result: {
654-
matches: Array<Record<string, unknown>>;
656+
items: Array<{ address: Record<string, unknown> }>;
655657
};
656658
}>
657659
>(findResult);
658-
const address = findEnvelope.data.result.matches[0];
660+
const address = findEnvelope.data.result.items[0]?.address;
659661
expect(address).toBeDefined();
660662
if (!address) return;
661663

@@ -689,12 +691,12 @@ describe('superdoc CLI', () => {
689691
const findEnvelope = parseJsonOutput<
690692
SuccessEnvelope<{
691693
result: {
692-
matches: Array<{ kind: string; nodeType: string; nodeId: string }>;
694+
items: Array<{ address: { kind: string; nodeType: string; nodeId: string } }>;
693695
};
694696
}>
695697
>(findResult);
696698

697-
const firstMatch = findEnvelope.data.result.matches[0];
699+
const firstMatch = findEnvelope.data.result.items[0].address;
698700
expect(firstMatch.kind).toBe('block');
699701

700702
const getByIdResult = await runCli([
@@ -719,12 +721,12 @@ describe('superdoc CLI', () => {
719721
const findEnvelope = parseJsonOutput<
720722
SuccessEnvelope<{
721723
result: {
722-
matches: Array<{ kind: string; nodeType: string; nodeId: string }>;
724+
items: Array<{ address: { kind: string; nodeType: string; nodeId: string } }>;
723725
};
724726
}>
725727
>(findResult);
726728

727-
const firstMatch = findEnvelope.data.result.matches[0];
729+
const firstMatch = findEnvelope.data.result.items[0].address;
728730
expect(firstMatch.kind).toBe('block');
729731

730732
const prettyResult = await runCli([
@@ -1015,7 +1017,7 @@ describe('superdoc CLI', () => {
10151017
expect(result.code).toBe(1);
10161018
const envelope = parseJsonOutput<ErrorEnvelope>(result);
10171019
expect(envelope.error.code).toBe('INVALID_ARGUMENT');
1018-
expect(envelope.error.message).toContain('offset requires blockId');
1020+
expect(envelope.error.message).toContain('Unknown field');
10191021
});
10201022

10211023
test('create paragraph writes output and adds a new paragraph with seed text', async () => {
@@ -1074,13 +1076,13 @@ describe('superdoc CLI', () => {
10741076
SuccessEnvelope<{
10751077
result: {
10761078
total: number;
1077-
matches: ListItemAddress[];
1079+
items: Array<{ address: ListItemAddress }>;
10781080
};
10791081
}>
10801082
>(listResult);
10811083
expect(listEnvelope.data.result.total).toBeGreaterThan(0);
10821084

1083-
const address = listEnvelope.data.result.matches[0];
1085+
const address = listEnvelope.data.result.items[0]?.address;
10841086
expect(address).toBeDefined();
10851087
if (!address) return;
10861088

@@ -1310,8 +1312,7 @@ describe('superdoc CLI', () => {
13101312
const jsonEnvelope = parseJsonOutput<
13111313
SuccessEnvelope<{
13121314
result: {
1313-
changes?: Array<{ id?: string }>;
1314-
matches?: Array<{ entityId?: string }>;
1315+
items?: Array<{ id?: string }>;
13151316
};
13161317
}>
13171318
>(jsonResult);
@@ -1321,12 +1322,9 @@ describe('superdoc CLI', () => {
13211322
expect(prettyResult.stdout).toContain('Revision 0:');
13221323
expect(prettyResult.stdout).toContain('tracked changes');
13231324

1324-
const firstChangeId = jsonEnvelope.data.result.changes?.[0]?.id;
1325-
const firstMatchId = jsonEnvelope.data.result.matches?.[0]?.entityId;
1326-
if (firstChangeId) {
1327-
expect(prettyResult.stdout).toContain(firstChangeId);
1328-
} else if (firstMatchId) {
1329-
expect(prettyResult.stdout).toContain(firstMatchId);
1325+
const firstItemId = jsonEnvelope.data.result.items?.[0]?.id;
1326+
if (firstItemId) {
1327+
expect(prettyResult.stdout).toContain(firstItemId);
13301328
}
13311329
});
13321330

@@ -1452,28 +1450,11 @@ describe('superdoc CLI', () => {
14521450
expect(envelope.error.code).toBe('MISSING_REQUIRED');
14531451
});
14541452

1455-
test('comments set-active without --out succeeds in stateless mode', async () => {
1456-
const source = join(TEST_DIR, 'comments-set-active-no-out-source.docx');
1457-
const addOut = join(TEST_DIR, 'comments-set-active-no-out-added.docx');
1458-
await copyFile(SAMPLE_DOC, source);
1459-
1460-
const target = await firstTextRange(['find', source, '--type', 'text', '--pattern', 'Wilde']);
1461-
const addResult = await runCli([
1462-
'comments',
1463-
'add',
1464-
source,
1465-
'--target-json',
1466-
JSON.stringify(target),
1467-
'--text',
1468-
'anchor for set-active',
1469-
'--out',
1470-
addOut,
1471-
]);
1472-
expect(addResult.code).toBe(0);
1473-
const commentId = firstInsertedEntityId(addResult);
1474-
1475-
const setActiveResult = await runCli(['comments', 'set-active', addOut, '--clear']);
1476-
expect(setActiveResult.code).toBe(0);
1453+
test('comments set-active is not part of the canonical CLI surface', async () => {
1454+
const setActiveResult = await runCli(['comments', 'set-active', '--clear']);
1455+
expect(setActiveResult.code).toBe(1);
1456+
const envelope = parseJsonOutput<ErrorEnvelope>(setActiveResult);
1457+
expect(envelope.error.code).toBe('UNKNOWN_COMMAND');
14771458
});
14781459

14791460
test('comments list pretty includes comment ids for actionable output', async () => {
@@ -1545,12 +1526,6 @@ describe('superdoc CLI', () => {
15451526
const listEnvelope = parseJsonOutput<SuccessEnvelope<{ result: { total: number } }>>(listResult);
15461527
expect(listEnvelope.data.result.total).toBeGreaterThanOrEqual(1);
15471528

1548-
const setActiveResult = await runCli(['comments', 'set-active', '--id', commentId]);
1549-
expect(setActiveResult.code).toBe(0);
1550-
1551-
const goToResult = await runCli(['comments', 'go-to', '--id', commentId]);
1552-
expect(goToResult.code).toBe(0);
1553-
15541529
const resolveResult = await runCli(['comments', 'resolve', '--id', commentId]);
15551530
expect(resolveResult.code).toBe(0);
15561531

@@ -1574,9 +1549,6 @@ describe('superdoc CLI', () => {
15741549
const missingGetEnvelope = parseJsonOutput<ErrorEnvelope>(missingGetResult);
15751550
expect(missingGetEnvelope.error.code).toBe('TARGET_NOT_FOUND');
15761551

1577-
const clearActiveResult = await runCli(['comments', 'set-active', '--clear']);
1578-
expect(clearActiveResult.code).toBe(0);
1579-
15801552
const setInternalResult = await runCli(['comments', 'set-internal', '--id', commentId, '--is-internal', 'true']);
15811553
expect(setInternalResult.code).toBe(0);
15821554

0 commit comments

Comments
 (0)