@superdoc/sdk 2.10.0-next.7 → 2.10.0-next.9

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 (53) 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 +203 -223
  9. package/dist/agent/actions.d.ts +5 -0
  10. package/dist/agent/actions.js +203 -223
  11. package/dist/agent/catalog.cjs +15 -0
  12. package/dist/agent/catalog.js +15 -0
  13. package/dist/agent/doc-snapshot.cjs +22 -46
  14. package/dist/agent/doc-snapshot.js +22 -46
  15. package/dist/agent/execution-context.cjs +385 -0
  16. package/dist/agent/execution-context.d.ts +97 -0
  17. package/dist/agent/execution-context.js +376 -0
  18. package/dist/agent/runtime.cjs +116 -158
  19. package/dist/agent/runtime.d.ts +3 -0
  20. package/dist/agent/runtime.js +117 -159
  21. package/dist/agent/v2-preset-compat.cjs +5 -1
  22. package/dist/agent/v2-preset-compat.js +4 -1
  23. package/dist/embedded-tools.generated.cjs +5 -5
  24. package/dist/embedded-tools.generated.js +5 -5
  25. package/dist/generated/client.cjs +2 -0
  26. package/dist/generated/client.d.ts +47 -0
  27. package/dist/generated/client.js +2 -0
  28. package/dist/generated/contract.cjs +1605 -1297
  29. package/dist/generated/contract.js +1605 -1297
  30. package/dist/index.cjs +11 -0
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.js +11 -0
  33. package/dist/presets/core.cjs +1 -1
  34. package/dist/presets/core.js +1 -1
  35. package/dist/runtime/document-evidence.cjs +40 -0
  36. package/dist/runtime/document-evidence.d.ts +13 -0
  37. package/dist/runtime/document-evidence.js +30 -0
  38. package/dist/runtime/host.cjs +30 -2
  39. package/dist/runtime/host.d.ts +2 -0
  40. package/dist/runtime/host.js +30 -2
  41. package/dist/runtime/process.cjs +6 -0
  42. package/dist/runtime/process.d.ts +2 -0
  43. package/dist/runtime/process.js +6 -0
  44. package/dist/runtime/sdk-version.generated.cjs +1 -1
  45. package/dist/runtime/sdk-version.generated.d.ts +1 -1
  46. package/dist/runtime/sdk-version.generated.js +1 -1
  47. package/package.json +10 -9
  48. package/tools/catalog.json +35 -0
  49. package/tools/tools-policy.json +1 -1
  50. package/tools/tools.anthropic.json +35 -0
  51. package/tools/tools.generic.json +35 -0
  52. package/tools/tools.openai.json +35 -0
  53. package/tools/tools.vercel.json +35 -0
@@ -1,3 +1,4 @@
1
+ import { ExecutionContext, evaluateFactChecks } from './execution-context.js';
1
2
  import { SuperDocCliError } from '../runtime/errors.js';
2
3
  import { runSuperdocListTransformWorkflow } from '../action-primitives/tools/list-transform.js';
3
4
  import { runSuperdocStructureInsertWorkflow } from '../action-primitives/tools/structure-insert.js';
@@ -167,18 +168,18 @@ export const ACTION_GROUPS = [
167
168
  * action at compile time.
168
169
  */
169
170
  export const ACTION_ARGS = {
170
- insert_paragraphs: ['text', 'texts', 'headingLevel', 'placement', 'changeMode'],
171
+ insert_paragraphs: ['text', 'texts', 'headingLevel', 'placement', 'changeMode', 'evidence'],
171
172
  insert_heading: ['text', 'level', 'placement', 'changeMode'],
172
173
  replace_text: ['edits', 'selector', 'caseSensitive', 'changeMode'],
173
174
  delete_text: ['finds', 'selector', 'caseSensitive', 'changeMode'],
174
175
  delete_blocks: ['selectors', 'selector', 'changeMode'],
175
176
  append_list: ['items', 'kind', 'headingText', 'headingLevel', 'placement', 'changeMode'],
176
- create_table: ['rows', 'columns', 'cellTexts', 'placement', 'changeMode'],
177
+ create_table: ['rows', 'columns', 'cellTexts', 'placement', 'changeMode', 'evidence'],
177
178
  comment_paragraphs: ['commentText', 'scope', 'excludeBlockQuotes'],
178
179
  add_comments: ['commentText', 'selector', 'selectors'],
179
180
  resolve_comments: ['anchorText', 'reopen'],
180
181
  reply_to_comment: ['commentText', 'anchorText', 'commentId'],
181
- rewrite_block: ['text', 'selector', 'changeMode'],
182
+ rewrite_block: ['text', 'selector', 'changeMode', 'evidence'],
182
183
  accept_tracked_changes: ['author', 'changeType'],
183
184
  reject_tracked_changes: ['author', 'changeType'],
184
185
  normalize_body_font_size: ['fontSize', 'changeMode'],
@@ -213,7 +214,7 @@ export const ACTION_ARGS = {
213
214
  'changeMode',
214
215
  ],
215
216
  apply_style: ['selector', 'styleId', 'headingLevel', 'likeText'],
216
- format_paragraph: ['selector', 'alignment', 'changeMode'],
217
+ format_paragraph: ['selector', 'alignment', 'changeMode', 'evidence'],
217
218
  move_text: ['text', 'afterText', 'changeMode'],
218
219
  style_table: ['tableOrdinal', 'accentColor'],
219
220
  move_table: ['tableOrdinal', 'placement'],
@@ -316,10 +317,6 @@ function findSnapshotTextByNodeId(snapshot, nodeId) {
316
317
  }
317
318
  return null;
318
319
  }
319
- function lastBlock(snapshot) {
320
- const block = snapshot.blocks[snapshot.blocks.length - 1];
321
- return block ? { nodeId: block.nodeId, nodeType: block.nodeType } : null;
322
- }
323
320
  function createdBlockTarget(result) {
324
321
  const rec = asRecord(result);
325
322
  const paragraph = asRecord(rec?.paragraph);
@@ -857,7 +854,7 @@ forceDirectFill = false) {
857
854
  });
