@superdoc/sdk 2.10.0-next.1 → 2.10.0-next.10

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.
Files changed (57) hide show
  1. package/README.md +45 -0
  2. package/dist/action-primitives/doc-index.cjs +6 -4
  3. package/dist/action-primitives/doc-index.js +6 -4
  4. package/dist/action-primitives/engine.cjs +6 -2
  5. package/dist/action-primitives/engine.js +6 -2
  6. package/dist/action-primitives/receipt.d.ts +4 -0
  7. package/dist/action-primitives/tools/structure-insert.d.ts +1 -1
  8. package/dist/agent/actions.cjs +384 -373
  9. package/dist/agent/actions.d.ts +5 -0
  10. package/dist/agent/actions.js +385 -374
  11. package/dist/agent/catalog.cjs +15 -0
  12. package/dist/agent/catalog.js +15 -0
  13. package/dist/agent/doc-snapshot.cjs +205 -86
  14. package/dist/agent/doc-snapshot.d.ts +11 -0
  15. package/dist/agent/doc-snapshot.js +203 -86
  16. package/dist/agent/execution-context.cjs +385 -0
  17. package/dist/agent/execution-context.d.ts +97 -0
  18. package/dist/agent/execution-context.js +376 -0
  19. package/dist/agent/runtime.cjs +123 -117
  20. package/dist/agent/runtime.d.ts +3 -0
  21. package/dist/agent/runtime.js +124 -118
  22. package/dist/agent/v2-preset-compat.cjs +5 -1
  23. package/dist/agent/v2-preset-compat.js +4 -1
  24. package/dist/embedded-tools.generated.cjs +5 -5
  25. package/dist/embedded-tools.generated.js +5 -5
  26. package/dist/generated/client.cjs +2 -0
  27. package/dist/generated/client.d.ts +85 -0
  28. package/dist/generated/client.js +2 -0
  29. package/dist/generated/contract.cjs +1796 -1297
  30. package/dist/generated/contract.js +1797 -1297
  31. package/dist/index.cjs +23 -0
  32. package/dist/index.d.ts +7 -2
  33. package/dist/index.js +23 -0
  34. package/dist/presets/core.cjs +1 -1
  35. package/dist/presets/core.js +1 -1
  36. package/dist/runtime/document-evidence.cjs +40 -0
  37. package/dist/runtime/document-evidence.d.ts +13 -0
  38. package/dist/runtime/document-evidence.js +30 -0
  39. package/dist/runtime/document-rpc.cjs +27 -0
  40. package/dist/runtime/document-rpc.d.ts +2 -0
  41. package/dist/runtime/document-rpc.js +25 -0
  42. package/dist/runtime/host.cjs +65 -4
  43. package/dist/runtime/host.d.ts +3 -0
  44. package/dist/runtime/host.js +66 -5
  45. package/dist/runtime/process.cjs +38 -0
  46. package/dist/runtime/process.d.ts +16 -0
  47. package/dist/runtime/process.js +38 -0
  48. package/dist/runtime/sdk-version.generated.cjs +1 -1
  49. package/dist/runtime/sdk-version.generated.d.ts +1 -1
  50. package/dist/runtime/sdk-version.generated.js +1 -1
  51. package/package.json +10 -8
  52. package/tools/catalog.json +89 -0
  53. package/tools/tools-policy.json +1 -1
  54. package/tools/tools.anthropic.json +89 -0
  55. package/tools/tools.generic.json +89 -0
  56. package/tools/tools.openai.json +89 -0
  57. package/tools/tools.vercel.json +89 -0
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var executionContext = require('./execution-context.cjs');
3
4
  var errors = require('../runtime/errors.cjs');
4
5
  var listTransform = require('../action-primitives/tools/list-transform.cjs');
5
6
  var structureInsert = require('../action-primitives/tools/structure-insert.cjs');
