@superdoc/sdk 2.10.0-next.4 → 2.10.0-next.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -514,6 +514,8 @@ function revisionVerification(preRevision, postRevision, expectChanged) {
514
514
  }
515
515
  function failedReceipt(intent, err, preSnapshot) {
516
516
  const message = reasonOf(err);
517
+ const snapshotError = err instanceof docSnapshot.MutationSnapshotError ? err : null;
518
+ const errorCode = snapshotError?.code ?? asString(asRecord(err)?.code) ?? 'ACTION_FAILED';
517
519
  return {
518
520
  status: 'failed',
519
521
  intent,
@@ -523,7 +525,13 @@ function failedReceipt(intent, err, preSnapshot) {
523
525
  selectedTargets: [],
524
526
  executedOperations: [],
525
527
  verification: [],
526
- errors: [{ code: 'ACTION_FAILED', message }],
528
+ errors: [
529
+ {
530
+ code: errorCode,
531
+ message,
532
+ ...(snapshotError || errorCode === 'REVISION_CONFLICT' ? { recovery: { kind: 'reinspect' } } : {}),
533
+ },
534
+ ],
527
535
  };
528
536
  }
529
537
  async function receiptFromWorkflowResult(doc, intent, pre, workflowResult, selectedTargets = [], checks = [{ kind: 'revision-changed' }]) {
@@ -567,14 +575,16 @@ async function receiptFromWorkflowResult(doc, intent, pre, workflowResult, selec
567
575
  }
568
576
  function buildFullBlockTextTarget(snapshot, blockId) {
569
577
  const block = snapshot.blocks.find((entry) => entry.nodeId === blockId);
570
- if (!block)
578
+ const cell = block ? null : snapshot.tables.flatMap((table) => table.cells).find((entry) => entry.nodeId === blockId);
579
+ const text = block?.text ?? cell?.text;
580
+ if (text == null)
571
581
  return null;
572
582
  return {
573
583
  kind: 'text',
574
584
  blockId,
575
585
  range: {
576
586
  start: 0,
577
- end: block.text.length,
587
+ end: text.length,
578
588
  },
579
589
  };
580
590
  }
@@ -627,8 +637,8 @@ function evaluateChecks(pre, post, checks) {
627
637
  else if (check.kind === 'comment-count-delta') {
628
638
  results.push({
629
639
  check,
630
- passed: post.comments.length - pre.comments.length === check.delta,
631
- detail: `pre=${pre.comments.length} post=${post.comments.length}`,
640
+ passed: post.counts.comments - pre.counts.comments === check.delta,
641
+ detail: `pre=${pre.counts.comments} post=${post.counts.comments}`,
632
642
  });
633
643
  }
634
644
  else if (check.kind === 'tracked-change-count-delta') {
@@ -1040,7 +1050,9 @@ async function styleTableCells(doc, cells, changeMode) {
1040
1050
  return styled.length;
1041
1051
  }
1042
1052
  async function runInsertParagraphs(doc, args) {
1043
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1053
+ const pre = args.placement?.at === 'before' || args.placement?.at === 'after'
1054
+ ? await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] })
1055
+ : await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1044
1056
  try {
1045
1057
  // `texts` is the canonical input; a single `text` is normalized to one item
1046
1058
  // upstream in the dispatcher, but tolerate it here too.
@@ -1067,7 +1079,7 @@ async function runInsertParagraphs(doc, args) {
1067
1079
  if (created) {
1068
1080
  return { kind: 'after', target: { kind: 'block', nodeType: created.nodeType, nodeId: created.nodeId } };
1069
1081
  }
1070
- const mid = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1082
+ const mid = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
1071
1083
  const last = lastBlock(mid);
1072
1084
  return last
1073
1085
  ? { kind: 'after', target: { kind: 'block', nodeType: last.nodeType, nodeId: last.nodeId } }
@@ -1129,7 +1141,9 @@ async function runInsertParagraphs(doc, args) {
1129
1141
  }
1130
1142
  }
1131
1143
  async function runInsertHeading(doc, args) {
1132
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1144
+ const pre = args.placement?.at === 'before' || args.placement?.at === 'after'
1145
+ ? await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] })
1146
+ : await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1133
1147
  try {
1134
1148
  const placement = resolvePlacement(args.placement, pre);
1135
1149
  const result = await executeCreateHeading(doc, args.text, args.level, placement, args.changeMode);
@@ -1154,7 +1168,7 @@ async function runReplaceText(doc, args) {
1154
1168
  const requiresBlockSnapshot = args.selector != null;
1155
1169
  const preIdentity = requiresBlockSnapshot ? null : await readDocumentIdentity(doc);
1156
1170
  const pre = requiresBlockSnapshot
1157
- ? await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
1171
+ ? await docSnapshot.buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
1158
1172
  : snapshotFromIdentity(preIdentity);
1159
1173
  try {
1160
1174
  if (args.edits.length === 0) {
@@ -1206,6 +1220,7 @@ async function runReplaceText(doc, args) {
1206
1220
  },
1207
1221
  },
1208
1222
  ];
1223
+ applyExpectedRevision = pre.revision;
1209
1224
  selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
1210
1225
  preserveTargets.push({
1211
1226
  nodeId: target.nodeId,
@@ -1278,7 +1293,7 @@ async function runReplaceText(doc, args) {
1278
1293
  preserved.push({ nodeId: entry.nodeId, ...restored });
1279
1294
  }
1280
1295
  if (args.selector && selectedTargets[0]) {
1281
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] });
1296
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] });
1282
1297
  const blockId = selectedTargets[0].matched[0];