858
855
  }
859
856
  const extracted = asRecord(await extractFn({}));
860
- const blocks = (Array.isArray(extracted?.blocks) ? extracted?.blocks : [])
857
+ const blocks = (Array.isArray(extracted?.blocks) ? extracted.blocks : [])
861
858
  .map((block) => asRecord(block))
862
859
  .filter((block) => block != null)
863
860
  .flatMap((block) => {
@@ -1046,95 +1043,68 @@ async function styleTableCells(doc, cells, changeMode) {
1046
1043
  await applyItemStyling(doc, nodeIds, specs, changeMode);
1047
1044
  return styled.length;
1048
1045
  }
1049
- async function runInsertParagraphs(doc, args) {
1050
- const pre = args.placement?.at === 'before' || args.placement?.at === 'after'
1051
- ? await buildMutationSnapshot(doc, { includeDomains: ['blocks'] })
1052
- : await buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
1046
+ async function runInsertParagraphs(doc, args, formatArgs) {
1047
+ const context = new ExecutionContext(doc, { evidence: args.evidence, selectors: placementSelectors(args.placement) });
1053
1048
  try {
1054
- // `texts` is the canonical input; a single `text` is normalized to one item
1055
- // upstream in the dispatcher, but tolerate it here too.
1056
- const texts = args.texts ?? (args.text ? [args.text] : []);
1057
- const placement = resolvePlacement(args.placement, pre);
1058
- const executedOperations = [];
1059
- // Blank-line spacing between drafted paragraphs is only desirable when
1060
- // creating a NEW document (a blank doc has ~one empty block). Inserting into
1061
- // an existing doc must NOT scatter blank paragraphs through it.
1062
- const isNewDocument = (pre.counts?.blocks ?? 0) <= 1;
1063
- // For the first item: respect the requested placement and headingLevel.
1064
- // For subsequent items: append after the previously inserted block by
1065
- // using `documentEnd` (the SDK keeps blocks contiguous) so order is
1066
- // preserved.
1049
+ const pre = await context.start();
1050
+ const placement = await resolveFactPlacement(args.placement, pre);
1051
+ const isNewDocument = (await pre.count()) <= 1;
1052
+ const anchor = placement.kind === 'before' || placement.kind === 'after'
1053
+ ? await pre.block(placement.target.nodeId)
1054
+ : await pre.target({
1055
+ kind: 'placement',
1056
+ at: placement.kind === 'documentStart' ? 'document_start' : 'document_end',
1057
+ });
1058
+ const preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
1067
1059
  let currentPlacement = placement;
1068
- let headingFirst = false;
1069
- if (typeof args.headingLevel === 'number' && args.headingLevel >= 1 && args.headingLevel <= 6) {
1070
- headingFirst = true;
1071
- }
1072
- // Advance the insertion point to sit right after the block just created
1073
- // (prefer the receipt's created id; fall back to the document's last block).
1074
- const advanceAfter = async (result) => {
1075
- const created = createdBlockTarget(result);
1076
- if (created) {
1077
- return { kind: 'after', target: { kind: 'block', nodeType: created.nodeType, nodeId: created.nodeId } };
1060
+ const nodeIds = [];
1061
+ const specs = [];
1062
+ const advance = async (result) => {
1063
+ const target = createdBlockTarget(result);
1064
+ if (target) {
1065
+ currentPlacement = {
1066
+ kind: 'after',
1067
+ target: { kind: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
1068
+ };
1069
+ return target.nodeId;
1078
1070
  }
1079
- const mid = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
1080
- const last = lastBlock(mid);
1081
- return last
1082
- ? { kind: 'after', target: { kind: 'block', nodeType: last.nodeType, nodeId: last.nodeId } }
1083
- : { kind: 'documentEnd' };
1071
+ const current = await context.finishRevision();
1072
+ const last = await current.target({ kind: 'placement', at: 'document_end' });
1073
+ if (!last)
1074
+ throw new Error('Created paragraph identity is unavailable.');
1075
+ currentPlacement = { kind: 'after', target: { kind: 'block', nodeType: last.nodeType, nodeId: last.nodeId } };
1076
+ return last.nodeId;
1084
1077
  };
1085
- const styledNodeIds = [];
1086
- const styledSpecs = [];
1087
- for (let i = 0; i < texts.length; i += 1) {
1088
- const text = texts[i];
1089
- const isFirst = i === 0;
1090
- const result = isFirst && headingFirst
1091
- ? await executeCreateHeading(doc, text, args.headingLevel, currentPlacement, args.changeMode)
1092
- : await executeCreateParagraph(doc, text, currentPlacement, args.changeMode);
1093
- executedOperations.push({
1094
- operationId: isFirst && headingFirst ? 'doc.create.heading' : 'doc.create.paragraph',
1095
- result,
1096
- });
1097
- // Remember the created block for the post-insert styling pass when this
1098
- // paragraph carries a runs/marks spec.
1099
- const spec = args.textSpecs?.[i];
1100
- if (spec && ((spec.runs && spec.runs.length > 0) || spec.marks != null)) {
1101
- const created = createdBlockTarget(result);
1102
- if (created?.nodeId) {
1103
- styledNodeIds.push(created.nodeId);
1104
- styledSpecs.push({ ...spec, text });
1105
- }
1106
- }
1107
- currentPlacement = await advanceAfter(result);
1108
- // New-document drafts get a blank paragraph after each item for spacing;
1109
- // inserts into an existing document do not (would scatter blank lines).
1110
- if (isNewDocument) {
1111
- const spacer = await executeCreateParagraph(doc, '', currentPlacement, args.changeMode);
1112
- executedOperations.push({ operationId: 'doc.create.paragraph', result: spacer, rationale: 'spacing' });
1113
- currentPlacement = await advanceAfter(spacer);
1078
+ const texts = args.texts ?? (args.text ? [args.text] : []);
1079
+ for (let i = 0; i < texts.length; i++) {
1080
+ const result = i === 0 && args.headingLevel
1081
+ ? await executeCreateHeading(context.document, texts[i], args.headingLevel, currentPlacement, args.changeMode)
1082
+ : await executeCreateParagraph(context.document, texts[i], currentPlacement, args.changeMode);
1083
+ const id = await advance(result);
1084
+ nodeIds.push(id);
1085
+ specs.push({ ...args.textSpecs?.[i], text: texts[i] });
1086
+ if (isNewDocument)
1087
+ await advance(await executeCreateParagraph(context.document, '', currentPlacement, args.changeMode));
1088
+ }
1089
+ if (specs.some((spec) => spec.runs?.length || spec.marks))
1090
+ await applyItemStyling(context.document, nodeIds, specs, args.changeMode);
1091
+ let post = await context.finishRevision();
1092
+ let formatted;
1093
+ if (formatArgs) {
1094
+ let localRows;
1095
+ if (!pre.complete) {
1096
+ const created = await post.page({ nodeIds, limit: Math.max(nodeIds.length, 1) });
1097
+ localRows = [...(anchor ? [anchor] : []), ...created].sort((a, b) => a.ordinal - b.ordinal);
1114
1098
  }
1099
+ const count = context.executedOperations.length;
1100
+ formatted = await matchInsertedBlockFormatting(context.document, preRows, context.receipt('insert_paragraphs', []), formatArgs, localRows);
1101
+ if (context.executedOperations.length !== count)
1102
+ post = await context.finishRevision();
1115
1103
  }
1116
- if (styledNodeIds.length > 0) {
1117
- const styling = await applyItemStyling(doc, styledNodeIds, styledSpecs, args.changeMode);
1118
- executedOperations.push({
1119
- operationId: 'doc.format.apply',
1120
- result: styling,
1121
- rationale: `Styled ${styledNodeIds.length} inserted paragraph(s)`,
1122
- });
1123
- }
1124
- const postIdentity = await readDocumentIdentity(doc);
1125
- const verification = [revisionVerification(pre.revision, postIdentity.revision, true)];
1126
- return {
1127
- status: verification.every((v) => v.passed) ? 'ok' : 'failed',
1128
- intent: 'insert_paragraphs',
1129
- preSnapshot: { revision: pre.revision, counts: pre.counts },
1130
- postSnapshot: postIdentity,
1131
- selectedTargets: [],
1132
- executedOperations,
1133
- verification,
1134
- };
1104
+ return context.receipt('insert_paragraphs', await evaluateFactChecks(pre, post, [{ kind: 'revision-changed' }]), formatted?.contextualFormatting ? { contextualFormatting: formatted.contextualFormatting } : {});
1135
1105
  }
1136
- catch (err) {
1137
- return failedReceipt('insert_paragraphs', err, pre);
1106
+ catch (error) {
1107
+ return context.failure('insert_paragraphs', error);
1138
1108
  }
1139
1109
  }
1140
1110
  async function runInsertHeading(doc, args) {
@@ -2251,52 +2221,75 @@ async function runSplitList(doc, args) {
2251
2221
  return failedReceipt('split_list', err, pre);
2252
2222
  }
2253
2223
  }
2224
+ async function resolveFactPlacement(placement, facts) {
2225
+ if (!placement || placement.at === 'document_end')
2226
+ return { kind: 'documentEnd' };
2227
+ if (placement.at === 'document_start')
2228
+ return { kind: 'documentStart' };
2229
+ const target = await facts.target(placement.selector);
2230
+ if (!target)
2231
+ throw new SuperDocCliError('placement selector did not resolve to a unique body block', {
2232
+ code: 'INVALID_ARGUMENT',
2233
+ });
2234
+ return { kind: placement.at, target: { kind: 'block', nodeType: target.nodeType, nodeId: target.nodeId } };
2235
+ }
2236
+ function placementSelectors(placement) {
2237
+ return placement?.at === 'before' || placement?.at === 'after' ? [placement.selector] : [];
2238
+ }
2254
2239
  async function runCreateTable(doc, args) {
2255
- const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
2240
+ const checks = [
2241
+ { kind: 'revision-changed' },
2242
+ { kind: 'block-count-delta', nodeType: 'table', delta: 1 },
2243
+ ];
2244
+ const context = new ExecutionContext(doc, {
2245
+ evidence: args.evidence,
2246
+ selectors: placementSelectors(args.placement),
2247
+ checks,
2248
+ });
2256
2249
  try {
2257
- if (!Number.isInteger(args.rows) || args.rows < 1 || !Number.isInteger(args.columns) || args.columns < 1) {
2258
- return failedReceipt('create_table', new Error('rows and columns must be positive integers'), pre);
2259
- }
2260
- const placement = resolvePlacement(args.placement, pre);
2261
- const insertedTableOrdinal = estimateInsertedTableOrdinal(pre, placement);
2262
- const result = await executeCreateTable(doc, args, placement);
2263
- const executedOperations = [
2264
- { operationId: 'doc.create.table', result },
2265
- ];
2266
- const createdTableNodeId = asString(asRecord(asRecord(result)?.table)?.nodeId);
2267
- if (createdTableNodeId && args.cellTexts) {
2268
- const appliedCells = await applyTableCellTexts(doc, createdTableNodeId, insertedTableOrdinal, flattenCellTexts(args.cellTexts), args.changeMode);
2269
- if (appliedCells.length > 0) {
2270
- executedOperations.push({
2271
- operationId: 'doc.mutations.apply',
2272
- rationale: `Populated ${appliedCells.length} table cells.`,
2273
- });
2250
+ if (!Number.isInteger(args.rows) || args.rows < 1 || !Number.isInteger(args.columns) || args.columns < 1)
2251
+ throw new Error('rows and columns must be positive integers');
2252
+ const pre = await context.start();
2253
+ const placement = await resolveFactPlacement(args.placement, pre);
2254
+ const result = await executeCreateTable(context.document, args, placement);
2255
+ const tableNodeId = asString(asRecord(asRecord(result)?.table)?.nodeId);
2256
+ if (args.cellTexts && tableNodeId) {
2257
+ const cells = flattenCellTexts(args.cellTexts).filter((cell) => cell.text.trim());
2258
+ if (pre.complete) {
2259
+ const applied = await applyTableCellTexts(context.document, tableNodeId, estimateInsertedTableOrdinal(pre.complete, placement), cells, args.changeMode);
2260
+ await styleTableCells(context.document, applied, args.changeMode);
2274
2261
  }
2275
- const styledCount = await styleTableCells(doc, appliedCells, args.changeMode);
2276
- if (styledCount > 0) {
2277
- executedOperations.push({
2278
- operationId: 'doc.format.apply',
2279
- rationale: `Styled ${styledCount} table cell(s).`,
2280
- });
2262
+ else {
2263
+ for (const cell of cells)
2264
+ await context.document.tables.setCellText({
2265
+ target: { kind: 'block', nodeType: 'table', nodeId: tableNodeId },
2266
+ rowIndex: cell.rowIndex,
2267
+ columnIndex: cell.columnIndex,
2268
+ text: cell.text,
2269
+ changeMode: args.changeMode,
2270
+ });
2271
+ const styled = cells.filter((cell) => cell.runs?.length || cell.marks);
2272
+ if (styled.length) {
2273
+ const identities = await doc.tables.getCells({ nodeId: tableNodeId });
2274
+ const resolved = styled.map((cell) => {
2275
+ const identity = identities.cells.find((item) => item.rowIndex === cell.rowIndex &&
2276
+ item.columnIndex <= cell.columnIndex &&
2277
+ item.columnIndex + item.colspan > cell.columnIndex);
2278
+ if (!identity?.firstParagraphNodeId)
2279
+ throw new Error('Required cell paragraph identity is unavailable.');
2280
+ return { ...cell, nodeId: identity.firstParagraphNodeId };
2281
+ });
2282
+ const styling = await applyItemStyling(context.document, resolved.map((cell) => cell.nodeId), resolved, args.changeMode);
2283
+ if (styling.skipped)
2284
+ throw new Error(`Requested cell styling failed: ${styling.skipped}`);
2285
+ }
2281
2286
  }
2282
2287
  }
2283
- const post = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
2284
- const verification = evaluateChecks(pre, post, [
2285
- { kind: 'revision-changed' },
2286
- { kind: 'block-count-delta', nodeType: 'table', delta: 1 },
2287
- ]);
2288
- return {
2289
- status: verification.every((v) => v.passed) ? 'ok' : 'failed',
2290
- intent: 'create_table',
2291
- preSnapshot: { revision: pre.revision, counts: pre.counts },
2292
- postSnapshot: { revision: post.revision, counts: post.counts },
2293
- selectedTargets: [],
2294
- executedOperations,
2295
- verification,
2296
- };
2288
+ const post = await context.finish();
2289
+ return context.receipt('create_table', await evaluateFactChecks(pre, post, checks));
2297
2290
  }
2298
- catch (err) {
2299
- return failedReceipt('create_table', err, pre);
2291
+ catch (error) {
2292
+ return context.failure('create_table', error);
2300
2293
  }
2301
2294
  }
2302
2295
  async function runCommentParagraphs(doc, args) {
@@ -2579,67 +2572,39 @@ async function runReplyToComment(doc, args) {
2579
2572
  }
2580
2573
  }
2581
2574
  async function runRewriteBlock(doc, args) {
2582
- const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
2575
+ const context = new ExecutionContext(doc, { evidence: args.evidence, selectors: [args.selector] });
2583
2576
  try {
2584
- const target = selectorToBlockTarget(args.selector, pre);
2585
- if (!target) {
2586
- return failedReceipt('rewrite_block', new Error('selector did not resolve to a body block'), pre);
2587
- }
2588
- const normalizedText = normalizeTitleLikeRewriteText(target.text, args.text);
2589
- // The engine rewrites the whole block as ONE uniform run — losing an
2590
- // intra-paragraph pattern (bold "(n)" lead-in), or smearing the first
2591
- // run's marks across the entire text. Capture the current run layout so
2592
- // it can be re-applied over the unchanged prefix/suffix afterward.
2593
- const preRuns = await captureBlockRuns(doc, target.nodeId, target.text);
2594
- const steps = [
2577
+ const pre = await context.start();
2578
+ const target = await pre.target(args.selector);
2579
+ if (!target)
2580
+ throw new Error('selector did not resolve to a body block');
2581
+ context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
2582
+ const text = normalizeTitleLikeRewriteText(target.text, args.text);
2583
+ const runs = await captureBlockRuns(doc, target.nodeId, target.text);
2584
+ await executeMutations(context.document, [
2595
2585
  {
2596
2586
  id: 'rewrite-block-1',
2597
2587
  op: 'text.rewrite',
2598
2588
  where: { by: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
2599
- args: {
2600
- replacement: { text: normalizedText },
2601
- style: preserveRewriteStyle(),
2602
- },
2589
+ args: { replacement: { text }, style: preserveRewriteStyle() },
2603
2590
  },
2604
- ];
2605
- const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
2606
- const preservation = await preserveRunPatternAfterRewrite(doc, target.nodeId, target.text, preRuns, normalizedText);
2607
- const post = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
2608
- const rewrittenBlock = findSnapshotTextByNodeId(post, target.nodeId);
2591
+ ], args.changeMode, pre.revision);
2592
+ await preserveRunPatternAfterRewrite(context.document, target.nodeId, target.text, runs, text);
2593
+ const post = await context.finish();
2594
+ const rewritten = await post.block(target.nodeId);
2609
2595
  const verification = [
2610
2596
  revisionVerification(pre.revision, post.revision, true),
2611
2597
  {
2612
- check: { kind: 'block-text-contains', nodeId: target.nodeId, text: normalizedText },
2613
- passed: !!rewrittenBlock && verifyRewrittenBlockText(rewrittenBlock.text, normalizedText, args.changeMode),
2598
+ check: { kind: 'block-text-contains', nodeId: target.nodeId, text },
2599
+ passed: !!rewritten && verifyRewrittenBlockText(rewritten.text, text, args.changeMode),
2614
2600
  },
2615
2601
  ];
2616
- const changed = pre.revision !== post.revision;
2617
- const rewritten = !!rewrittenBlock && verifyRewrittenBlockText(rewrittenBlock.text, args.text, args.changeMode);
2618
- return {
2619
- status: changed && rewritten ? 'ok' : 'failed',
2620
- intent: 'rewrite_block',
2621
- preSnapshot: { revision: pre.revision, counts: pre.counts },
2622
- postSnapshot: { revision: post.revision, counts: post.counts },
2623
- selectedTargets: [{ selector: args.selector, matched: [target.nodeId] }],
2624
- executedOperations: [
2625
- { operationId: 'doc.mutations.apply', result },
2626
- ...(preservation ? [{ operationId: 'doc.format.apply', result: preservation }] : []),
2627
- ],
2628
- verification,
2629
- errors: changed && rewritten
2630
- ? undefined
2631
- : [
2632
- {
2633
- code: 'ACTION_FAILED',
2634
- message: changed
2635
- ? 'rewrite_block did not produce the requested rewritten text for the selected block'
2636
- : 'rewrite_block produced no change for the selected block; keep the same target and provide a changed rewrite',
2637
- },
2638
- ],
2639
- };
2602
+ return context.receipt('rewrite_block', verification, verification.every((check) => check.passed)
2603
+ ? {}
2604
+ : { errors: [{ code: 'ACTION_FAILED', message: 'rewrite_block did not produce the requested changed text' }] });
2640
2605
  }
2641
- catch (err) {
2642
- return failedReceipt('rewrite_block', err, pre);
2606
+ catch (error) {
2607
+ return context.failure('rewrite_block', error);
2643
2608
  }
2644
2609
  }
2645
2610
  const RUN_BOOL_MARKS = ['bold', 'italic', 'underline', 'strike'];
@@ -3290,43 +3255,30 @@ async function runApplyStyle(doc, args) {
3290
3255
  * escape to superdoc_execute_code and apply the alignment untracked (no pPrChange).
3291
3256
  */
3292
3257
  async function runFormatParagraph(doc, args) {
3293
- const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
3258
+ const context = new ExecutionContext(doc, { evidence: args.evidence, selectors: [args.selector] });
3294
3259
  try {
3295
- const target = selectorToBlockTarget(args.selector, pre);
3296
- if (!target) {
3297
- return failedReceipt('format_paragraph', new Error('selector did not resolve to a unique body block'), pre);
3298
- }
3299
3260
  const alignment = typeof args.alignment === 'string' ? args.alignment.trim().toLowerCase() : '';
3300
- const normalizedAlignment = alignment === 'both' ? 'justify' : alignment;
3301
- if (!['left', 'center', 'right', 'justify'].includes(normalizedAlignment)) {
3302
- return failedReceipt('format_paragraph', new Error('format_paragraph requires alignment: left | center | right | justify'), pre);
3303
- }
3304
- const steps = [
3261
+ const normalized = alignment === 'both' ? 'justify' : alignment;
3262
+ if (!['left', 'center', 'right', 'justify'].includes(normalized))
3263
+ throw new Error('format_paragraph requires alignment: left | center | right | justify');
3264
+ const pre = await context.start();
3265
+ const target = await pre.target(args.selector);
3266
+ if (!target)
3267
+ throw new Error('selector did not resolve to a unique body block');
3268
+ context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
3269
+ await executeMutations(context.document, [
3305
3270
  {
3306
3271
  id: 'format-paragraph-1',
3307
3272
  op: 'format.apply',
3308
3273
  where: { by: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
3309
- args: { alignment: normalizedAlignment, scope: 'block' },
3274
+ args: { alignment: normalized, scope: 'block' },
3310
3275
  },
3311
- ];
3312
- const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
3313
- const post = await buildDocumentSnapshot(doc);
3314
- const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
3315
- return {
3316
- status: verification.every((v) => v.passed) ? 'ok' : 'failed',
3317
- intent: `format_paragraph: ${normalizedAlignment}`,
3318
- preSnapshot: { revision: pre.revision, counts: pre.counts },
3319
- postSnapshot: { revision: post.revision, counts: post.counts },
3320
- selectedTargets: [{ selector: args.selector, matched: [target.nodeId] }],
3321
- applied: { alignment: normalizedAlignment },
3322
- executedOperations: [
3323
- { operationId: 'doc.mutations.apply', result: compactOpResult(result), rationale: `align ${target.nodeId}` },
3324
- ],
3325
- verification,
3326
- };
3276
+ ], args.changeMode, pre.revision);
3277
+ const post = await context.finish();
3278
+ return context.receipt(`format_paragraph: ${normalized}`, await evaluateFactChecks(pre, post, [{ kind: 'revision-changed' }]), { applied: { alignment: normalized } });
3327
3279
  }
3328
- catch (err) {
3329
- return failedReceipt('format_paragraph', err, pre);
3280
+ catch (error) {
3281
+ return context.failure('format_paragraph', error);
3330
3282
  }
3331
3283
  }
3332
3284
  /**
@@ -5982,8 +5934,9 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
5982
5934
  const formatApplyFn = maybeMethod(doc, ['format', 'apply']);
5983
5935
  if (formatApplyFn) {
5984
5936
  const textLength = (created.textPreview ?? '').length;
5985
- const post = await buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
5986
- const full = post.blocks.find((b) => b.nodeId === created.nodeId)?.text ?? '';
5937
+ const full = createdText ||
5938
+ (await doc.blocks.list({ nodeIds: [created.nodeId], limit: 1, includeText: true })).blocks.find((block) => block.nodeId === created.nodeId)?.text ||
5939
+ '';
5987
5940
  await formatApplyFn({
5988
5941
  blockId: created.nodeId,
5989
5942
  start: 0,
@@ -6180,11 +6133,11 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
6180
6133
  }
6181
6134
  return Object.keys(applied).length > 0 ? { matchedSibling: reference.nodeId, ...applied } : null;
6182
6135
  }
6183
- async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args) {
6136
+ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, availableRows) {
6184
6137
  if (receipt.status !== 'ok')
6185
6138
  return receipt;
6186
6139
  try {
6187
- const postBlocks = await listBlockRows(doc);
6140
+ const postBlocks = availableRows ?? (await listBlockRows(doc));
6188
6141
  const preIds = new Set(preBlocks.map((b) => b.nodeId));
6189
6142
  const createdRows = postBlocks
6190
6143
  .map((b, i) => ({ row: b, index: i }))
@@ -6475,9 +6428,9 @@ async function dispatchSuperdocPerformAction(doc, args) {
6475
6428
  });
6476
6429
  }
6477
6430
  const headingLevel = asNumber(args.headingLevel);
6478
- const preRows = await listBlockRows(doc);
6479
- const receipt = await runInsertParagraphs(doc, {
6431
+ return runInsertParagraphs(doc, {
6480
6432
  action,
6433
+ evidence: args.evidence,
6481
6434
  texts,
6482
6435
  textSpecs,
6483
6436
  placement: parsePlacement(args.placement),
@@ -6485,8 +6438,7 @@ async function dispatchSuperdocPerformAction(doc, args) {
6485
6438
  headingLevel: headingLevel != null && Number.isInteger(headingLevel) && headingLevel >= 1 && headingLevel <= 6
6486
6439
  ? headingLevel
6487
6440
  : undefined,
6488
- });
6489
- return matchInsertedBlockFormatting(doc, preRows, receipt, args);
6441
+ }, args);
6490
6442
  }
6491
6443
  case 'insert_heading': {
6492
6444
  const text = asString(args.text);
@@ -6610,6 +6562,7 @@ async function dispatchSuperdocPerformAction(doc, args) {
6610
6562
  }
6611
6563
  return runCreateTable(doc, {
6612
6564
  action,
6565
+ evidence: args.evidence,
6613
6566
  rows,
6614
6567
  columns,
6615
6568
  cellTexts: parseCellTexts(args.cellTexts),
@@ -6633,16 +6586,41 @@ async function dispatchSuperdocPerformAction(doc, args) {
6633
6586
  }
6634
6587
  case 'add_comments': {
6635
6588
  const commentText = asString(args.commentText);
6636
- const selector = parseSelector(args.selector);
6637
- const selectors = Array.isArray(args.selectors)
6638
- ? args.selectors.map((s) => parseSelector(s)).filter((s) => Boolean(s))
6639
- : undefined;
6640
- if (!commentText || (!selector && !(selectors && selectors.length))) {
6589
+ if (!commentText) {
6641
6590
  throw new SuperDocCliError('add_comments requires "commentText" and a "selector" or non-empty "selectors" array', {
6642
6591
  code: 'INVALID_ARGUMENT',
6643
6592
  });
6644
6593
  }
6645
- return runAddComments(doc, { action, commentText, selector: selector ?? undefined, selectors });
6594
+ const hasSelector = args.selector != null;
6595
+ const hasSelectors = args.selectors != null;
6596
+ if (hasSelector && hasSelectors) {
6597
+ throw new SuperDocCliError('add_comments requires either "selector" or "selectors", not both', {
6598
+ code: 'INVALID_ARGUMENT',
6599
+ });
6600
+ }
6601
+ if (hasSelectors) {
6602
+ if (!Array.isArray(args.selectors) || args.selectors.length === 0) {
6603
+ throw new SuperDocCliError('add_comments requires "selectors" to be a non-empty array', {
6604
+ code: 'INVALID_ARGUMENT',
6605
+ });
6606
+ }
6607
+ // Parse the complete batch before mutation. Filtering an invalid entry
6608
+ // would make the receipt report success for only part of the request.
6609
+ const selectors = [];
6610
+ for (const [index, raw] of args.selectors.entries()) {
6611
+ const parsed = parseSelector(raw);
6612
+ if (!parsed) {
6613
+ throw new SuperDocCliError(`add_comments selectors[${index}] is not a valid selector; no comments were added`, { code: 'INVALID_ARGUMENT' });
6614
+ }
6615
+ selectors.push(parsed);
6616
+ }
6617
+ return runAddComments(doc, { action, commentText, selectors });
6618
+ }
6619
+ const selector = parseSelector(args.selector);
6620
+ if (!selector) {
6621
+ throw new SuperDocCliError('add_comments requires "selector" to be valid or "selectors" to be a non-empty array', { code: 'INVALID_ARGUMENT' });
6622
+ }
6623
+ return runAddComments(doc, { action, commentText, selector });
6646
6624
  }
6647
6625
  case 'resolve_comments':
6648
6626
  return runResolveComments(doc, {
@@ -6671,6 +6649,7 @@ async function dispatchSuperdocPerformAction(doc, args) {
6671
6649
  }
6672
6650
  return runRewriteBlock(doc, {
6673
6651
  action,
6652
+ evidence: args.evidence,
6674
6653
  text,
6675
6654
  selector,
6676
6655
  changeMode: parseChangeMode(args.changeMode),
@@ -6924,6 +6903,7 @@ async function dispatchSuperdocPerformAction(doc, args) {
6924
6903
  }
6925
6904
  return runFormatParagraph(doc, {
6926
6905
  action,
6906
+ evidence: args.evidence,
6927
6907
  selector,
6928
6908
  alignment: asString(args.alignment) ?? '',
6929
6909
  changeMode: parseChangeMode(args.changeMode),
@@ -146,6 +146,11 @@ const ACTION_ARG_SCHEMA = {
146
146
  },
147
147
  finds: { type: 'array', items: { type: 'string' } },
148
148
  caseSensitive: { type: 'boolean' },
149
+ evidence: {
150
+ type: 'string',
151
+ enum: ['required', 'full'],
152
+ description: 'Required evidence by default; full includes complete count summaries.',
153
+ },
149
154
  changeMode: { type: 'string', enum: ['direct', 'tracked'] },
150
155
  placement: { type: 'object', additionalProperties: true, properties: {} },
151
156
  selector: SELECTOR_SCHEMA,
@@ -390,6 +395,11 @@ const AGENT_TOOL_DEFINITIONS = [
390
395
  additionalProperties: false,
391
396
  required: ['plan'],
392
397
  properties: {
398
+ evidence: {
399
+ type: 'string',
400
+ enum: ['required', 'full'],
401
+ description: 'Required evidence by default; full includes complete count summaries.',
402
+ },
393
403
  plan: {
394
404
  type: 'object',
395
405
  additionalProperties: false,
@@ -428,6 +438,11 @@ const AGENT_TOOL_DEFINITIONS = [
428
438
  additionalProperties: false,
429
439
  required: ['checks'],
430
440
  properties: {
441
+ evidence: {
442
+ type: 'string',
443
+ enum: ['required', 'full'],
444
+ description: 'Required evidence by default; full includes complete count summaries.',
445
+ },
431
446
  checks: {
432
447
  type: 'array',
433
448
  minItems: 1,