@@ -170,18 +171,18 @@ const ACTION_GROUPS = [
170
171
  * action at compile time.
171
172
  */
172
173
  const ACTION_ARGS = {
173
- insert_paragraphs: ['text', 'texts', 'headingLevel', 'placement', 'changeMode'],
174
+ insert_paragraphs: ['text', 'texts', 'headingLevel', 'placement', 'changeMode', 'evidence'],
174
175
  insert_heading: ['text', 'level', 'placement', 'changeMode'],
175
176
  replace_text: ['edits', 'selector', 'caseSensitive', 'changeMode'],
176
177
  delete_text: ['finds', 'selector', 'caseSensitive', 'changeMode'],
177
178
  delete_blocks: ['selectors', 'selector', 'changeMode'],
178
179
  append_list: ['items', 'kind', 'headingText', 'headingLevel', 'placement', 'changeMode'],
179
- create_table: ['rows', 'columns', 'cellTexts', 'placement', 'changeMode'],
180
+ create_table: ['rows', 'columns', 'cellTexts', 'placement', 'changeMode', 'evidence'],
180
181
  comment_paragraphs: ['commentText', 'scope', 'excludeBlockQuotes'],
181
182
  add_comments: ['commentText', 'selector', 'selectors'],
182
183
  resolve_comments: ['anchorText', 'reopen'],
183
184
  reply_to_comment: ['commentText', 'anchorText', 'commentId'],
184
- rewrite_block: ['text', 'selector', 'changeMode'],
185
+ rewrite_block: ['text', 'selector', 'changeMode', 'evidence'],
185
186
  accept_tracked_changes: ['author', 'changeType'],
186
187
  reject_tracked_changes: ['author', 'changeType'],
187
188
  normalize_body_font_size: ['fontSize', 'changeMode'],
@@ -216,7 +217,7 @@ const ACTION_ARGS = {
216
217
  'changeMode',
217
218
  ],
218
219
  apply_style: ['selector', 'styleId', 'headingLevel', 'likeText'],
219
- format_paragraph: ['selector', 'alignment', 'changeMode'],
220
+ format_paragraph: ['selector', 'alignment', 'changeMode', 'evidence'],
220
221
  move_text: ['text', 'afterText', 'changeMode'],
221
222
  style_table: ['tableOrdinal', 'accentColor'],
222
223
  move_table: ['tableOrdinal', 'placement'],
@@ -319,10 +320,6 @@ function findSnapshotTextByNodeId(snapshot, nodeId) {
319
320
  }
320
321
  return null;
321
322
  }
322
- function lastBlock(snapshot) {
323
- const block = snapshot.blocks[snapshot.blocks.length - 1];
324
- return block ? { nodeId: block.nodeId, nodeType: block.nodeType } : null;
325
- }
326
323
  function createdBlockTarget(result) {
327
324
  const rec = asRecord(result);
328
325
  const paragraph = asRecord(rec?.paragraph);
@@ -514,6 +511,8 @@ function revisionVerification(preRevision, postRevision, expectChanged) {
514
511
  }
515
512
  function failedReceipt(intent, err, preSnapshot) {
516
513
  const message = reasonOf(err);
514
+ const snapshotError = err instanceof docSnapshot.MutationSnapshotError ? err : null;
515
+ const errorCode = snapshotError?.code ?? asString(asRecord(err)?.code) ?? 'ACTION_FAILED';
517
516
  return {
518
517
  status: 'failed',
519
518
  intent,
@@ -523,7 +522,13 @@ function failedReceipt(intent, err, preSnapshot) {
523
522
  selectedTargets: [],
524
523
  executedOperations: [],
525
524
  verification: [],
526
- errors: [{ code: 'ACTION_FAILED', message }],
525
+ errors: [
526
+ {
527
+ code: errorCode,
528
+ message,
529
+ ...(snapshotError || errorCode === 'REVISION_CONFLICT' ? { recovery: { kind: 'reinspect' } } : {}),
530
+ },
531
+ ],
527
532
  };
528
533
  }
529
534
  async function receiptFromWorkflowResult(doc, intent, pre, workflowResult, selectedTargets = [], checks = [{ kind: 'revision-changed' }]) {
@@ -567,14 +572,16 @@ async function receiptFromWorkflowResult(doc, intent, pre, workflowResult, selec
567
572
  }
568
573
  function buildFullBlockTextTarget(snapshot, blockId) {
569
574
  const block = snapshot.blocks.find((entry) => entry.nodeId === blockId);
570
- if (!block)
575
+ const cell = block ? null : snapshot.tables.flatMap((table) => table.cells).find((entry) => entry.nodeId === blockId);
576
+ const text = block?.text ?? cell?.text;
577
+ if (text == null)
571
578
  return null;
572
579
  return {
573
580
  kind: 'text',
574
581
  blockId,
575
582
  range: {
576
583
  start: 0,
577
- end: block.text.length,
584
+ end: text.length,
578
585
  },
579
586
  };
580
587
  }
@@ -627,8 +634,8 @@ function evaluateChecks(pre, post, checks) {
627
634
  else if (check.kind === 'comment-count-delta') {
628
635
  results.push({
629
636
  check,
630
- passed: post.comments.length - pre.comments.length === check.delta,
631
- detail: `pre=${pre.comments.length} post=${post.comments.length}`,
637
+ passed: post.counts.comments - pre.counts.comments === check.delta,
638
+ detail: `pre=${pre.counts.comments} post=${post.counts.comments}`,
632
639
  });
633
640
  }
634
641
  else if (check.kind === 'tracked-change-count-delta') {
@@ -775,13 +782,28 @@ async function executeCreateHeading(doc, text, level, placement, changeMode) {
775
782
  // Dual dialect — see executeCreateParagraph.
776
783
  return fn(params, changeMode ? { changeMode } : undefined);
777
784
  }
778
- async function executeMutations(doc, steps, changeMode) {
785
+ async function executeMutations(doc, steps, changeMode, expectedRevision) {
779
786
  const fn = maybeMethod(doc, ['mutations', 'apply']);
780
787
  if (!fn)
781
788
  throw new errors.SuperDocCliError('doc.mutations.apply is not available on the document handle.', {
782
789
  code: 'TOOL_DISPATCH_NOT_FOUND',
783
790
  });
784
- return fn({ atomic: true, changeMode: changeMode ?? 'direct', steps });
791
+ return fn({
792
+ atomic: true,
793
+ changeMode: changeMode ?? 'direct',
794
+ steps,
795
+ ...(expectedRevision ? { expectedRevision } : {}),
796
+ });
797
+ }
798
+ async function previewMutations(doc, steps, changeMode, expectedRevision) {
799
+ const fn = maybeMethod(doc, ['mutations', 'preview']);
800
+ if (!fn) {
801
+ throw new errors.SuperDocCliError('doc.mutations.preview is not available on the document handle.', {
802
+ code: 'TOOL_DISPATCH_NOT_FOUND',
803
+ });
804
+ }
805
+ const result = await fn({ atomic: true, changeMode: changeMode ?? 'direct', expectedRevision, steps });
806
+ return isRecord(result) ? result : {};
785
807
  }
786
808
  async function executeCreateTable(doc, args, placement) {
787
809
  const fn = maybeMethod(doc, ['create', 'table']);
@@ -835,7 +857,7 @@ forceDirectFill = false) {
835
857
  });
836
858
  }
837
859
  const extracted = asRecord(await extractFn({}));
838
- const blocks = (Array.isArray(extracted?.blocks) ? extracted?.blocks : [])
860
+ const blocks = (Array.isArray(extracted?.blocks) ? extracted.blocks : [])
839
861
  .map((block) => asRecord(block))
840
862
  .filter((block) => block != null)
841
863
  .flatMap((block) => {
@@ -1024,97 +1046,74 @@ async function styleTableCells(doc, cells, changeMode) {
1024
1046
  await applyItemStyling(doc, nodeIds, specs, changeMode);
1025
1047
  return styled.length;
1026
1048
  }
1027
- async function runInsertParagraphs(doc, args) {
1028
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1049
+ async function runInsertParagraphs(doc, args, formatArgs) {
1050
+ const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, selectors: placementSelectors(args.placement) });
1029
1051
  try {
1030
- // `texts` is the canonical input; a single `text` is normalized to one item
1031
- // upstream in the dispatcher, but tolerate it here too.
1032
- const texts = args.texts ?? (args.text ? [args.text] : []);
1033
- const placement = resolvePlacement(args.placement, pre);
1034
- const executedOperations = [];
1035
- // Blank-line spacing between drafted paragraphs is only desirable when
1036
- // creating a NEW document (a blank doc has ~one empty block). Inserting into
1037
- // an existing doc must NOT scatter blank paragraphs through it.
1038
- const isNewDocument = (pre.counts?.blocks ?? 0) <= 1;
1039
- // For the first item: respect the requested placement and headingLevel.
1040
- // For subsequent items: append after the previously inserted block by
1041
- // using `documentEnd` (the SDK keeps blocks contiguous) so order is
1042
- // preserved.
1052
+ const pre = await context.start();
1053
+ const placement = await resolveFactPlacement(args.placement, pre);
1054
+ const isNewDocument = (await pre.count()) <= 1;
1055
+ const anchor = placement.kind === 'before' || placement.kind === 'after'
1056
+ ? await pre.block(placement.target.nodeId)
1057
+ : await pre.target({
1058
+ kind: 'placement',
1059
+ at: placement.kind === 'documentStart' ? 'document_start' : 'document_end',
1060
+ });
1061
+ const preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
1043
1062
  let currentPlacement = placement;
1044
- let headingFirst = false;
1045
- if (typeof args.headingLevel === 'number' && args.headingLevel >= 1 && args.headingLevel <= 6) {
1046
- headingFirst = true;
1047
- }
1048
- // Advance the insertion point to sit right after the block just created
1049
- // (prefer the receipt's created id; fall back to the document's last block).
1050
- const advanceAfter = async (result) => {
1051
- const created = createdBlockTarget(result);
1052
- if (created) {
1053
- return { kind: 'after', target: { kind: 'block', nodeType: created.nodeType, nodeId: created.nodeId } };
1063
+ const nodeIds = [];
1064
+ const specs = [];
1065
+ const advance = async (result) => {
1066
+ const target = createdBlockTarget(result);
1067
+ if (target) {
1068
+ currentPlacement = {
1069
+ kind: 'after',
1070
+ target: { kind: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
1071
+ };
1072
+ return target.nodeId;
1054
1073
  }
1055
- const mid = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1056
- const last = lastBlock(mid);
1057
- return last
1058
- ? { kind: 'after', target: { kind: 'block', nodeType: last.nodeType, nodeId: last.nodeId } }
1059
- : { kind: 'documentEnd' };
1074
+ const current = await context.finishRevision();
1075
+ const last = await current.target({ kind: 'placement', at: 'document_end' });
1076
+ if (!last)
1077
+ throw new Error('Created paragraph identity is unavailable.');
1078
+ currentPlacement = { kind: 'after', target: { kind: 'block', nodeType: last.nodeType, nodeId: last.nodeId } };
1079
+ return last.nodeId;
1060
1080
  };
1061
- const styledNodeIds = [];
1062
- const styledSpecs = [];
1063
- for (let i = 0; i < texts.length; i += 1) {
1064
- const text = texts[i];
1065
- const isFirst = i === 0;
1066
- const result = isFirst && headingFirst
1067
- ? await executeCreateHeading(doc, text, args.headingLevel, currentPlacement, args.changeMode)
1068
- : await executeCreateParagraph(doc, text, currentPlacement, args.changeMode);
1069
- executedOperations.push({
1070
- operationId: isFirst && headingFirst ? 'doc.create.heading' : 'doc.create.paragraph',
1071
- result,
1072
- });
1073
- // Remember the created block for the post-insert styling pass when this
1074
- // paragraph carries a runs/marks spec.
1075
- const spec = args.textSpecs?.[i];
1076
- if (spec && ((spec.runs && spec.runs.length > 0) || spec.marks != null)) {
1077
- const created = createdBlockTarget(result);
1078
- if (created?.nodeId) {
1079
- styledNodeIds.push(created.nodeId);
1080
- styledSpecs.push({ ...spec, text });
1081
- }
1082
- }
1083
- currentPlacement = await advanceAfter(result);
1084
- // New-document drafts get a blank paragraph after each item for spacing;
1085
- // inserts into an existing document do not (would scatter blank lines).
1086
- if (isNewDocument) {
1087
- const spacer = await executeCreateParagraph(doc, '', currentPlacement, args.changeMode);
1088
- executedOperations.push({ operationId: 'doc.create.paragraph', result: spacer, rationale: 'spacing' });
1089
- currentPlacement = await advanceAfter(spacer);
1081
+ const texts = args.texts ?? (args.text ? [args.text] : []);
1082
+ for (let i = 0; i < texts.length; i++) {
1083
+ const result = i === 0 && args.headingLevel
1084
+ ? await executeCreateHeading(context.document, texts[i], args.headingLevel, currentPlacement, args.changeMode)
1085
+ : await executeCreateParagraph(context.document, texts[i], currentPlacement, args.changeMode);
1086
+ const id = await advance(result);
1087
+ nodeIds.push(id);
1088
+ specs.push({ ...args.textSpecs?.[i], text: texts[i] });
1089
+ if (isNewDocument)
1090
+ await advance(await executeCreateParagraph(context.document, '', currentPlacement, args.changeMode));
1091
+ }
1092
+ if (specs.some((spec) => spec.runs?.length || spec.marks))
1093
+ await applyItemStyling(context.document, nodeIds, specs, args.changeMode);
1094
+ let post = await context.finishRevision();
1095
+ let formatted;
1096
+ if (formatArgs) {
1097
+ let localRows;
1098
+ if (!pre.complete) {
1099
+ const created = await post.page({ nodeIds, limit: Math.max(nodeIds.length, 1) });
1100
+ localRows = [...(anchor ? [anchor] : []), ...created].sort((a, b) => a.ordinal - b.ordinal);
1090
1101
  }
1102
+ const count = context.executedOperations.length;
1103
+ formatted = await matchInsertedBlockFormatting(context.document, preRows, context.receipt('insert_paragraphs', []), formatArgs, localRows);
1104
+ if (context.executedOperations.length !== count)
1105
+ post = await context.finishRevision();
1091
1106
  }
1092
- if (styledNodeIds.length > 0) {
1093
- const styling = await applyItemStyling(doc, styledNodeIds, styledSpecs, args.changeMode);
1094
- executedOperations.push({
1095
- operationId: 'doc.format.apply',
1096
- result: styling,
1097
- rationale: `Styled ${styledNodeIds.length} inserted paragraph(s)`,
1098
- });
1099
- }
1100
- const postIdentity = await readDocumentIdentity(doc);
1101
- const verification = [revisionVerification(pre.revision, postIdentity.revision, true)];
1102
- return {
1103
- status: verification.every((v) => v.passed) ? 'ok' : 'failed',
1104
- intent: 'insert_paragraphs',
1105
- preSnapshot: { revision: pre.revision, counts: pre.counts },
1106
- postSnapshot: postIdentity,
1107
- selectedTargets: [],
1108
- executedOperations,
1109
- verification,
1110
- };
1107
+ return context.receipt('insert_paragraphs', await executionContext.evaluateFactChecks(pre, post, [{ kind: 'revision-changed' }]), formatted?.contextualFormatting ? { contextualFormatting: formatted.contextualFormatting } : {});
1111
1108
  }
1112
- catch (err) {
1113
- return failedReceipt('insert_paragraphs', err, pre);
1109
+ catch (error) {
1110
+ return context.failure('insert_paragraphs', error);
1114
1111
  }
1115
1112
  }
1116
1113
  async function runInsertHeading(doc, args) {
1117
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1114
+ const pre = args.placement?.at === 'before' || args.placement?.at === 'after'
1115
+ ? await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] })
1116
+ : await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1118
1117
  try {
1119
1118
  const placement = resolvePlacement(args.placement, pre);
1120
1119
  const result = await executeCreateHeading(doc, args.text, args.level, placement, args.changeMode);
@@ -1136,10 +1135,10 @@ async function runInsertHeading(doc, args) {
1136
1135
  }
1137
1136
  async function runReplaceText(doc, args) {
1138
1137
  const selectorDomains = args.selector ? snapshotDomainsForSelector(args.selector) : null;
1139
- const requiresBlockSnapshot = args.selector != null || args.edits.length > 1;
1138
+ const requiresBlockSnapshot = args.selector != null;
1140
1139
  const preIdentity = requiresBlockSnapshot ? null : await readDocumentIdentity(doc);
1141
1140
  const pre = requiresBlockSnapshot
1142
- ? await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
1141
+ ? await docSnapshot.buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
1143
1142
  : snapshotFromIdentity(preIdentity);
1144
1143
  try {
1145
1144
  if (args.edits.length === 0) {
@@ -1149,6 +1148,7 @@ async function runReplaceText(doc, args) {
1149
1148
  const selectedTargets = [];
1150
1149
  let skippedEdits = [];
1151
1150
  let steps;
1151
+ let applyExpectedRevision;
1152
1152
  // Blocks whose text this call rewrites, with their pre-mutation run layout:
1153
1153
  // the engine replace lane renders the replaced range as uniform runs, so a
1154
1154
  // whole-paragraph find/replace flattens intra-paragraph formatting (bold
@@ -1190,6 +1190,7 @@ async function runReplaceText(doc, args) {
1190
1190
  },
1191
1191
  },
1192
1192
  ];
1193
+ applyExpectedRevision = pre.revision;
1193
1194
  selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
1194
1195
  preserveTargets.push({
1195
1196
  nodeId: target.nodeId,
@@ -1200,98 +1201,9 @@ async function runReplaceText(doc, args) {
1200
1201
  });
1201
1202
  }
1202
1203
  else {
1203
- const matchingEdits = args.edits.length === 1
1204
- ? args.edits
1205
- : args.edits.filter((edit) => pre.blocks.some((block) => textIncludes(block.text, edit.find, caseSensitive)));
1206
- skippedEdits = args.edits.filter((edit) => !matchingEdits.includes(edit));
1207
- if (matchingEdits.length === 0) {
1208
- return {
1209
- status: 'failed',
1210
- intent: 'replace_text',
1211
- preSnapshot: { revision: pre.revision, counts: pre.counts },
1212
- selectedTargets: [],
1213
- executedOperations: [],
1214
- verification: [],
1215
- errors: [
1216
- {
1217
- code: 'ACTION_FAILED',
1218
- message: 'none of the requested text replacements matched the current document',
1219
- },
1220
- ],
1221
- };
1222
- }
1223
- // Rewrite affected BODY blocks with block-targeted rewrites. The select
1224
- // (matched-range) rewrite lane flattens the WHOLE paragraph's runs in
1225
- // collaborative sessions and leaves format.apply inert on the block
1226
- // afterwards, so per-block rewrites + the preserve pass below are the
1227
- // only path that keeps intra-paragraph formatting alive. Bounded; edits
1228
- // that touch no scanned body block (e.g. header/footer text) fall back
1229
- // to a select-rewrite step.
1230
- // Tracked mode keeps the matched-range lane. A block rewrite asks the
1231
- // engine to delete and re-insert the WHOLE paragraph, so a reviewer sees
1232
- // the entire block struck and re-added instead of the words that changed,
1233
- // and each block contributes its own replacement/deletion pair. The
1234
- // per-block lane exists to keep intra-paragraph formatting alive, which
1235
- // is a direct-mode concern; under tracked mode the revision itself is the
1236
- // reviewable unit and must stay scoped to the edit.
1237
- const scanBlocks = args.changeMode === 'tracked'
1238
- ? []
1239
- : pre.blocks.length > 0
1240
- ? pre.blocks
1241
- : (await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] })).blocks;
1242
- // An edit counts as covered only when EVERY block holding it got a
1243
- // rewrite target. The per-block lane is bounded (run capture costs a read
1244
- // each), so a find present in more blocks than the cap must fall through
1245
- // to the select lane below, which carries `require: 'all'`. Marking such
1246
- // an edit covered after its first match would replace only the first
1247
- // PRESERVE_TARGET_CAP blocks and still report success.
1248
- const blocksHoldingEdit = matchingEdits.map((edit) => scanBlocks.filter((block) => block.nodeId && block.text && textIncludes(block.text, edit.find, caseSensitive))
1249
- .length);
1250
- const blocksCoveredForEdit = Array.from({ length: matchingEdits.length }, () => 0);
1251
- for (const block of scanBlocks) {
1252
- if (preserveTargets.length >= PRESERVE_TARGET_CAP)
1253
- break;
1254
- if (!block.nodeId || !block.text)
1255
- continue;
1256
- let newText = block.text;
1257
- const touched = [];
1258
- matchingEdits.forEach((edit, editIndex) => {
1259
- if (textIncludes(newText, edit.find, caseSensitive)) {
1260
- newText = replaceAllText(newText, edit.find, edit.replace, caseSensitive);
1261
- touched.push(editIndex);
1262
- }
1263
- });
1264
- if (newText === block.text)
1265
- continue;
1266
- preserveTargets.push({
1267
- nodeId: block.nodeId,
1268
- nodeType: block.nodeType ?? 'paragraph',
1269
- oldText: block.text,
1270
- newText,
1271
- runs: await captureBlockRuns(doc, block.nodeId, block.text),
1272
- });
1273
- for (const editIndex of touched)
1274
- blocksCoveredForEdit[editIndex] += 1;
1275
- }
1276
- const coveredEdits = new Set(matchingEdits
1277
- .map((_edit, editIndex) => editIndex)
1278
- .filter((editIndex) => blocksHoldingEdit[editIndex] > 0 && blocksCoveredForEdit[editIndex] >= blocksHoldingEdit[editIndex]));
1279
- steps = [
1280
- ...preserveTargets.map((entry, index) => ({
1281
- id: `replace-block-${index + 1}`,
1282
- op: 'text.rewrite',
1283
- where: { by: 'block', nodeType: entry.nodeType, nodeId: entry.nodeId },
1284
- args: {
1285
- replacement: { text: entry.newText },
1286
- style: preserveRewriteStyle(),
1287
- },
1288
- })),
1289
- // Edits that touched no scanned body block (header/footer text, or a
1290
- // doc larger than the scan window) keep the original select-rewrite.
1291
- ...matchingEdits
1292
- .map((edit, editIndex) => ({ edit, editIndex }))
1293
- .filter(({ editIndex }) => !coveredEdits.has(editIndex))
1294
- .map(({ edit, editIndex }) => ({
1204
+ let candidates = args.edits.map((edit, editIndex) => ({
1205
+ edit,
1206
+ step: {
1295
1207
  id: `replace-${editIndex + 1}`,
1296
1208
  op: 'text.rewrite',
1297
1209
  where: {
@@ -1308,10 +1220,42 @@ async function runReplaceText(doc, args) {
1308
1220
  replacement: { text: edit.replace },
1309
1221
  style: preserveRewriteStyle(),
1310
1222
  },
1311
- })),
1312
- ];
1223
+ },
1224
+ }));
1225
+ if (candidates.length > 1) {
1226
+ applyExpectedRevision = pre.revision;
1227
+ while (candidates.length > 0) {
1228
+ const preview = await previewMutations(doc, candidates.map((candidate) => candidate.step), args.changeMode, pre.revision);
1229
+ if (preview.valid === true)
1230
+ break;
1231
+ const failures = Array.isArray(preview.failures) ? preview.failures.filter(isRecord) : [];
1232
+ const absent = failures.find((failure) => failure.code === 'MATCH_NOT_FOUND' && typeof failure.stepId === 'string');
1233
+ if (!absent) {
1234
+ throw new Error(typeof failures[0]?.message === 'string' ? failures[0].message : 'replacement plan could not be prepared');
1235
+ }
1236
+ const absentIndex = candidates.findIndex((candidate) => candidate.step.id === absent.stepId);
1237
+ if (absentIndex < 0)
1238
+ throw new Error('replacement preview returned an unknown step id');
1239
+ skippedEdits.push({ find: candidates[absentIndex].edit.find });
1240
+ candidates = candidates.filter((_candidate, index) => index !== absentIndex);
1241
+ }
1242
+ }
1243
+ if (candidates.length === 0) {
1244
+ return {
1245
+ status: 'failed',
1246
+ intent: 'replace_text',
1247
+ preSnapshot: { revision: pre.revision, counts: pre.counts },
1248
+ selectedTargets: [],
1249
+ executedOperations: [],
1250
+ verification: [],
1251
+ errors: [
1252
+ { code: 'ACTION_FAILED', message: 'none of the requested text replacements matched the current document' },
1253
+ ],
1254
+ };
1255
+ }
1256
+ steps = candidates.map((candidate) => candidate.step);
1313
1257
  }
1314
- const result = await executeMutations(doc, steps, args.changeMode);
1258
+ const result = await executeMutations(doc, steps, args.changeMode, applyExpectedRevision);
1315
1259
  const preserved = [];
1316
1260
  for (const entry of preserveTargets) {
1317
1261
  const restored = await preserveRunPatternAfterRewrite(doc, entry.nodeId, entry.oldText, entry.runs, entry.newText);
@@ -1319,7 +1263,7 @@ async function runReplaceText(doc, args) {
1319
1263
  preserved.push({ nodeId: entry.nodeId, ...restored });
1320
1264
  }
1321
1265
  if (args.selector && selectedTargets[0]) {
1322
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] });
1266
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] });
1323
1267
  const blockId = selectedTargets[0].matched[0];
1324
1268
  const postTarget = findSnapshotTextByNodeId(post, blockId);
1325
1269
  const preTarget = findSnapshotTextByNodeId(pre, blockId);
@@ -1388,7 +1332,7 @@ async function runDeleteText(doc, args) {
1388
1332
  // path). Rewriting the block minus the finds keeps the deletion local — without
1389
1333
  // this a short/whitespace find matches document-wide and blows the target cap.
1390
1334
  if (args.selector) {
1391
- const scopedPre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: snapshotDomainsForSelector(args.selector) });
1335
+ const scopedPre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: snapshotDomainsForSelector(args.selector) });
1392
1336
  try {
1393
1337
  if (args.finds.length === 0) {
1394
1338
  return failedReceipt('delete_text', new Error('finds must be non-empty'), scopedPre);
@@ -1431,7 +1375,7 @@ async function runDeleteText(doc, args) {
1431
1375
  args: { replacement: { text: rewritten }, style: preserveRewriteStyle() },
1432
1376
  },
1433
1377
  ];
1434
- const scopedResult = await executeMutations(doc, scopedSteps, args.changeMode);
1378
+ const scopedResult = await executeMutations(doc, scopedSteps, args.changeMode, scopedPre.revision);
1435
1379
  const scopedPost = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1436
1380
  const verification = evaluateChecks(scopedPre, scopedPost, [{ kind: 'revision-changed' }]);
1437
1381
  return {
@@ -1493,7 +1437,7 @@ async function runDeleteText(doc, args) {
1493
1437
  },
1494
1438
  args: {},
1495
1439
  }));
1496
- const result = await executeMutations(doc, steps, args.changeMode);
1440
+ const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
1497
1441
  const revision = asRecord(asRecord(result)?.revision);
1498
1442
  const postIdentity = args.changeMode === 'tracked'
1499
1443
  ? await readDocumentIdentity(doc)
@@ -1536,7 +1480,7 @@ async function runDeleteBlocks(doc, args) {
1536
1480
  for (const domain of snapshotDomainsForSelector(selector))
1537
1481
  domains.add(domain);
1538
1482
  }
1539
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: [...domains] });
1483
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: [...domains] });
1540
1484
  try {
1541
1485
  if (args.selectors.length === 0) {
1542
1486
  return failedReceipt('delete_blocks', new Error('selectors must be non-empty'), pre);
@@ -1629,7 +1573,7 @@ async function runDeleteBlocks(doc, args) {
1629
1573
  });
1630
1574
  }