1283
1298
  const postTarget = findSnapshotTextByNodeId(post, blockId);
1284
1299
  const preTarget = findSnapshotTextByNodeId(pre, blockId);
@@ -1347,7 +1362,7 @@ async function runDeleteText(doc, args) {
1347
1362
  // path). Rewriting the block minus the finds keeps the deletion local — without
1348
1363
  // this a short/whitespace find matches document-wide and blows the target cap.
1349
1364
  if (args.selector) {
1350
- const scopedPre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: snapshotDomainsForSelector(args.selector) });
1365
+ const scopedPre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: snapshotDomainsForSelector(args.selector) });
1351
1366
  try {
1352
1367
  if (args.finds.length === 0) {
1353
1368
  return failedReceipt('delete_text', new Error('finds must be non-empty'), scopedPre);
@@ -1390,7 +1405,7 @@ async function runDeleteText(doc, args) {
1390
1405
  args: { replacement: { text: rewritten }, style: preserveRewriteStyle() },
1391
1406
  },
1392
1407
  ];
1393
- const scopedResult = await executeMutations(doc, scopedSteps, args.changeMode);
1408
+ const scopedResult = await executeMutations(doc, scopedSteps, args.changeMode, scopedPre.revision);
1394
1409
  const scopedPost = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1395
1410
  const verification = evaluateChecks(scopedPre, scopedPost, [{ kind: 'revision-changed' }]);
1396
1411
  return {
@@ -1452,7 +1467,7 @@ async function runDeleteText(doc, args) {
1452
1467
  },
1453
1468
  args: {},
1454
1469
  }));
1455
- const result = await executeMutations(doc, steps, args.changeMode);
1470
+ const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
1456
1471
  const revision = asRecord(asRecord(result)?.revision);
1457
1472
  const postIdentity = args.changeMode === 'tracked'
1458
1473
  ? await readDocumentIdentity(doc)
@@ -1495,7 +1510,7 @@ async function runDeleteBlocks(doc, args) {
1495
1510
  for (const domain of snapshotDomainsForSelector(selector))
1496
1511
  domains.add(domain);
1497
1512
  }
1498
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: [...domains] });
1513
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: [...domains] });
1499
1514
  try {
1500
1515
  if (args.selectors.length === 0) {
1501
1516
  return failedReceipt('delete_blocks', new Error('selectors must be non-empty'), pre);
@@ -1588,7 +1603,7 @@ async function runDeleteBlocks(doc, args) {
1588
1603
  });
1589
1604
  }
1590
1605
  }
1591
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: [...domains] });
1606
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: [...domains] });
1592
1607
  // Tracked deletions leave the block in place until the revision is decided,
1593
1608
  // so the block count cannot move — count the structural revisions instead.
1594
1609
  // Direct deletions must show one fewer block of each deleted node type.
@@ -1706,7 +1721,7 @@ async function runInsertListItems(doc, args) {
1706
1721
  }
1707
1722
  }