1631
1575
  }
1632
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: [...domains] });
1576
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: [...domains] });
1633
1577
  // Tracked deletions leave the block in place until the revision is decided,
1634
1578
  // so the block count cannot move — count the structural revisions instead.
1635
1579
  // Direct deletions must show one fewer block of each deleted node type.
@@ -1747,7 +1691,7 @@ async function runInsertListItems(doc, args) {
1747
1691
  }
1748
1692
  }
1749
1693
  async function runAddListItems(doc, args, opts) {
1750
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1694
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
1751
1695
  try {
1752
1696
  const needle = args.anchorText?.trim();
1753
1697
  const anchorNodeId = args.anchorNodeId?.trim();
@@ -2280,56 +2224,79 @@ async function runSplitList(doc, args) {
2280
2224
  return failedReceipt('split_list', err, pre);
2281
2225
  }
2282
2226
  }
2227
+ async function resolveFactPlacement(placement, facts) {
2228
+ if (!placement || placement.at === 'document_end')
2229
+ return { kind: 'documentEnd' };
2230
+ if (placement.at === 'document_start')
2231
+ return { kind: 'documentStart' };
2232
+ const target = await facts.target(placement.selector);
2233
+ if (!target)
2234
+ throw new errors.SuperDocCliError('placement selector did not resolve to a unique body block', {
2235
+ code: 'INVALID_ARGUMENT',
2236
+ });
2237
+ return { kind: placement.at, target: { kind: 'block', nodeType: target.nodeType, nodeId: target.nodeId } };
2238
+ }
2239
+ function placementSelectors(placement) {
2240
+ return placement?.at === 'before' || placement?.at === 'after' ? [placement.selector] : [];
2241
+ }
2283
2242
  async function runCreateTable(doc, args) {
2284
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
2243
+ const checks = [
2244
+ { kind: 'revision-changed' },
2245
+ { kind: 'block-count-delta', nodeType: 'table', delta: 1 },
2246
+ ];
2247
+ const context = new executionContext.ExecutionContext(doc, {
2248
+ evidence: args.evidence,
2249
+ selectors: placementSelectors(args.placement),
2250
+ checks,
2251
+ });
2285
2252
  try {
2286
- if (!Number.isInteger(args.rows) || args.rows < 1 || !Number.isInteger(args.columns) || args.columns < 1) {
2287
- return failedReceipt('create_table', new Error('rows and columns must be positive integers'), pre);
2288
- }
2289
- const placement = resolvePlacement(args.placement, pre);
2290
- const insertedTableOrdinal = estimateInsertedTableOrdinal(pre, placement);
2291
- const result = await executeCreateTable(doc, args, placement);
2292
- const executedOperations = [
2293
- { operationId: 'doc.create.table', result },
2294
- ];
2295
- const createdTableNodeId = asString(asRecord(asRecord(result)?.table)?.nodeId);
2296
- if (createdTableNodeId && args.cellTexts) {
2297
- const appliedCells = await applyTableCellTexts(doc, createdTableNodeId, insertedTableOrdinal, flattenCellTexts(args.cellTexts), args.changeMode);
2298
- if (appliedCells.length > 0) {
2299
- executedOperations.push({
2300
- operationId: 'doc.mutations.apply',
2301
- rationale: `Populated ${appliedCells.length} table cells.`,
2302
- });
2253
+ if (!Number.isInteger(args.rows) || args.rows < 1 || !Number.isInteger(args.columns) || args.columns < 1)
2254
+ throw new Error('rows and columns must be positive integers');
2255
+ const pre = await context.start();
2256
+ const placement = await resolveFactPlacement(args.placement, pre);
2257
+ const result = await executeCreateTable(context.document, args, placement);
2258
+ const tableNodeId = asString(asRecord(asRecord(result)?.table)?.nodeId);
2259
+ if (args.cellTexts && tableNodeId) {
2260
+ const cells = flattenCellTexts(args.cellTexts).filter((cell) => cell.text.trim());
2261
+ if (pre.complete) {
2262
+ const applied = await applyTableCellTexts(context.document, tableNodeId, estimateInsertedTableOrdinal(pre.complete, placement), cells, args.changeMode);
2263
+ await styleTableCells(context.document, applied, args.changeMode);
2303
2264
  }
2304
- const styledCount = await styleTableCells(doc, appliedCells, args.changeMode);
2305
- if (styledCount > 0) {
2306
- executedOperations.push({
2307
- operationId: 'doc.format.apply',
2308
- rationale: `Styled ${styledCount} table cell(s).`,
2309
- });
2265
+ else {
2266
+ for (const cell of cells)
2267
+ await context.document.tables.setCellText({
2268
+ target: { kind: 'block', nodeType: 'table', nodeId: tableNodeId },
2269
+ rowIndex: cell.rowIndex,
2270
+ columnIndex: cell.columnIndex,
2271
+ text: cell.text,
2272
+ changeMode: args.changeMode,
2273
+ });
2274
+ const styled = cells.filter((cell) => cell.runs?.length || cell.marks);
2275
+ if (styled.length) {
2276
+ const identities = await doc.tables.getCells({ nodeId: tableNodeId });
2277
+ const resolved = styled.map((cell) => {
2278
+ const identity = identities.cells.find((item) => item.rowIndex === cell.rowIndex &&
2279
+ item.columnIndex <= cell.columnIndex &&
2280
+ item.columnIndex + item.colspan > cell.columnIndex);
2281
+ if (!identity?.firstParagraphNodeId)
2282
+ throw new Error('Required cell paragraph identity is unavailable.');
2283
+ return { ...cell, nodeId: identity.firstParagraphNodeId };
2284
+ });
2285
+ const styling = await applyItemStyling(context.document, resolved.map((cell) => cell.nodeId), resolved, args.changeMode);
2286
+ if (styling.skipped)
2287
+ throw new Error(`Requested cell styling failed: ${styling.skipped}`);
2288
+ }
2310
2289
  }
2311
2290
  }
2312
- const post = await docSnapshot.buildDocumentSnapshot(doc);
2313
- const verification = evaluateChecks(pre, post, [
2314
- { kind: 'revision-changed' },
2315
- { kind: 'block-count-delta', nodeType: 'table', delta: 1 },
2316
- ]);
2317
- return {
2318
- status: verification.every((v) => v.passed) ? 'ok' : 'failed',
2319
- intent: 'create_table',
2320
- preSnapshot: { revision: pre.revision, counts: pre.counts },
2321
- postSnapshot: { revision: post.revision, counts: post.counts },
2322
- selectedTargets: [],
2323
- executedOperations,
2324
- verification,
2325
- };
2291
+ const post = await context.finish();
2292
+ return context.receipt('create_table', await executionContext.evaluateFactChecks(pre, post, checks));
2326
2293
  }
2327
- catch (err) {
2328
- return failedReceipt('create_table', err, pre);
2294
+ catch (error) {
2295
+ return context.failure('create_table', error);
2329
2296
  }
2330
2297
  }
2331
2298
  async function runCommentParagraphs(doc, args) {
2332
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
2299
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
2333
2300
  try {
2334
2301
  const blocks = pre.blocks.filter((b) => {
2335
2302
  if (args.scope === 'all') {
@@ -2371,13 +2338,18 @@ async function runCommentParagraphs(doc, args) {
2371
2338
  }
2372
2339
  }
2373
2340
  async function runAddComments(doc, args) {
2374
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
2341
+ const selectorList = args.selectors?.length ? args.selectors : args.selector ? [args.selector] : [];
2342
+ const domains = new Set(['blocks']);
2343
+ for (const selector of selectorList) {
2344
+ for (const domain of snapshotDomainsForSelector(selector))
2345
+ domains.add(domain);
2346
+ }
2347
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: [...domains] });
2375
2348
  try {
2376
2349
  // Batch form: `selectors` comments many blocks in ONE action call so the
2377
2350
  // model never fans out N concurrent add_comments tool calls (which race the
2378
2351
  // shared document and the comment-count verification). A single `selector`
2379
2352
  // is the one-target shorthand. Comments are applied sequentially.
2380
- const selectorList = args.selectors?.length ? args.selectors : args.selector ? [args.selector] : [];
2381
2353
  if (!selectorList.length) {
2382
2354
  return failedReceipt('add_comments', new Error('add_comments requires a "selector" or a non-empty "selectors" array'), pre);
2383
2355
  }
@@ -2391,7 +2363,21 @@ async function runAddComments(doc, args) {
2391
2363
  unresolved.push(sel);
2392
2364
  }
2393
2365
  if (!resolved.length) {
2394
- return failedReceipt('add_comments', new Error('no selector resolved to a body block'), pre);
2366
+ return {
2367
+ status: 'failed',
2368
+ intent: 'add_comments',
2369
+ preSnapshot: { revision: pre.revision, counts: pre.counts },
2370
+ selectedTargets: unresolved.map((selector) => ({ selector, matched: [] })),
2371
+ executedOperations: [],
2372
+ verification: [],
2373
+ errors: [
2374
+ {
2375
+ code: 'TARGET_NOT_FOUND',
2376
+ message: 'none of the requested comment selectors resolved to a block',
2377
+ recovery: { kind: 'reinspect' },
2378
+ },
2379
+ ],
2380
+ };
2395
2381
  }
2396
2382
  const executed = [];
2397
2383
  for (const { commentText, nodeId } of resolved.map((r) => ({ commentText: args.commentText, nodeId: r.nodeId }))) {
@@ -2400,15 +2386,20 @@ async function runAddComments(doc, args) {
2400
2386
  }
2401
2387
  const post = await docSnapshot.buildDocumentSnapshot(doc);
2402
2388
  const verification = evaluateChecks(pre, post, [{ kind: 'comment-count-delta', delta: resolved.length }]);
2389
+ const verified = verification.every((v) => v.passed);
2390
+ const status = !verified ? 'failed' : unresolved.length > 0 ? 'partial' : 'ok';
2403
2391
  return {
2404
- status: verification.every((v) => v.passed) ? 'ok' : 'failed',
2392
+ status,
2405
2393
  intent: 'add_comments',
2406
2394
  ...(unresolved.length
2407
2395
  ? { note: `${unresolved.length} selector(s) did not resolve to a block and were skipped` }
2408
2396
  : {}),
2409
2397
  preSnapshot: { revision: pre.revision, counts: pre.counts },
2410
2398
  postSnapshot: { revision: post.revision, counts: post.counts },
2411
- selectedTargets: resolved.map((r) => ({ selector: r.selector, matched: [r.nodeId] })),
2399
+ selectedTargets: [
2400
+ ...resolved.map((r) => ({ selector: r.selector, matched: [r.nodeId] })),
2401
+ ...unresolved.map((selector) => ({ selector, matched: [] })),
2402
+ ],
2412
2403
  executedOperations: executed,
2413
2404
  verification,
2414
2405
  };
@@ -2584,67 +2575,39 @@ async function runReplyToComment(doc, args) {
2584
2575
  }
2585
2576
  }
2586
2577
  async function runRewriteBlock(doc, args) {
2587
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
2578
+ const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, selectors: [args.selector] });
2588
2579
  try {
2589
- const target = selectorToBlockTarget(args.selector, pre);
2590
- if (!target) {
2591
- return failedReceipt('rewrite_block', new Error('selector did not resolve to a body block'), pre);
2592
- }
2593
- const normalizedText = normalizeTitleLikeRewriteText(target.text, args.text);
2594
- // The engine rewrites the whole block as ONE uniform run — losing an
2595
- // intra-paragraph pattern (bold "(n)" lead-in), or smearing the first
2596
- // run's marks across the entire text. Capture the current run layout so
2597
- // it can be re-applied over the unchanged prefix/suffix afterward.
2598
- const preRuns = await captureBlockRuns(doc, target.nodeId, target.text);
2599
- const steps = [
2580
+ const pre = await context.start();
2581
+ const target = await pre.target(args.selector);
2582
+ if (!target)
2583
+ throw new Error('selector did not resolve to a body block');
2584
+ context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
2585
+ const text = normalizeTitleLikeRewriteText(target.text, args.text);
2586
+ const runs = await captureBlockRuns(doc, target.nodeId, target.text);
2587
+ await executeMutations(context.document, [
2600
2588
  {
2601
2589
  id: 'rewrite-block-1',
2602
2590
  op: 'text.rewrite',
2603
2591
  where: { by: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
2604
- args: {
2605
- replacement: { text: normalizedText },
2606
- style: preserveRewriteStyle(),
2607
- },
2592
+ args: { replacement: { text }, style: preserveRewriteStyle() },
2608
2593
  },
2609
- ];
2610
- const result = await executeMutations(doc, steps, args.changeMode);
2611
- const preservation = await preserveRunPatternAfterRewrite(doc, target.nodeId, target.text, preRuns, normalizedText);
2612
- const post = await docSnapshot.buildDocumentSnapshot(doc);
2613
- const rewrittenBlock = findSnapshotTextByNodeId(post, target.nodeId);
2594
+ ], args.changeMode, pre.revision);
2595
+ await preserveRunPatternAfterRewrite(context.document, target.nodeId, target.text, runs, text);
2596
+ const post = await context.finish();
2597
+ const rewritten = await post.block(target.nodeId);
2614
2598
  const verification = [
2615
2599
  revisionVerification(pre.revision, post.revision, true),
2616
2600
  {
2617
- check: { kind: 'block-text-contains', nodeId: target.nodeId, text: normalizedText },
2618
- passed: !!rewrittenBlock && verifyRewrittenBlockText(rewrittenBlock.text, normalizedText, args.changeMode),
2601
+ check: { kind: 'block-text-contains', nodeId: target.nodeId, text },
2602
+ passed: !!rewritten && verifyRewrittenBlockText(rewritten.text, text, args.changeMode),
2619
2603
  },
2620
2604
  ];
2621
- const changed = pre.revision !== post.revision;
2622
- const rewritten = !!rewrittenBlock && verifyRewrittenBlockText(rewrittenBlock.text, args.text, args.changeMode);
2623
- return {
2624
- status: changed && rewritten ? 'ok' : 'failed',
2625
- intent: 'rewrite_block',
2626
- preSnapshot: { revision: pre.revision, counts: pre.counts },
2627
- postSnapshot: { revision: post.revision, counts: post.counts },
2628
- selectedTargets: [{ selector: args.selector, matched: [target.nodeId] }],
2629
- executedOperations: [
2630
- { operationId: 'doc.mutations.apply', result },
2631
- ...(preservation ? [{ operationId: 'doc.format.apply', result: preservation }] : []),
2632
- ],
2633
- verification,
2634
- errors: changed && rewritten
2635
- ? undefined
2636
- : [
2637
- {
2638
- code: 'ACTION_FAILED',
2639
- message: changed
2640
- ? 'rewrite_block did not produce the requested rewritten text for the selected block'
2641
- : 'rewrite_block produced no change for the selected block; keep the same target and provide a changed rewrite',
2642
- },
2643
- ],
2644
- };
2605
+ return context.receipt('rewrite_block', verification, verification.every((check) => check.passed)
2606
+ ? {}
2607
+ : { errors: [{ code: 'ACTION_FAILED', message: 'rewrite_block did not produce the requested changed text' }] });
2645
2608
  }
2646
- catch (err) {
2647
- return failedReceipt('rewrite_block', err, pre);
2609
+ catch (error) {
2610
+ return context.failure('rewrite_block', error);
2648
2611
  }
2649
2612
  }
2650
2613
  const RUN_BOOL_MARKS = ['bold', 'italic', 'underline', 'strike'];
@@ -3018,7 +2981,7 @@ async function listAllTrackedChanges(listFn) {
3018
2981
  }
3019
2982
  }
3020
2983
  async function runNormalizeBodyFontSize(doc, args) {
3021
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
2984
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3022
2985
  try {
3023
2986
  if (!Number.isFinite(args.fontSize) || args.fontSize <= 0) {
3024
2987
  return failedReceipt('normalize_body_font_size', new Error('fontSize must be a positive number'), pre);
@@ -3035,7 +2998,7 @@ async function runNormalizeBodyFontSize(doc, args) {
3035
2998
  where: { by: 'block', nodeType: block.nodeType, nodeId: block.nodeId },
3036
2999
  args: { inline: { fontSize: args.fontSize }, scope: 'block' },
3037
3000
  }));
3038
- const result = await executeMutations(doc, steps, args.changeMode);
3001
+ const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
3039
3002
  const post = await docSnapshot.buildDocumentSnapshot(doc);
3040
3003
  const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
3041
3004
  return {
@@ -3066,7 +3029,7 @@ async function runNormalizeBodyFontSize(doc, args) {
3066
3029
  * tracked-safe (changeMode).
3067
3030
  */
3068
3031
  async function runSetFontFamily(doc, args) {
3069
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3032
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
3070
3033
  try {
3071
3034
  const fontFamily = args.fontFamily.trim();
3072
3035
  if (fontFamily.length === 0) {
@@ -3193,7 +3156,7 @@ async function runSetFontFamily(doc, args) {
3193
3156
  * the recreated block inside an adjacent table cell.
3194
3157
  */
3195
3158
  async function runApplyStyle(doc, args) {
3196
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3159
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3197
3160
  try {
3198
3161
  const target = selectorToBlockTarget(args.selector, pre);
3199
3162
  if (!target) {
@@ -3295,43 +3258,30 @@ async function runApplyStyle(doc, args) {
3295
3258
  * escape to superdoc_execute_code and apply the alignment untracked (no pPrChange).
3296
3259
  */
3297
3260
  async function runFormatParagraph(doc, args) {
3298
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3261
+ const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, selectors: [args.selector] });
3299
3262
  try {
3300
- const target = selectorToBlockTarget(args.selector, pre);
3301
- if (!target) {
3302
- return failedReceipt('format_paragraph', new Error('selector did not resolve to a unique body block'), pre);
3303
- }
3304
3263
  const alignment = typeof args.alignment === 'string' ? args.alignment.trim().toLowerCase() : '';
3305
- const normalizedAlignment = alignment === 'both' ? 'justify' : alignment;
3306
- if (!['left', 'center', 'right', 'justify'].includes(normalizedAlignment)) {
3307
- return failedReceipt('format_paragraph', new Error('format_paragraph requires alignment: left | center | right | justify'), pre);
3308
- }
3309
- const steps = [
3264
+ const normalized = alignment === 'both' ? 'justify' : alignment;
3265
+ if (!['left', 'center', 'right', 'justify'].includes(normalized))
3266
+ throw new Error('format_paragraph requires alignment: left | center | right | justify');
3267
+ const pre = await context.start();
3268
+ const target = await pre.target(args.selector);
3269
+ if (!target)
3270
+ throw new Error('selector did not resolve to a unique body block');
3271
+ context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
3272
+ await executeMutations(context.document, [
3310
3273
  {
3311
3274
  id: 'format-paragraph-1',
3312
3275
  op: 'format.apply',
3313
3276
  where: { by: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
3314
- args: { alignment: normalizedAlignment, scope: 'block' },
3277
+ args: { alignment: normalized, scope: 'block' },
3315
3278
  },
3316
- ];
3317
- const result = await executeMutations(doc, steps, args.changeMode);
3318
- const post = await docSnapshot.buildDocumentSnapshot(doc);
3319
- const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
3320
- return {
3321
- status: verification.every((v) => v.passed) ? 'ok' : 'failed',
3322
- intent: `format_paragraph: ${normalizedAlignment}`,
3323
- preSnapshot: { revision: pre.revision, counts: pre.counts },
3324
- postSnapshot: { revision: post.revision, counts: post.counts },
3325
- selectedTargets: [{ selector: args.selector, matched: [target.nodeId] }],
3326
- applied: { alignment: normalizedAlignment },
3327
- executedOperations: [
3328
- { operationId: 'doc.mutations.apply', result: compactOpResult(result), rationale: `align ${target.nodeId}` },
3329
- ],
3330
- verification,
3331
- };
3279
+ ], args.changeMode, pre.revision);
3280
+ const post = await context.finish();
3281
+ return context.receipt(`format_paragraph: ${normalized}`, await executionContext.evaluateFactChecks(pre, post, [{ kind: 'revision-changed' }]), { applied: { alignment: normalized } });
3332
3282
  }
3333
- catch (err) {
3334
- return failedReceipt('format_paragraph', err, pre);
3283
+ catch (error) {
3284
+ return context.failure('format_paragraph', error);
3335
3285
  }
3336
3286
  }
3337
3287
  /**
@@ -3349,7 +3299,7 @@ async function runFormatParagraph(doc, args) {
3349
3299
  * still anchor on the (now tracked-deleted) source span and land right after it.
3350
3300
  */
3351
3301
  async function runMoveText(doc, args) {
3352
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3302
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
3353
3303
  try {
3354
3304
  if (!args.text || args.text.length === 0) {
3355
3305
  return failedReceipt('move_text', new Error('text (the exact source span to move) is required'), pre);
@@ -3466,7 +3416,7 @@ async function runMoveText(doc, args) {
3466
3416
  * instead of inserting blank paragraphs.
3467
3417
  */
3468
3418
  async function runSetParagraphSpacing(doc, args) {
3469
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
3419
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3470
3420
  try {
3471
3421
  const target = selectorToBlockTarget(args.selector, pre);
3472
3422
  if (!target) {
@@ -3523,7 +3473,7 @@ async function runSetParagraphSpacing(doc, args) {
3523
3473
  * THE way to "start X on a new page" instead of padding with empty paragraphs.
3524
3474
  */
3525
3475
  async function runInsertPageBreak(doc, args) {
3526
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
3476
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3527
3477
  try {
3528
3478
  const target = selectorToBlockTarget(args.selector, pre);
3529
3479
  if (!target) {
@@ -3560,7 +3510,7 @@ async function runInsertPageBreak(doc, args) {
3560
3510
  * Finds the text in the body and applies a link over its range.
3561
3511
  */
3562
3512
  async function runAddHyperlink(doc, args) {
3563
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
3513
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3564
3514
  try {
3565
3515
  const needle = args.text;
3566
3516
  const url = args.url;
@@ -3618,7 +3568,7 @@ async function runAddHyperlink(doc, args) {
3618
3568
  * dialect, and silently apply the formatting untracked.
3619
3569
  */
3620
3570
  async function runFormatText(doc, args) {
3621
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3571
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
3622
3572
  try {
3623
3573
  const inline = {};
3624
3574
  if (args.bold === true)
@@ -3779,7 +3729,7 @@ function findRanges(haystack, needle, caseSensitive) {
3779
3729
  return ranges;
3780
3730
  }
3781
3731
  async function runApplyLetterSpacing(doc, args) {
3782
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3732
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3783
3733
  try {
3784
3734
  if (!Number.isFinite(args.letterSpacing)) {
3785
3735
  return failedReceipt('apply_letter_spacing', new Error('letterSpacing must be a finite number'), pre);
@@ -3799,7 +3749,7 @@ async function runApplyLetterSpacing(doc, args) {
3799
3749
  args: { inline: { letterSpacing: args.letterSpacing }, scope: 'block' },
3800
3750
  },
3801
3751
  ];
3802
- const result = await executeMutations(doc, steps, args.changeMode);
3752
+ const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
3803
3753
  const post = await docSnapshot.buildDocumentSnapshot(doc);
3804
3754
  const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
3805
3755
  return {
@@ -3957,7 +3907,10 @@ async function runMoveRange(doc, args) {
3957
3907
  }
3958
3908
  }
3959
3909
  async function runInsertToc(doc, args) {
3960
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
3910
+ const placement = args.placement ?? { at: 'document_start' };
3911
+ const pre = placement.at === 'before' || placement.at === 'after'
3912
+ ? await docSnapshot.buildMutationSnapshot(doc)
3913
+ : await docSnapshot.buildDocumentSnapshot(doc);
3961
3914
  try {
3962
3915
  const tocFn = maybeMethod(doc, ['create', 'tableOfContents']);
3963
3916
  if (!tocFn) {
@@ -3965,11 +3918,11 @@ async function runInsertToc(doc, args) {
3965
3918
  code: 'TOOL_DISPATCH_NOT_FOUND',
3966
3919
  });
3967
3920
  }
3968
- const placement = resolvePlacement(args.placement ?? { at: 'document_start' }, pre);
3921
+ const resolvedPlacement = resolvePlacement(placement, pre);
3969
3922
  const executed = [];
3970
- let tocPlacement = placement;
3923
+ let tocPlacement = resolvedPlacement;
3971
3924
  if (args.title) {
3972
- const headingResult = await executeCreateHeading(doc, args.title, 1, placement, args.changeMode);
3925
+ const headingResult = await executeCreateHeading(doc, args.title, 1, resolvedPlacement, args.changeMode);
3973
3926
  executed.push({ operationId: 'doc.create.heading', result: headingResult });
3974
3927
  const headingNodeId = asString(asRecord(asRecord(headingResult)?.heading)?.nodeId);
3975
3928
  if (headingNodeId) {
@@ -4096,7 +4049,7 @@ async function runStyleTable(doc, args) {
4096
4049
  * should reach for instead of delete-and-recreate or insert/undo churn.
4097
4050
  */
4098
4051
  async function runMoveTable(doc, args) {
4099
- const pre = await docSnapshot.buildDocumentSnapshot(doc);
4052
+ const pre = await docSnapshot.buildMutationSnapshot(doc);
4100
4053
  try {
4101
4054
  const tableOrdinal = args.tableOrdinal ?? 1;
4102
4055
  const table = await resolveTableContextQuick(doc, tableOrdinal);
@@ -4622,8 +4575,8 @@ function parseScopedReplaceArgs(args) {
4622
4575
  changeMode: args.changeMode === 'tracked' ? 'tracked' : 'direct',
4623
4576
  };
4624
4577
  }
4625
- async function runScopedReplace(doc, args) {
4626
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
4578
+ async function runScopedReplaceUnchecked(doc, args) {
4579
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
4627
4580
  const matched = docSnapshot.resolveSnapshotSelector(pre, args.selector);
4628
4581
  if (matched.length !== 1) {
4629
4582
  const failure = describeSelectorFailure(args.selector, matched, pre, args.edits.map((e) => e.find));
@@ -4720,7 +4673,7 @@ async function runScopedReplace(doc, args) {
4720
4673
  // applies directly, so restoring marks adds no second reviewable change.
4721
4674
  const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
4722
4675
  try {
4723
- result = await executeMutations(doc, spanSteps, args.changeMode);
4676
+ result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
4724
4677
  if (result != null)
4725
4678
  await preserveScopedRunPattern(doc, nodeId, block.text, trackedPreRuns, planned);
4726
4679
  }
@@ -4742,10 +4695,10 @@ async function runScopedReplace(doc, args) {
4742
4695
  args: { replacement: { text: expected }, style: preserveRewriteStyle() },
4743
4696
  },
4744
4697
  ];
4745
- result = await executeMutations(doc, steps, args.changeMode);
4698
+ result = await executeMutations(doc, steps, args.changeMode, pre.revision);
4746
4699
  await preserveScopedRunPattern(doc, nodeId, block.text, preRuns, planned);
4747
4700
  }
4748
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
4701
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
4749
4702
  const postBlock = findTextTarget(post, nodeId);
4750
4703
  // In tracked mode the deleted text is still present in the block, so only
4751
4704
  // require the inserted text to be visible.
@@ -4784,6 +4737,14 @@ async function runScopedReplace(doc, args) {
4784
4737
  ],
4785
4738
  };
4786
4739
  }
4740
+ async function runScopedReplace(doc, args) {
4741
+ try {
4742
+ return await runScopedReplaceUnchecked(doc, args);
4743
+ }
4744
+ catch (error) {
4745
+ return failedReceipt('replace_text', error);
4746
+ }
4747
+ }
4787
4748
  /**
4788
4749
  * Build a `doc.format.apply` `inline` payload from a block row's sampled look.
4789
4750
  * Shared by apply_style (likeText copy) and add_list_items (anchor auto-match)
@@ -4839,12 +4800,6 @@ function paragraphMarkRunPropsOf(payload) {
4839
4800
  * Spaced dashes are listed separately from bare ones because the label run
4840
4801
  * usually stops BEFORE the space ("JavaScript / TypeScript" + " — SuperDoc…").
4841
4802
  */
4842
- /**
4843
- * How many blocks the per-block replace lane will capture runs for. Each target
4844
- * costs a `query.match` read, so the lane is bounded; edits reaching past it
4845
- * fall through to the select lane rather than being silently truncated.
4846
- */
4847
- const PRESERVE_TARGET_CAP = 20;
4848
4803
  const LEAD_IN_DELIMITERS = [':', ' — ', ' – ', ' - ', '—', '–'];
4849
4804
  /** A lead-in is a LABEL, not a sentence — longer spans are prose, not a pattern. */
4850
4805
  const LEAD_IN_MAX_CHARS = 90;
@@ -5327,8 +5282,18 @@ async function listBlockRows(doc) {
5327
5282
  const fn = maybeMethod(doc, ['blocks', 'list']);
5328
5283
  if (!fn)
5329
5284
  return [];
5330
- const raw = (await fn({}));
5331
- return Array.isArray(raw?.blocks) ? raw.blocks : [];
5285
+ const rows = [];
5286
+ const pageSize = 250;
5287
+ let offset = 0;
5288
+ while (true) {
5289
+ const raw = (await fn({ offset, limit: pageSize }));
5290
+ const page = Array.isArray(raw?.blocks) ? raw.blocks : [];
5291
+ rows.push(...page);
5292
+ offset += page.length;
5293
+ const total = typeof raw?.total === 'number' && Number.isFinite(raw.total) ? raw.total : rows.length;
5294
+ if (page.length === 0 || offset >= total)
5295
+ return rows;
5296
+ }
5332
5297
  }
5333
5298
  function blockNumbering(row) {
5334
5299
  const numbering = row?.numbering;
@@ -5494,7 +5459,7 @@ async function convertNumberedRange(doc, fromMarker, toMarker, kind, tracked) {
5494
5459
  };
5495
5460
  }
5496
5461
  async function convertParagraphRange(doc, fromText, toText, kind, tracked) {
5497
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
5462
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
5498
5463
  const blocks = pre.blocks ?? [];
5499
5464
  const findBlock = (needle) => blocks.findIndex((b) => b.nodeType === 'paragraph' && (b.text ?? '').toLowerCase().includes(needle.toLowerCase()));
5500
5465
  const fromIdx = findBlock(fromText);
@@ -5538,7 +5503,7 @@ async function convertParagraphRange(doc, fromText, toText, kind, tracked) {
5538
5503
  };
5539
5504
  }
5540
5505
  await createListFromParagraphRange(createFn, kind, range[0].nodeId, range[range.length - 1].nodeId, tracked ? 'tracked' : undefined);
5541
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
5506
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
5542
5507
  const postList = (post.lists ?? []).find((l) => l.items.some((it) => it.nodeId === range[0].nodeId));
5543
5508
  const allInOneList = postList != null && range.every((b) => postList.items.some((it) => it.nodeId === b.nodeId));
5544
5509
  return {
@@ -5807,7 +5772,7 @@ async function appendListAtPlacement(doc, args) {
5807
5772
  const selector = placement && isRecord(placement.selector) ? placement.selector : null;
5808
5773
  if (items.length === 0 || !placement || (at !== 'after' && at !== 'before') || !selector)
5809
5774
  return null;
5810
- const pre = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
5775
+ const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
5811
5776
  const blocks = pre.blocks ?? [];
5812
5777
  // Resolve through the SHARED selector resolver so the full selector
5813
5778
  // vocabulary works (nodeId / textSearch / ordinal / relative / ref) and
@@ -5868,7 +5833,7 @@ async function appendListAtPlacement(doc, args) {
5868
5833
  const listIds = typeof args.headingText === 'string' && args.headingText.length > 0 ? createdIds.slice(1) : createdIds;
5869
5834
  await createListFromParagraphRange(listsCreateFn, kind, listIds[0], listIds[listIds.length - 1], parseChangeMode(args.changeMode));
5870
5835
  // Verify both promises: the items form one list AND it sits at the anchor.
5871
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
5836
+ const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
5872
5837
  const postBlocks = post.blocks ?? [];
5873
5838
  const postList = (post.lists ?? []).find((l) => l.items.some((it) => it.nodeId === listIds[0]));
5874
5839
  const listOk = postList != null && listIds.every((id) => postList.items.some((it) => it.nodeId === id));
@@ -5972,8 +5937,9 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
5972
5937
  const formatApplyFn = maybeMethod(doc, ['format', 'apply']);
5973
5938
  if (formatApplyFn) {
5974
5939
  const textLength = (created.textPreview ?? '').length;
5975
- const post = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
5976
- const full = post.blocks.find((b) => b.nodeId === created.nodeId)?.text ?? '';
5940
+ const full = createdText ||
5941
+ (await doc.blocks.list({ nodeIds: [created.nodeId], limit: 1, includeText: true })).blocks.find((block) => block.nodeId === created.nodeId)?.text ||
5942
+ '';
5977
5943
  await formatApplyFn({
5978
5944
  blockId: created.nodeId,
5979
5945
  start: 0,
@@ -6170,11 +6136,11 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
6170
6136
  }
6171
6137
  return Object.keys(applied).length > 0 ? { matchedSibling: reference.nodeId, ...applied } : null;
6172
6138
  }
6173
- async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args) {
6139
+ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, availableRows) {
6174
6140
  if (receipt.status !== 'ok')
6175
6141
  return receipt;
6176
6142
  try {
6177
- const postBlocks = await listBlockRows(doc);
6143
+ const postBlocks = availableRows ?? (await listBlockRows(doc));
6178
6144
  const preIds = new Set(preBlocks.map((b) => b.nodeId));
6179
6145
  const createdRows = postBlocks
6180
6146
  .map((b, i) => ({ row: b, index: i }))
@@ -6431,7 +6397,7 @@ async function runAttachNumbering(doc, args) {
6431
6397
  verification: [{ check: { kind: 'marker-rendered' }, passed: !!newMarker }],
6432
6398
  };
6433
6399
  }
6434
- async function superdocPerformAction(doc, args) {
6400
+ async function dispatchSuperdocPerformAction(doc, args) {
6435
6401
  if (!isRecord(args)) {
6436
6402
  throw new errors.SuperDocCliError('superdoc_perform_action arguments must be an object', {
6437
6403
  code: 'INVALID_ARGUMENT',
@@ -6465,9 +6431,9 @@ async function superdocPerformAction(doc, args) {
6465
6431
  });
6466
6432
  }
6467
6433
  const headingLevel = asNumber(args.headingLevel);
6468
- const preRows = await listBlockRows(doc);
6469
- const receipt = await runInsertParagraphs(doc, {
6434
+ return runInsertParagraphs(doc, {
6470
6435
  action,
6436
+ evidence: args.evidence,
6471
6437
  texts,
6472
6438
  textSpecs,
6473
6439
  placement: parsePlacement(args.placement),
@@ -6475,8 +6441,7 @@ async function superdocPerformAction(doc, args) {
6475
6441
  headingLevel: headingLevel != null && Number.isInteger(headingLevel) && headingLevel >= 1 && headingLevel <= 6
6476
6442
  ? headingLevel
6477
6443
  : undefined,
6478
- });
6479
- return matchInsertedBlockFormatting(doc, preRows, receipt, args);
6444
+ }, args);
6480
6445
  }
6481
6446
  case 'insert_heading': {
6482
6447
  const text = asString(args.text);
@@ -6600,6 +6565,7 @@ async function superdocPerformAction(doc, args) {
6600
6565
  }
6601
6566
  return runCreateTable(doc, {
6602
6567
  action,
6568
+ evidence: args.evidence,
6603
6569
  rows,
6604
6570
  columns,
6605
6571
  cellTexts: parseCellTexts(args.cellTexts),
@@ -6623,16 +6589,41 @@ async function superdocPerformAction(doc, args) {
6623
6589
  }
6624
6590
  case 'add_comments': {
6625
6591
  const commentText = asString(args.commentText);
6626
- const selector = parseSelector(args.selector);
6627
- const selectors = Array.isArray(args.selectors)
6628
- ? args.selectors.map((s) => parseSelector(s)).filter((s) => Boolean(s))
6629
- : undefined;
6630
- if (!commentText || (!selector && !(selectors && selectors.length))) {
6592
+ if (!commentText) {
6631
6593
  throw new errors.SuperDocCliError('add_comments requires "commentText" and a "selector" or non-empty "selectors" array', {
6632
6594
  code: 'INVALID_ARGUMENT',
6633
6595
  });
6634
6596
  }
6635
- return runAddComments(doc, { action, commentText, selector: selector ?? undefined, selectors });
6597
+ const hasSelector = args.selector != null;
6598
+ const hasSelectors = args.selectors != null;
6599
+ if (hasSelector && hasSelectors) {
6600
+ throw new errors.SuperDocCliError('add_comments requires either "selector" or "selectors", not both', {
6601
+ code: 'INVALID_ARGUMENT',
6602
+ });
6603
+ }
6604
+ if (hasSelectors) {
6605
+ if (!Array.isArray(args.selectors) || args.selectors.length === 0) {
6606
+ throw new errors.SuperDocCliError('add_comments requires "selectors" to be a non-empty array', {
6607
+ code: 'INVALID_ARGUMENT',
6608
+ });
6609
+ }
6610
+ // Parse the complete batch before mutation. Filtering an invalid entry
6611
+ // would make the receipt report success for only part of the request.
6612
+ const selectors = [];
6613
+ for (const [index, raw] of args.selectors.entries()) {
6614
+ const parsed = parseSelector(raw);
6615
+ if (!parsed) {
6616
+ throw new errors.SuperDocCliError(`add_comments selectors[${index}] is not a valid selector; no comments were added`, { code: 'INVALID_ARGUMENT' });
6617
+ }
6618
+ selectors.push(parsed);
6619
+ }
6620
+ return runAddComments(doc, { action, commentText, selectors });
6621
+ }
6622
+ const selector = parseSelector(args.selector);
6623
+ if (!selector) {
6624
+ throw new errors.SuperDocCliError('add_comments requires "selector" to be valid or "selectors" to be a non-empty array', { code: 'INVALID_ARGUMENT' });
6625
+ }
6626
+ return runAddComments(doc, { action, commentText, selector });
6636
6627
  }
6637
6628
  case 'resolve_comments':
6638
6629
  return runResolveComments(doc, {
@@ -6661,6 +6652,7 @@ async function superdocPerformAction(doc, args) {
6661
6652
  }
6662
6653
  return runRewriteBlock(doc, {
6663
6654
  action,
6655
+ evidence: args.evidence,
6664
6656
  text,
6665
6657
  selector,
6666
6658
  changeMode: parseChangeMode(args.changeMode),
@@ -6912,6 +6904,7 @@ async function superdocPerformAction(doc, args) {
6912
6904
  }
6913
6905
  return runFormatParagraph(doc, {
6914
6906
  action,
6907
+ evidence: args.evidence,
6915
6908
  selector,
6916
6909
  alignment: asString(args.alignment) ?? '',
6917
6910
  changeMode: parseChangeMode(args.changeMode),
@@ -7043,6 +7036,24 @@ async function superdocPerformAction(doc, args) {
7043
7036
  }
7044
7037
  }
7045
7038
  }
7039
+ async function superdocPerformAction(doc, args) {
7040
+ try {
7041
+ return await dispatchSuperdocPerformAction(doc, args);
7042
+ }
7043
+ catch (error) {
7044
+ if (!(error instanceof docSnapshot.MutationSnapshotError))
7045
+ throw error;
7046
+ const action = isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
7047
+ return {
7048
+ status: 'failed',
7049
+ intent: action,
7050
+ selectedTargets: [],
7051
+ executedOperations: [],
7052
+ verification: [],
7053
+ errors: [{ code: error.code, message: error.message, recovery: { kind: 'reinspect' } }],
7054
+ };
7055
+ }
7056
+ }
7046
7057
  function parseChangeMode(value) {
7047
7058
  if (value === 'direct' || value === 'tracked')
7048
7059
  return value;