1708
1723
  async function runAddListItems(doc, args, opts) {
1709
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1724
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
1710
1725
  try {
1711
1726
  const needle = args.anchorText?.trim();
1712
1727
  const anchorNodeId = args.anchorNodeId?.trim();
@@ -2240,7 +2255,7 @@ async function runSplitList(doc, args) {
2240
2255
  }
2241
2256
  }
2242
2257
  async function runCreateTable(doc, args) {
2243
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
2258
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
2244
2259
  try {
2245
2260
  if (!Number.isInteger(args.rows) || args.rows < 1 || !Number.isInteger(args.columns) || args.columns < 1) {
2246
2261
  return failedReceipt('create_table', new Error('rows and columns must be positive integers'), pre);
@@ -2268,7 +2283,7 @@ async function runCreateTable(doc, args) {
2268
2283
  });
2269
2284
  }
2270
2285
  }
2271
- const post = await docSnapshot.buildDocumentSnapshot(doc);
2286
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
2272
2287
  const verification = evaluateChecks(pre, post, [
2273
2288
  { kind: 'revision-changed' },
2274
2289
  { kind: 'block-count-delta', nodeType: 'table', delta: 1 },
@@ -2288,7 +2303,7 @@ async function runCreateTable(doc, args) {
2288
2303
  }
2289
2304
  }
2290
2305
  async function runCommentParagraphs(doc, args) {
2291
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
2306
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
2292
2307
  try {
2293
2308
  const blocks = pre.blocks.filter((b) => {
2294
2309
  if (args.scope === 'all') {
@@ -2330,13 +2345,18 @@ async function runCommentParagraphs(doc, args) {
2330
2345
  }
2331
2346
  }
2332
2347
  async function runAddComments(doc, args) {
2333
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
2348
+ const selectorList = args.selectors?.length ? args.selectors : args.selector ? [args.selector] : [];
2349
+ const domains = new Set(['blocks']);
2350
+ for (const selector of selectorList) {
2351
+ for (const domain of snapshotDomainsForSelector(selector))
2352
+ domains.add(domain);
2353
+ }
2354
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: [...domains] });
2334
2355
  try {
2335
2356
  // Batch form: `selectors` comments many blocks in ONE action call so the
2336
2357
  // model never fans out N concurrent add_comments tool calls (which race the
2337
2358
  // shared document and the comment-count verification). A single `selector`
2338
2359
  // is the one-target shorthand. Comments are applied sequentially.
2339
- const selectorList = args.selectors?.length ? args.selectors : args.selector ? [args.selector] : [];
2340
2360
  if (!selectorList.length) {
2341
2361
  return failedReceipt('add_comments', new Error('add_comments requires a "selector" or a non-empty "selectors" array'), pre);
2342
2362
  }
@@ -2350,7 +2370,21 @@ async function runAddComments(doc, args) {
2350
2370
  unresolved.push(sel);
2351
2371
  }
2352
2372
  if (!resolved.length) {
2353
- return failedReceipt('add_comments', new Error('no selector resolved to a body block'), pre);
2373
+ return {
2374
+ status: 'failed',
2375
+ intent: 'add_comments',
2376
+ preSnapshot: { revision: pre.revision, counts: pre.counts },
2377
+ selectedTargets: unresolved.map((selector) => ({ selector, matched: [] })),
2378
+ executedOperations: [],
2379
+ verification: [],
2380
+ errors: [
2381
+ {
2382
+ code: 'TARGET_NOT_FOUND',
2383
+ message: 'none of the requested comment selectors resolved to a block',
2384
+ recovery: { kind: 'reinspect' },
2385
+ },
2386
+ ],
2387
+ };
2354
2388
  }
2355
2389
  const executed = [];
2356
2390
  for (const { commentText, nodeId } of resolved.map((r) => ({ commentText: args.commentText, nodeId: r.nodeId }))) {
@@ -2359,15 +2393,20 @@ async function runAddComments(doc, args) {
2359
2393
  }
2360
2394
  const post = await docSnapshot.buildDocumentSnapshot(doc);
2361
2395
  const verification = evaluateChecks(pre, post, [{ kind: 'comment-count-delta', delta: resolved.length }]);
2396
+ const verified = verification.every((v) => v.passed);
2397
+ const status = !verified ? 'failed' : unresolved.length > 0 ? 'partial' : 'ok';
2362
2398
  return {
2363
- status: verification.every((v) => v.passed) ? 'ok' : 'failed',
2399
+ status,
2364
2400
  intent: 'add_comments',
2365
2401
  ...(unresolved.length
2366
2402
  ? { note: `${unresolved.length} selector(s) did not resolve to a block and were skipped` }
2367
2403
  : {}),
2368
2404
  preSnapshot: { revision: pre.revision, counts: pre.counts },
2369
2405
  postSnapshot: { revision: post.revision, counts: post.counts },
2370
- selectedTargets: resolved.map((r) => ({ selector: r.selector, matched: [r.nodeId] })),
2406
+ selectedTargets: [
2407
+ ...resolved.map((r) => ({ selector: r.selector, matched: [r.nodeId] })),
2408
+ ...unresolved.map((selector) => ({ selector, matched: [] })),
2409
+ ],
2371
2410
  executedOperations: executed,
2372
2411
  verification,
2373
2412
  };
@@ -2543,7 +2582,7 @@ async function runReplyToComment(doc, args) {
2543
2582
  }
2544
2583
  }
2545
2584
  async function runRewriteBlock(doc, args) {
2546
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
2585
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
2547
2586
  try {
2548
2587
  const target = selectorToBlockTarget(args.selector, pre);
2549
2588
  if (!target) {
@@ -2566,9 +2605,9 @@ async function runRewriteBlock(doc, args) {
2566
2605
  },
2567
2606
  },
2568
2607
  ];
2569
- const result = await executeMutations(doc, steps, args.changeMode);
2608
+ const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
2570
2609
  const preservation = await preserveRunPatternAfterRewrite(doc, target.nodeId, target.text, preRuns, normalizedText);
2571
- const post = await docSnapshot.buildDocumentSnapshot(doc);
2610
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
2572
2611
  const rewrittenBlock = findSnapshotTextByNodeId(post, target.nodeId);
2573
2612
  const verification = [
2574
2613
  revisionVerification(pre.revision, post.revision, true),
@@ -2977,7 +3016,7 @@ async function listAllTrackedChanges(listFn) {
2977
3016
  }
2978
3017
  }
2979
3018
  async function runNormalizeBodyFontSize(doc, args) {
2980
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3019
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
2981
3020
  try {
2982
3021
  if (!Number.isFinite(args.fontSize) || args.fontSize <= 0) {
2983
3022
  return failedReceipt('normalize_body_font_size', new Error('fontSize must be a positive number'), pre);
@@ -2994,7 +3033,7 @@ async function runNormalizeBodyFontSize(doc, args) {
2994
3033
  where: { by: 'block', nodeType: block.nodeType, nodeId: block.nodeId },
2995
3034
  args: { inline: { fontSize: args.fontSize }, scope: 'block' },
2996
3035
  }));
2997
- const result = await executeMutations(doc, steps, args.changeMode);
3036
+ const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
2998
3037
  const post = await docSnapshot.buildDocumentSnapshot(doc);
2999
3038
  const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
3000
3039
  return {
@@ -3025,7 +3064,7 @@ async function runNormalizeBodyFontSize(doc, args) {
3025
3064
  * tracked-safe (changeMode).
3026
3065
  */
3027
3066
  async function runSetFontFamily(doc, args) {
3028
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3067
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
3029
3068
  try {
3030
3069
  const fontFamily = args.fontFamily.trim();
3031
3070
  if (fontFamily.length === 0) {
@@ -3152,7 +3191,7 @@ async function runSetFontFamily(doc, args) {
3152
3191
  * the recreated block inside an adjacent table cell.
3153
3192
  */
3154
3193
  async function runApplyStyle(doc, args) {
3155
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3194
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3156
3195
  try {
3157
3196
  const target = selectorToBlockTarget(args.selector, pre);
3158
3197
  if (!target) {
@@ -3254,7 +3293,7 @@ async function runApplyStyle(doc, args) {
3254
3293
  * escape to superdoc_execute_code and apply the alignment untracked (no pPrChange).
3255
3294
  */
3256
3295
  async function runFormatParagraph(doc, args) {
3257
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3296
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3258
3297
  try {
3259
3298
  const target = selectorToBlockTarget(args.selector, pre);
3260
3299
  if (!target) {
@@ -3273,7 +3312,7 @@ async function runFormatParagraph(doc, args) {
3273
3312
  args: { alignment: normalizedAlignment, scope: 'block' },
3274
3313
  },
3275
3314
  ];
3276
- const result = await executeMutations(doc, steps, args.changeMode);
3315
+ const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
3277
3316
  const post = await docSnapshot.buildDocumentSnapshot(doc);
3278
3317
  const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
3279
3318
  return {
@@ -3308,7 +3347,7 @@ async function runFormatParagraph(doc, args) {
3308
3347
  * still anchor on the (now tracked-deleted) source span and land right after it.
3309
3348
  */
3310
3349
  async function runMoveText(doc, args) {
3311
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3350
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
3312
3351
  try {
3313
3352
  if (!args.text || args.text.length === 0) {
3314
3353
  return failedReceipt('move_text', new Error('text (the exact source span to move) is required'), pre);
@@ -3425,7 +3464,7 @@ async function runMoveText(doc, args) {
3425
3464
  * instead of inserting blank paragraphs.
3426
3465
  */
3427
3466
  async function runSetParagraphSpacing(doc, args) {
3428
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
3467
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3429
3468
  try {
3430
3469
  const target = selectorToBlockTarget(args.selector, pre);
3431
3470
  if (!target) {
@@ -3482,7 +3521,7 @@ async function runSetParagraphSpacing(doc, args) {
3482
3521
  * THE way to "start X on a new page" instead of padding with empty paragraphs.
3483
3522
  */
3484
3523
  async function runInsertPageBreak(doc, args) {
3485
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
3524
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3486
3525
  try {
3487
3526
  const target = selectorToBlockTarget(args.selector, pre);
3488
3527
  if (!target) {
@@ -3519,7 +3558,7 @@ async function runInsertPageBreak(doc, args) {
3519
3558
  * Finds the text in the body and applies a link over its range.
3520
3559
  */
3521
3560
  async function runAddHyperlink(doc, args) {
3522
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
3561
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3523
3562
  try {
3524
3563
  const needle = args.text;
3525
3564
  const url = args.url;
@@ -3577,7 +3616,7 @@ async function runAddHyperlink(doc, args) {
3577
3616
  * dialect, and silently apply the formatting untracked.
3578
3617
  */
3579
3618
  async function runFormatText(doc, args) {
3580
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3619
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
3581
3620
  try {
3582
3621
  const inline = {};
3583
3622
  if (args.bold === true)
@@ -3738,7 +3777,7 @@ function findRanges(haystack, needle, caseSensitive) {
3738
3777
  return ranges;
3739
3778
  }
3740
3779
  async function runApplyLetterSpacing(doc, args) {
3741
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3780
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3742
3781
  try {
3743
3782
  if (!Number.isFinite(args.letterSpacing)) {
3744
3783
  return failedReceipt('apply_letter_spacing', new Error('letterSpacing must be a finite number'), pre);
@@ -3758,7 +3797,7 @@ async function runApplyLetterSpacing(doc, args) {
3758
3797
  args: { inline: { letterSpacing: args.letterSpacing }, scope: 'block' },
3759
3798
  },
3760
3799
  ];
3761
- const result = await executeMutations(doc, steps, args.changeMode);
3800
+ const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
3762
3801
  const post = await docSnapshot.buildDocumentSnapshot(doc);
3763
3802
  const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
3764
3803
  return {
@@ -3916,7 +3955,10 @@ async function runMoveRange(doc, args) {
3916
3955
  }
3917
3956
  }
3918
3957
  async function runInsertToc(doc, args) {
3919
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3958
+ const placement = args.placement ?? { at: 'document_start' };
3959
+ const pre = placement.at === 'before' || placement.at === 'after'
3960
+ ? await docSnapshot.buildMutationSnapshot(doc)
3961
+ : await docSnapshot.buildDocumentSnapshot(doc);
3920
3962
  try {
3921
3963
  const tocFn = maybeMethod(doc, ['create', 'tableOfContents']);
3922
3964
  if (!tocFn) {
@@ -3924,11 +3966,11 @@ async function runInsertToc(doc, args) {
3924
3966
  code: 'TOOL_DISPATCH_NOT_FOUND',
3925
3967
  });
3926
3968
  }
3927
- const placement = resolvePlacement(args.placement ?? { at: 'document_start' }, pre);
3969
+ const resolvedPlacement = resolvePlacement(placement, pre);
3928
3970
  const executed = [];
3929
- let tocPlacement = placement;
3971
+ let tocPlacement = resolvedPlacement;
3930
3972
  if (args.title) {
3931
- const headingResult = await executeCreateHeading(doc, args.title, 1, placement, args.changeMode);
3973
+ const headingResult = await executeCreateHeading(doc, args.title, 1, resolvedPlacement, args.changeMode);
3932
3974
  executed.push({ operationId: 'doc.create.heading', result: headingResult });
3933
3975
  const headingNodeId = asString(asRecord(asRecord(headingResult)?.heading)?.nodeId);
3934
3976
  if (headingNodeId) {
@@ -4055,7 +4097,7 @@ async function runStyleTable(doc, args) {
4055
4097
  * should reach for instead of delete-and-recreate or insert/undo churn.
4056
4098
  */
4057
4099
  async function runMoveTable(doc, args) {
4058
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
4100
+ const pre = await docSnapshot.buildMutationSnapshot(doc);
4059
4101
  try {
4060
4102
  const tableOrdinal = args.tableOrdinal ?? 1;
4061
4103
  const table = await resolveTableContextQuick(doc, tableOrdinal);
@@ -4581,8 +4623,8 @@ function parseScopedReplaceArgs(args) {
4581
4623
  changeMode: args.changeMode === 'tracked' ? 'tracked' : 'direct',
4582
4624
  };
4583
4625
  }
4584
- async function runScopedReplace(doc, args) {
4585
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
4626
+ async function runScopedReplaceUnchecked(doc, args) {
4627
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
4586
4628
  const matched = docSnapshot.resolveSnapshotSelector(pre, args.selector);
4587
4629
  if (matched.length !== 1) {
4588
4630
  const failure = describeSelectorFailure(args.selector, matched, pre, args.edits.map((e) => e.find));
@@ -4679,7 +4721,7 @@ async function runScopedReplace(doc, args) {
4679
4721
  // applies directly, so restoring marks adds no second reviewable change.
4680
4722
  const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
4681
4723
  try {
4682
- result = await executeMutations(doc, spanSteps, args.changeMode);
4724
+ result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
4683
4725
  if (result != null)
4684
4726
  await preserveScopedRunPattern(doc, nodeId, block.text, trackedPreRuns, planned);
4685
4727
  }
@@ -4701,10 +4743,10 @@ async function runScopedReplace(doc, args) {
4701
4743
  args: { replacement: { text: expected }, style: preserveRewriteStyle() },
4702
4744
  },
4703
4745
  ];
4704
- result = await executeMutations(doc, steps, args.changeMode);
4746
+ result = await executeMutations(doc, steps, args.changeMode, pre.revision);
4705
4747
  await preserveScopedRunPattern(doc, nodeId, block.text, preRuns, planned);
4706
4748
  }
4707
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
4749
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
4708
4750
  const postBlock = findTextTarget(post, nodeId);
4709
4751
  // In tracked mode the deleted text is still present in the block, so only
4710
4752
  // require the inserted text to be visible.
@@ -4743,6 +4785,14 @@ async function runScopedReplace(doc, args) {
4743
4785
  ],
4744
4786
  };
4745
4787
  }
4788
+ async function runScopedReplace(doc, args) {
4789
+ try {
4790
+ return await runScopedReplaceUnchecked(doc, args);
4791
+ }
4792
+ catch (error) {
4793
+ return failedReceipt('replace_text', error);
4794
+ }
4795
+ }
4746
4796
  /**
4747
4797
  * Build a `doc.format.apply` `inline` payload from a block row's sampled look.
4748
4798
  * Shared by apply_style (likeText copy) and add_list_items (anchor auto-match)
@@ -5280,8 +5330,18 @@ async function listBlockRows(doc) {
5280
5330
  const fn = maybeMethod(doc, ['blocks', 'list']);
5281
5331
  if (!fn)
5282
5332
  return [];
5283
- const raw = (await fn({}));
5284
- return Array.isArray(raw?.blocks) ? raw.blocks : [];
5333
+ const rows = [];
5334
+ const pageSize = 250;
5335
+ let offset = 0;
5336
+ while (true) {
5337
+ const raw = (await fn({ offset, limit: pageSize }));
5338
+ const page = Array.isArray(raw?.blocks) ? raw.blocks : [];
5339
+ rows.push(...page);
5340
+ offset += page.length;
5341
+ const total = typeof raw?.total === 'number' && Number.isFinite(raw.total) ? raw.total : rows.length;
5342
+ if (page.length === 0 || offset >= total)
5343
+ return rows;
5344
+ }
5285
5345
  }
5286
5346
  function blockNumbering(row) {
5287
5347
  const numbering = row?.numbering;
@@ -5447,7 +5507,7 @@ async function convertNumberedRange(doc, fromMarker, toMarker, kind, tracked) {
5447
5507
  };
5448
5508
  }
5449
5509
  async function convertParagraphRange(doc, fromText, toText, kind, tracked) {
5450
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
5510
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
5451
5511
  const blocks = pre.blocks ?? [];
5452
5512
  const findBlock = (needle) => blocks.findIndex((b) => b.nodeType === 'paragraph' && (b.text ?? '').toLowerCase().includes(needle.toLowerCase()));
5453
5513
  const fromIdx = findBlock(fromText);
@@ -5491,7 +5551,7 @@ async function convertParagraphRange(doc, fromText, toText, kind, tracked) {
5491
5551
  };
5492
5552
  }
5493
5553
  await createListFromParagraphRange(createFn, kind, range[0].nodeId, range[range.length - 1].nodeId, tracked ? 'tracked' : undefined);
5494
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
5554
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
5495
5555
  const postList = (post.lists ?? []).find((l) => l.items.some((it) => it.nodeId === range[0].nodeId));
5496
5556
  const allInOneList = postList != null && range.every((b) => postList.items.some((it) => it.nodeId === b.nodeId));
5497
5557
  return {
@@ -5760,7 +5820,7 @@ async function appendListAtPlacement(doc, args) {
5760
5820
  const selector = placement && isRecord(placement.selector) ? placement.selector : null;
5761
5821
  if (items.length === 0 || !placement || (at !== 'after' && at !== 'before') || !selector)
5762
5822
  return null;
5763
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
5823
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
5764
5824
  const blocks = pre.blocks ?? [];
5765
5825
  // Resolve through the SHARED selector resolver so the full selector
5766
5826
  // vocabulary works (nodeId / textSearch / ordinal / relative / ref) and
@@ -5821,7 +5881,7 @@ async function appendListAtPlacement(doc, args) {
5821
5881
  const listIds = typeof args.headingText === 'string' && args.headingText.length > 0 ? createdIds.slice(1) : createdIds;
5822
5882
  await createListFromParagraphRange(listsCreateFn, kind, listIds[0], listIds[listIds.length - 1], parseChangeMode(args.changeMode));
5823
5883
  // Verify both promises: the items form one list AND it sits at the anchor.
5824
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
5884
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
5825
5885
  const postBlocks = post.blocks ?? [];
5826
5886
  const postList = (post.lists ?? []).find((l) => l.items.some((it) => it.nodeId === listIds[0]));
5827
5887
  const listOk = postList != null && listIds.every((id) => postList.items.some((it) => it.nodeId === id));
@@ -6384,7 +6444,7 @@ async function runAttachNumbering(doc, args) {
6384
6444
  verification: [{ check: { kind: 'marker-rendered' }, passed: !!newMarker }],
6385
6445
  };
6386
6446
  }
6387
- async function superdocPerformAction(doc, args) {
6447
+ async function dispatchSuperdocPerformAction(doc, args) {
6388
6448
  if (!isRecord(args)) {
6389
6449
  throw new errors.SuperDocCliError('superdoc_perform_action arguments must be an object', {
6390
6450
  code: 'INVALID_ARGUMENT',
@@ -6996,6 +7056,24 @@ async function superdocPerformAction(doc, args) {
6996
7056
  }
6997
7057
  }
6998
7058
  }
7059
+ async function superdocPerformAction(doc, args) {
7060
+ try {
7061
+ return await dispatchSuperdocPerformAction(doc, args);
7062
+ }
7063
+ catch (error) {
7064
+ if (!(error instanceof docSnapshot.MutationSnapshotError))
7065
+ throw error;
7066
+ const action = isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
7067
+ return {
7068
+ status: 'failed',
7069
+ intent: action,
7070
+ selectedTargets: [],
7071
+ executedOperations: [],
7072
+ verification: [],
7073
+ errors: [{ code: error.code, message: error.message, recovery: { kind: 'reinspect' } }],
7074
+ };
7075
+ }
7076
+ }
6999
7077
  function parseChangeMode(value) {
7000
7078
  if (value === 'direct' || value === 'tracked')
7001
7079
  return value;