@superdoc-dev/sdk 1.19.2 → 1.20.1

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 (103) hide show
  1. package/dist/action-primitives/doc-index.cjs +215 -0
  2. package/dist/action-primitives/doc-index.d.ts +83 -0
  3. package/dist/action-primitives/doc-index.d.ts.map +1 -0
  4. package/dist/action-primitives/doc-index.js +211 -0
  5. package/dist/action-primitives/engine.cjs +204 -0
  6. package/dist/action-primitives/engine.d.ts +71 -0
  7. package/dist/action-primitives/engine.d.ts.map +1 -0
  8. package/dist/action-primitives/engine.js +196 -0
  9. package/dist/action-primitives/receipt.cjs +39 -0
  10. package/dist/action-primitives/receipt.d.ts +49 -0
  11. package/dist/action-primitives/receipt.d.ts.map +1 -0
  12. package/dist/action-primitives/receipt.js +32 -0
  13. package/dist/action-primitives/resolve.cjs +252 -0
  14. package/dist/action-primitives/resolve.d.ts +56 -0
  15. package/dist/action-primitives/resolve.d.ts.map +1 -0
  16. package/dist/action-primitives/resolve.js +246 -0
  17. package/dist/action-primitives/session-cache.cjs +43 -0
  18. package/dist/action-primitives/session-cache.d.ts +19 -0
  19. package/dist/action-primitives/session-cache.d.ts.map +1 -0
  20. package/dist/action-primitives/session-cache.js +37 -0
  21. package/dist/action-primitives/tools/list-transform.cjs +661 -0
  22. package/dist/action-primitives/tools/list-transform.d.ts +98 -0
  23. package/dist/action-primitives/tools/list-transform.d.ts.map +1 -0
  24. package/dist/action-primitives/tools/list-transform.js +656 -0
  25. package/dist/action-primitives/tools/structure-insert.cjs +1343 -0
  26. package/dist/action-primitives/tools/structure-insert.d.ts +183 -0
  27. package/dist/action-primitives/tools/structure-insert.d.ts.map +1 -0
  28. package/dist/action-primitives/tools/structure-insert.js +1338 -0
  29. package/dist/action-primitives/tools/text-transform.cjs +669 -0
  30. package/dist/action-primitives/tools/text-transform.d.ts +64 -0
  31. package/dist/action-primitives/tools/text-transform.d.ts.map +1 -0
  32. package/dist/action-primitives/tools/text-transform.js +664 -0
  33. package/dist/action-primitives/types.d.ts +36 -0
  34. package/dist/action-primitives/types.d.ts.map +1 -0
  35. package/dist/action-primitives/types.js +15 -0
  36. package/dist/agent/actions.cjs +5381 -0
  37. package/dist/agent/actions.d.ts +404 -0
  38. package/dist/agent/actions.d.ts.map +1 -0
  39. package/dist/agent/actions.js +5373 -0
  40. package/dist/agent/catalog.cjs +483 -0
  41. package/dist/agent/catalog.d.ts +103 -0
  42. package/dist/agent/catalog.d.ts.map +1 -0
  43. package/dist/agent/catalog.js +471 -0
  44. package/dist/agent/doc-snapshot.cjs +663 -0
  45. package/dist/agent/doc-snapshot.d.ts +247 -0
  46. package/dist/agent/doc-snapshot.d.ts.map +1 -0
  47. package/dist/agent/doc-snapshot.js +657 -0
  48. package/dist/agent/index.d.ts +16 -0
  49. package/dist/agent/index.d.ts.map +1 -0
  50. package/dist/agent/index.js +15 -0
  51. package/dist/agent/ir.cjs +170 -0
  52. package/dist/agent/ir.d.ts +216 -0
  53. package/dist/agent/ir.d.ts.map +1 -0
  54. package/dist/agent/ir.js +181 -0
  55. package/dist/agent/operation-catalog.cjs +418 -0
  56. package/dist/agent/operation-catalog.d.ts +36 -0
  57. package/dist/agent/operation-catalog.d.ts.map +1 -0
  58. package/dist/agent/operation-catalog.js +446 -0
  59. package/dist/agent/runtime.cjs +501 -0
  60. package/dist/agent/runtime.d.ts +120 -0
  61. package/dist/agent/runtime.d.ts.map +1 -0
  62. package/dist/agent/runtime.js +493 -0
  63. package/dist/embedded-prompts.generated.cjs +13 -0
  64. package/dist/embedded-prompts.generated.d.ts +4 -0
  65. package/dist/embedded-prompts.generated.d.ts.map +1 -0
  66. package/dist/embedded-prompts.generated.js +9 -0
  67. package/dist/generated/client.cjs +18 -0
  68. package/dist/generated/client.d.ts +134 -0
  69. package/dist/generated/client.d.ts.map +1 -1
  70. package/dist/generated/client.js +18 -0
  71. package/dist/generated/contract.cjs +622 -3
  72. package/dist/generated/contract.d.ts.map +1 -1
  73. package/dist/generated/contract.js +622 -3
  74. package/dist/index.cjs +3 -7
  75. package/dist/index.d.ts +8 -6
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +2 -8
  78. package/dist/presets/core.cjs +454 -0
  79. package/dist/presets/core.d.ts +20 -0
  80. package/dist/presets/core.d.ts.map +1 -0
  81. package/dist/presets/core.js +447 -0
  82. package/dist/presets.cjs +55 -8
  83. package/dist/presets.d.ts +39 -8
  84. package/dist/presets.d.ts.map +1 -1
  85. package/dist/presets.js +53 -8
  86. package/dist/prompts/mcp-prompt.md +23 -0
  87. package/dist/prompts/system-prompt.md +108 -0
  88. package/dist/runtime/transport-common.cjs +8 -0
  89. package/dist/runtime/transport-common.d.ts.map +1 -1
  90. package/dist/runtime/transport-common.js +8 -0
  91. package/dist/tools.cjs +46 -11
  92. package/dist/tools.d.ts +55 -8
  93. package/dist/tools.d.ts.map +1 -1
  94. package/dist/tools.js +45 -13
  95. package/package.json +9 -8
  96. package/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  97. package/tools/__pycache__/intent_dispatch_generated.cpython-312.pyc +0 -0
  98. package/tools/catalog.json +14 -0
  99. package/tools/tools-policy.json +1 -1
  100. package/tools/tools.anthropic.json +14 -0
  101. package/tools/tools.generic.json +14 -0
  102. package/tools/tools.openai.json +14 -0
  103. package/tools/tools.vercel.json +14 -0
@@ -0,0 +1,1338 @@
1
+ import { buildWorkflowDocIndex } from '../doc-index.js';
2
+ import { runWorkflowEngine, workflowStepFailure, workflowStepSuccess, } from '../engine.js';
3
+ import { resolveWorkflowTargetFromUnknown, } from '../resolve.js';
4
+ const STRUCTURE_INSERT_ACTIONS = [
5
+ 'insert_toc',
6
+ 'insert_section_break',
7
+ 'insert_paragraph',
8
+ 'insert_paragraphs',
9
+ 'move_range',
10
+ ];
11
+ const DOCUMENT_PLACEMENTS = ['document_start', 'document_end'];
12
+ const RELATIVE_POSITIONS = ['before', 'after'];
13
+ const SECTION_BREAK_TYPES = ['continuous', 'nextPage', 'evenPage', 'oddPage'];
14
+ const CHANGE_MODES = ['direct', 'tracked'];
15
+ const DEFAULT_TOC_CONFIG = {
16
+ outlineLevels: { from: 1, to: 3 },
17
+ hyperlinks: true,
18
+ includePageNumbers: true,
19
+ rightAlignPageNumbers: true,
20
+ tabLeader: 'dot',
21
+ };
22
+ function isObjectRecord(value) {
23
+ return value != null && typeof value === 'object' && !Array.isArray(value);
24
+ }
25
+ function parseAction(raw) {
26
+ if (typeof raw !== 'string') {
27
+ return undefined;
28
+ }
29
+ return STRUCTURE_INSERT_ACTIONS.find((action) => action === raw);
30
+ }
31
+ function parseOptionalText(raw) {
32
+ if (raw == null) {
33
+ return undefined;
34
+ }
35
+ if (typeof raw !== 'string') {
36
+ return undefined;
37
+ }
38
+ const trimmed = raw.trim();
39
+ return trimmed.length === 0 ? undefined : trimmed;
40
+ }
41
+ function parseTexts(raw) {
42
+ if (!Array.isArray(raw)) {
43
+ return undefined;
44
+ }
45
+ const texts = raw.map((value) => (typeof value === 'string' ? value.trim() : '')).filter((value) => value.length > 0);
46
+ return texts.length === raw.length && texts.length > 0 ? texts : undefined;
47
+ }
48
+ function parseHeadingLevel(raw) {
49
+ if (raw == null) {
50
+ return undefined;
51
+ }
52
+ if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1 || raw > 6) {
53
+ return Number.NaN;
54
+ }
55
+ return raw;
56
+ }
57
+ function parseRequiredNodeId(raw) {
58
+ if (typeof raw !== 'string') {
59
+ return undefined;
60
+ }
61
+ const trimmed = raw.trim();
62
+ return trimmed.length === 0 ? undefined : trimmed;
63
+ }
64
+ function parseBreakType(raw) {
65
+ if (raw == null) {
66
+ return 'nextPage';
67
+ }
68
+ if (typeof raw !== 'string') {
69
+ return undefined;
70
+ }
71
+ return SECTION_BREAK_TYPES.find((value) => value === raw);
72
+ }
73
+ function parseChangeMode(raw) {
74
+ if (raw == null) {
75
+ return 'direct';
76
+ }
77
+ if (typeof raw !== 'string') {
78
+ return undefined;
79
+ }
80
+ return CHANGE_MODES.find((mode) => mode === raw);
81
+ }
82
+ function parseRelativePosition(raw) {
83
+ if (raw == null) {
84
+ return 'before';
85
+ }
86
+ if (typeof raw !== 'string') {
87
+ return undefined;
88
+ }
89
+ return RELATIVE_POSITIONS.find((position) => position === raw);
90
+ }
91
+ function toApiRelativeTarget(target) {
92
+ if (target.entity.kind === 'table') {
93
+ return {
94
+ kind: 'block',
95
+ nodeType: 'table',
96
+ nodeId: target.entity.nodeId,
97
+ };
98
+ }
99
+ if (target.entity.kind === 'listItem') {
100
+ return {
101
+ kind: 'block',
102
+ nodeType: 'listItem',
103
+ nodeId: target.entity.nodeId,
104
+ };
105
+ }
106
+ return {
107
+ kind: 'block',
108
+ nodeType: target.entity.nodeType,
109
+ nodeId: target.entity.nodeId,
110
+ };
111
+ }
112
+ function toApiPlacement(placement) {
113
+ if (placement.mode === 'document') {
114
+ return {
115
+ kind: placement.at === 'document_start' ? 'documentStart' : 'documentEnd',
116
+ };
117
+ }
118
+ return {
119
+ kind: placement.position,
120
+ target: toApiRelativeTarget(placement.target),
121
+ };
122
+ }
123
+ function summarizePlacement(placement) {
124
+ if (placement.mode === 'document') {
125
+ return {
126
+ mode: placement.mode,
127
+ at: placement.at,
128
+ source: placement.source,
129
+ };
130
+ }
131
+ return {
132
+ mode: placement.mode,
133
+ position: placement.position,
134
+ source: placement.source,
135
+ targetNodeId: placement.target.nodeId,
136
+ targetKind: placement.target.entity.kind,
137
+ };
138
+ }
139
+ function resolvePlacement(context, action) {
140
+ const rawPlacement = context.args.placement;
141
+ const defaultPlacement = {
142
+ mode: 'document',
143
+ at: action === 'insert_toc' ? 'document_start' : 'document_end',
144
+ source: 'default',
145
+ };
146
+ if (rawPlacement == null) {
147
+ return workflowStepSuccess(defaultPlacement);
148
+ }
149
+ if (typeof rawPlacement === 'string') {
150
+ const at = DOCUMENT_PLACEMENTS.find((value) => value === rawPlacement);
151
+ if (at == null) {
152
+ return workflowStepFailure({
153
+ status: 'failed',
154
+ phase: 'resolve',
155
+ code: 'WORKFLOW_STRUCTURE_INSERT_PLACEMENT_INVALID',
156
+ message: 'placement must be document_start, document_end, or an object with {at} or {position,target}.',
157
+ details: { received: rawPlacement },
158
+ });
159
+ }
160
+ return workflowStepSuccess({
161
+ mode: 'document',
162
+ at,
163
+ source: 'provided',
164
+ });
165
+ }
166
+ if (!isObjectRecord(rawPlacement)) {
167
+ return workflowStepFailure({
168
+ status: 'failed',
169
+ phase: 'resolve',
170
+ code: 'WORKFLOW_STRUCTURE_INSERT_PLACEMENT_INVALID',
171
+ message: 'placement must be an object when not using a document_start/document_end string.',
172
+ details: { receivedType: typeof rawPlacement },
173
+ });
174
+ }
175
+ const hasAt = rawPlacement.at != null;
176
+ const hasPosition = rawPlacement.position != null || rawPlacement.target != null;
177
+ if (hasAt && hasPosition) {
178
+ return workflowStepFailure({
179
+ status: 'failed',
180
+ phase: 'resolve',
181
+ code: 'WORKFLOW_STRUCTURE_INSERT_PLACEMENT_INVALID',
182
+ message: 'placement must specify either {at} or {position,target}, not both.',
183
+ });
184
+ }
185
+ if (hasAt) {
186
+ const at = DOCUMENT_PLACEMENTS.find((value) => value === rawPlacement.at);
187
+ if (at == null) {
188
+ return workflowStepFailure({
189
+ status: 'failed',
190
+ phase: 'resolve',
191
+ code: 'WORKFLOW_STRUCTURE_INSERT_PLACEMENT_INVALID',
192
+ message: 'placement.at must be document_start or document_end.',
193
+ details: { received: rawPlacement.at },
194
+ });
195
+ }
196
+ return workflowStepSuccess({
197
+ mode: 'document',
198
+ at,
199
+ source: 'provided',
200
+ });
201
+ }
202
+ const position = RELATIVE_POSITIONS.find((value) => value === rawPlacement.position);
203
+ if (position == null || rawPlacement.target == null) {
204
+ return workflowStepFailure({
205
+ status: 'failed',
206
+ phase: 'resolve',
207
+ code: 'WORKFLOW_STRUCTURE_INSERT_PLACEMENT_INVALID',
208
+ message: 'Relative placement requires {position,target} with position before or after.',
209
+ details: {
210
+ position: rawPlacement.position,
211
+ hasTarget: rawPlacement.target != null,
212
+ },
213
+ });
214
+ }
215
+ const resolved = resolveWorkflowTargetFromUnknown(context.index, rawPlacement.target);
216
+ if (!resolved.ok) {
217
+ return workflowStepFailure({
218
+ status: 'failed',
219
+ phase: 'resolve',
220
+ code: `WORKFLOW_${resolved.code}`,
221
+ message: resolved.message,
222
+ details: {
223
+ targetArgKey: 'placement.target',
224
+ ...resolved.details,
225
+ },
226
+ });
227
+ }
228
+ return workflowStepSuccess({
229
+ mode: 'relative',
230
+ position,
231
+ source: 'provided',
232
+ request: resolved.request,
233
+ target: resolved.target,
234
+ });
235
+ }
236
+ function resolveStep(context) {
237
+ const action = parseAction(context.args.action);
238
+ if (action == null) {
239
+ return workflowStepFailure({
240
+ status: 'failed',
241
+ phase: 'resolve',
242
+ code: 'WORKFLOW_STRUCTURE_INSERT_ACTION_INVALID',
243
+ message: 'superdoc_structure_insert requires action to be insert_toc, insert_section_break, insert_paragraph, insert_paragraphs, or move_range.',
244
+ });
245
+ }
246
+ if (action === 'move_range') {
247
+ // The caller (SDK move_range action) resolves fromText/toText/afterText/beforeText
248
+ // to nodeIds and a destination position, so the workflow only validates presence.
249
+ const startNodeId = parseRequiredNodeId(context.args.startNodeId);
250
+ const endNodeId = parseRequiredNodeId(context.args.endNodeId);
251
+ const destinationNodeId = parseRequiredNodeId(context.args.destinationNodeId);
252
+ const position = parseRelativePosition(context.args.position);
253
+ const changeMode = parseChangeMode(context.args.changeMode);
254
+ if (startNodeId == null || destinationNodeId == null) {
255
+ return workflowStepFailure({
256
+ status: 'failed',
257
+ phase: 'resolve',
258
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_NODE_REQUIRED',
259
+ message: 'move_range requires startNodeId and destinationNodeId.',
260
+ details: {
261
+ startNodeId: context.args.startNodeId,
262
+ destinationNodeId: context.args.destinationNodeId,
263
+ },
264
+ });
265
+ }
266
+ if (startNodeId === destinationNodeId) {
267
+ return workflowStepFailure({
268
+ status: 'failed',
269
+ phase: 'resolve',
270
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_NODE_INVALID',
271
+ message: 'move_range startNodeId and destinationNodeId must differ.',
272
+ });
273
+ }
274
+ if (position == null) {
275
+ return workflowStepFailure({
276
+ status: 'failed',
277
+ phase: 'resolve',
278
+ code: 'WORKFLOW_STRUCTURE_INSERT_POSITION_INVALID',
279
+ message: 'move_range position must be "before" or "after".',
280
+ details: { received: context.args.position },
281
+ });
282
+ }
283
+ if (changeMode == null) {
284
+ return workflowStepFailure({
285
+ status: 'failed',
286
+ phase: 'resolve',
287
+ code: 'WORKFLOW_STRUCTURE_INSERT_CHANGE_MODE_INVALID',
288
+ message: 'changeMode must be "direct" or "tracked".',
289
+ details: { received: context.args.changeMode },
290
+ });
291
+ }
292
+ return workflowStepSuccess({
293
+ action,
294
+ startNodeId,
295
+ endNodeId,
296
+ destinationNodeId,
297
+ position,
298
+ changeMode,
299
+ });
300
+ }
301
+ const placement = resolvePlacement(context, action);
302
+ if (!placement.ok) {
303
+ return placement;
304
+ }
305
+ if (action === 'insert_toc') {
306
+ if (context.args.breakType != null) {
307
+ return workflowStepFailure({
308
+ status: 'failed',
309
+ phase: 'resolve',
310
+ code: 'WORKFLOW_STRUCTURE_INSERT_ARG_UNSUPPORTED',
311
+ message: 'breakType is only supported for insert_section_break.',
312
+ });
313
+ }
314
+ return workflowStepSuccess({
315
+ action,
316
+ placement: placement.value,
317
+ title: parseOptionalText(context.args.title),
318
+ });
319
+ }
320
+ if (action === 'insert_paragraph') {
321
+ if (context.args.breakType != null || context.args.title != null) {
322
+ return workflowStepFailure({
323
+ status: 'failed',
324
+ phase: 'resolve',
325
+ code: 'WORKFLOW_STRUCTURE_INSERT_ARG_UNSUPPORTED',
326
+ message: 'title and breakType are not supported for insert_paragraph.',
327
+ });
328
+ }
329
+ const text = parseOptionalText(context.args.text);
330
+ if (text == null) {
331
+ return workflowStepFailure({
332
+ status: 'failed',
333
+ phase: 'resolve',
334
+ code: 'WORKFLOW_STRUCTURE_INSERT_TEXT_REQUIRED',
335
+ message: 'insert_paragraph requires non-empty text.',
336
+ });
337
+ }
338
+ const changeMode = parseChangeMode(context.args.changeMode);
339
+ if (changeMode == null) {
340
+ return workflowStepFailure({
341
+ status: 'failed',
342
+ phase: 'resolve',
343
+ code: 'WORKFLOW_STRUCTURE_INSERT_CHANGE_MODE_INVALID',
344
+ message: 'changeMode must be "direct" or "tracked".',
345
+ details: { received: context.args.changeMode },
346
+ });
347
+ }
348
+ return workflowStepSuccess({
349
+ action,
350
+ placement: placement.value,
351
+ text,
352
+ changeMode,
353
+ });
354
+ }
355
+ if (action === 'insert_paragraphs') {
356
+ if (context.args.breakType != null || context.args.title != null || context.args.text != null) {
357
+ return workflowStepFailure({
358
+ status: 'failed',
359
+ phase: 'resolve',
360
+ code: 'WORKFLOW_STRUCTURE_INSERT_ARG_UNSUPPORTED',
361
+ message: 'text, title, and breakType are not supported for insert_paragraphs; use texts.',
362
+ });
363
+ }
364
+ const texts = parseTexts(context.args.texts);
365
+ if (texts == null) {
366
+ return workflowStepFailure({
367
+ status: 'failed',
368
+ phase: 'resolve',
369
+ code: 'WORKFLOW_STRUCTURE_INSERT_TEXTS_REQUIRED',
370
+ message: 'insert_paragraphs requires a non-empty texts array of non-empty paragraph strings.',
371
+ });
372
+ }
373
+ const headingLevel = parseHeadingLevel(context.args.headingLevel);
374
+ if (Number.isNaN(headingLevel)) {
375
+ return workflowStepFailure({
376
+ status: 'failed',
377
+ phase: 'resolve',
378
+ code: 'WORKFLOW_STRUCTURE_INSERT_HEADING_LEVEL_INVALID',
379
+ message: 'headingLevel must be an integer from 1 to 6 when provided.',
380
+ details: { received: context.args.headingLevel },
381
+ });
382
+ }
383
+ const changeMode = parseChangeMode(context.args.changeMode);
384
+ if (changeMode == null) {
385
+ return workflowStepFailure({
386
+ status: 'failed',
387
+ phase: 'resolve',
388
+ code: 'WORKFLOW_STRUCTURE_INSERT_CHANGE_MODE_INVALID',
389
+ message: 'changeMode must be "direct" or "tracked".',
390
+ details: { received: context.args.changeMode },
391
+ });
392
+ }
393
+ return workflowStepSuccess({
394
+ action,
395
+ placement: placement.value,
396
+ texts,
397
+ headingLevel,
398
+ changeMode,
399
+ });
400
+ }
401
+ if (context.args.title != null) {
402
+ return workflowStepFailure({
403
+ status: 'failed',
404
+ phase: 'resolve',
405
+ code: 'WORKFLOW_STRUCTURE_INSERT_ARG_UNSUPPORTED',
406
+ message: 'title is only supported for insert_toc.',
407
+ });
408
+ }
409
+ const breakType = parseBreakType(context.args.breakType);
410
+ if (breakType == null) {
411
+ return workflowStepFailure({
412
+ status: 'failed',
413
+ phase: 'resolve',
414
+ code: 'WORKFLOW_STRUCTURE_INSERT_BREAK_TYPE_INVALID',
415
+ message: 'breakType must be one of continuous, nextPage, evenPage, or oddPage.',
416
+ details: { received: context.args.breakType },
417
+ });
418
+ }
419
+ return workflowStepSuccess({
420
+ action,
421
+ placement: placement.value,
422
+ breakType,
423
+ });
424
+ }
425
+ function planStep(_context, resolved) {
426
+ if (resolved.action === 'move_range') {
427
+ return workflowStepSuccess(resolved);
428
+ }
429
+ const at = toApiPlacement(resolved.placement);
430
+ if (resolved.action === 'insert_toc') {
431
+ return workflowStepSuccess({
432
+ action: resolved.action,
433
+ placement: resolved.placement,
434
+ title: resolved.title,
435
+ titleParagraphParams: resolved.title == null
436
+ ? undefined
437
+ : {
438
+ text: resolved.title,
439
+ at,
440
+ },
441
+ tocParams: {
442
+ ...(resolved.title == null ? { at } : {}),
443
+ config: DEFAULT_TOC_CONFIG,
444
+ },
445
+ });
446
+ }
447
+ if (resolved.action === 'insert_paragraph') {
448
+ return workflowStepSuccess({
449
+ action: resolved.action,
450
+ placement: resolved.placement,
451
+ text: resolved.text,
452
+ changeMode: resolved.changeMode,
453
+ paragraphParams: {
454
+ text: resolved.text,
455
+ changeMode: resolved.changeMode,
456
+ at,
457
+ },
458
+ });
459
+ }
460
+ if (resolved.action === 'insert_paragraphs') {
461
+ return workflowStepSuccess({
462
+ action: resolved.action,
463
+ placement: resolved.placement,
464
+ texts: resolved.texts,
465
+ headingLevel: resolved.headingLevel,
466
+ changeMode: resolved.changeMode,
467
+ firstParagraphParams: {
468
+ text: resolved.texts[0] ?? '',
469
+ changeMode: resolved.changeMode,
470
+ at,
471
+ },
472
+ firstHeadingParams: resolved.headingLevel == null
473
+ ? undefined
474
+ : {
475
+ text: resolved.texts[0] ?? '',
476
+ level: resolved.headingLevel,
477
+ changeMode: resolved.changeMode,
478
+ at,
479
+ },
480
+ });
481
+ }
482
+ return workflowStepSuccess({
483
+ action: resolved.action,
484
+ placement: resolved.placement,
485
+ breakType: resolved.breakType,
486
+ sectionBreakParams: {
487
+ at,
488
+ breakType: resolved.breakType,
489
+ },
490
+ });
491
+ }
492
+ function textOfListedBlock(block) {
493
+ if (block == null) {
494
+ return '';
495
+ }
496
+ const text = typeof block.text === 'string' ? block.text : typeof block.textPreview === 'string' ? block.textPreview : '';
497
+ return text.trim();
498
+ }
499
+ function normalizeSectionHeadingText(value) {
500
+ return value.replace(/\s+/g, ' ').trim();
501
+ }
502
+ /**
503
+ * Broadened "section boundary" detector used by move_range's visual-section
504
+ * logic. A real contract/Will rarely uses Word heading NODES: its "sections"
505
+ * (PREAMBLE, SCHEDULE A, RECITALS…) are short paragraphs STYLED to look like
506
+ * titles — ALL-CAPS, bold, or carrying a heading/title style. We treat both as
507
+ * section boundaries so the range auto-extends across a visual section and so a
508
+ * destination "after SCHEDULE A" lands past that whole visual section.
509
+ *
510
+ * The length/word/punctuation guards keep body sentences (which can inherit a
511
+ * heading style via numbering) from being mistaken for a section title.
512
+ */
513
+ function isHeadingLikeBlock(block) {
514
+ const text = normalizeSectionHeadingText(textOfListedBlock(block));
515
+ if (text.length === 0 || text.length > 80) {
516
+ return false;
517
+ }
518
+ const words = text.split(/\s+/).filter(Boolean);
519
+ if (words.length > 8) {
520
+ return false;
521
+ }
522
+ // Sentence-terminated text is body content, not a movable section title —
523
+ // this also filters long clause bodies that surface as Heading3 via inherited
524
+ // numbering styles.
525
+ if (/[.:;!?]$/.test(text)) {
526
+ return false;
527
+ }
528
+ // 1) A real heading node is always a boundary.
529
+ if (block.nodeType === 'heading') {
530
+ return true;
531
+ }
532
+ // Only paragraphs can masquerade as styled titles; skip tables, list items, images…
533
+ if (block.nodeType !== 'paragraph') {
534
+ return false;
535
+ }
536
+ // 2) A heading/title/subtitle paragraph style.
537
+ const styleId = typeof block.styleId === 'string' ? block.styleId.toLowerCase() : '';
538
+ if (/heading|title|subtitle/.test(styleId)) {
539
+ return true;
540
+ }
541
+ // 3) A short bold line (e.g. a bold clause title).
542
+ if (block.bold === true) {
543
+ return true;
544
+ }
545
+ // 4) A short ALL-CAPS line (e.g. "PREAMBLE", "SCHEDULE A").
546
+ const letters = text.replace(/[^A-Za-z]/g, '');
547
+ if (letters.length >= 2 && text === text.toUpperCase() && /[A-Z]/.test(text)) {
548
+ return true;
549
+ }
550
+ return false;
551
+ }
552
+ async function listAllBlocksForStructureMove(documentHandle, invokeOptions) {
553
+ const blocks = [];
554
+ const pageLimit = 250;
555
+ let offset = 0;
556
+ for (;;) {
557
+ const page = await documentHandle.blocks.list({ offset, limit: pageLimit, includeText: true }, invokeOptions);
558
+ blocks.push(...page.blocks);
559
+ if (blocks.length >= page.total || page.blocks.length === 0) {
560
+ break;
561
+ }
562
+ offset += page.blocks.length;
563
+ }
564
+ return blocks;
565
+ }
566
+ function findBlockIndexByNodeId(blocks, nodeId) {
567
+ return blocks.findIndex((block) => block.nodeId === nodeId);
568
+ }
569
+ /**
570
+ * Given the index of a section-opening block, return the index of the LAST
571
+ * block belonging to that visual section — i.e. the block just before the next
572
+ * heading-like block (`nextHeadingIndex - 1`), or the end of the document when
573
+ * no further section boundary exists. This is the same next-heading detection
574
+ * the section move has always used, generalized to visual (styled-paragraph) sections.
575
+ *
576
+ * When the opening block carries a heading level, only boundaries at the same
577
+ * or a higher (shallower) level end the section, so a sub-heading does not cut
578
+ * the section short. Styled-title paragraphs (no heading level) always act as
579
+ * boundaries.
580
+ */
581
+ function computeVisualSectionEndIndex(blocks, startIndex) {
582
+ const startBlock = blocks[startIndex];
583
+ const sourceLevel = typeof startBlock?.headingLevel === 'number' ? startBlock.headingLevel : 9;
584
+ const relativeNextIndex = blocks
585
+ .slice(startIndex + 1)
586
+ .findIndex((block) => isHeadingLikeBlock(block) && (typeof block.headingLevel !== 'number' || block.headingLevel <= sourceLevel));
587
+ if (relativeNextIndex < 0) {
588
+ return blocks.length - 1;
589
+ }
590
+ // absolute next-boundary index = startIndex + 1 + relativeNextIndex; section end is the block before it.
591
+ return startIndex + relativeNextIndex;
592
+ }
593
+ function toListedBlockTarget(block) {
594
+ return {
595
+ kind: 'block',
596
+ nodeType: block.nodeType,
597
+ nodeId: block.nodeId,
598
+ };
599
+ }
600
+ function toCreatePlacementFromListedBlock(block, position) {
601
+ return {
602
+ kind: position,
603
+ target: toListedBlockTarget(block),
604
+ };
605
+ }
606
+ function headingLevelForCopiedBlock(block) {
607
+ return typeof block.headingLevel === 'number' &&
608
+ Number.isInteger(block.headingLevel) &&
609
+ block.headingLevel >= 1 &&
610
+ block.headingLevel <= 6
611
+ ? block.headingLevel
612
+ : 3;
613
+ }
614
+ function withChangeModeOption(invokeOptions, changeMode) {
615
+ return { ...(invokeOptions ?? {}), changeMode };
616
+ }
617
+ async function createCopiedSectionBlock(input) {
618
+ const text = textOfListedBlock(input.block);
619
+ if (input.block.nodeType === 'heading') {
620
+ const result = await input.context.documentHandle.create.heading({
621
+ text,
622
+ level: headingLevelForCopiedBlock(input.block),
623
+ at: input.at,
624
+ }, withChangeModeOption(input.context.invokeOptions, input.changeMode));
625
+ return {
626
+ nodeId: result.heading.nodeId,
627
+ nodeType: 'heading',
628
+ };
629
+ }
630
+ const result = await input.context.documentHandle.create.paragraph({
631
+ text,
632
+ at: input.at,
633
+ }, withChangeModeOption(input.context.invokeOptions, input.changeMode));
634
+ return {
635
+ nodeId: result.paragraph.nodeId,
636
+ nodeType: 'paragraph',
637
+ };
638
+ }
639
+ async function executeMoveRangeStep(context, plan) {
640
+ const beforeRevision = context.info.revision;
641
+ const blocks = await listAllBlocksForStructureMove(context.documentHandle, context.invokeOptions);
642
+ const startIndex = findBlockIndexByNodeId(blocks, plan.startNodeId);
643
+ if (startIndex < 0) {
644
+ return workflowStepFailure({
645
+ status: 'failed',
646
+ phase: 'execute',
647
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_START_NOT_FOUND',
648
+ message: 'move_range could not find the range start block (startNodeId) in the current document.',
649
+ details: { startNodeId: plan.startNodeId },
650
+ });
651
+ }
652
+ // Resolve the range end: an explicit endNodeId, else auto-extend across the
653
+ // visual section (up to but excluding the next heading-like block).
654
+ let endIndex;
655
+ if (plan.endNodeId != null) {
656
+ const explicitEnd = findBlockIndexByNodeId(blocks, plan.endNodeId);
657
+ if (explicitEnd < 0) {
658
+ return workflowStepFailure({
659
+ status: 'failed',
660
+ phase: 'execute',
661
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_END_NOT_FOUND',
662
+ message: 'move_range could not find the range end block (endNodeId) in the current document.',
663
+ details: { endNodeId: plan.endNodeId },
664
+ });
665
+ }
666
+ if (explicitEnd < startIndex) {
667
+ return workflowStepFailure({
668
+ status: 'failed',
669
+ phase: 'execute',
670
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_REVERSED',
671
+ message: 'move_range resolved toText before fromText. Nothing was changed. Use fromText from the first block and toText from the last block of the range.',
672
+ details: { startNodeId: plan.startNodeId, endNodeId: plan.endNodeId },
673
+ });
674
+ }
675
+ endIndex = explicitEnd;
676
+ }
677
+ else {
678
+ endIndex = computeVisualSectionEndIndex(blocks, startIndex);
679
+ }
680
+ const resolvedEndNodeId = blocks[endIndex]?.nodeId ?? plan.startNodeId;
681
+ // move_range recreates blocks as plain paragraph/heading text — a table,
682
+ // list, or image in the range would be silently flattened to its text
683
+ // preview and the original destroyed. Refuse instead of losing content.
684
+ // (Structural relocation that preserves node subtrees is follow-up work.)
685
+ const unsupportedBlocks = blocks
686
+ .slice(startIndex, endIndex + 1)
687
+ .filter((block) => block.nodeType !== 'paragraph' && block.nodeType !== 'heading');
688
+ if (unsupportedBlocks.length > 0) {
689
+ const kinds = [...new Set(unsupportedBlocks.map((block) => block.nodeType))].join(', ');
690
+ return workflowStepFailure({
691
+ status: 'failed',
692
+ phase: 'execute',
693
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_UNSUPPORTED_CONTENT',
694
+ message: `move_range only moves plain paragraph/heading text, but the resolved range contains: ${kinds}. ` +
695
+ 'Nothing was changed. Narrow the range to exclude those blocks (tighten fromText/toText or pass an explicit endNodeId), or move the surrounding paragraphs in smaller ranges around them.',
696
+ details: {
697
+ startNodeId: plan.startNodeId,
698
+ endNodeId: resolvedEndNodeId,
699
+ unsupported: unsupportedBlocks.map((block) => ({ nodeId: block.nodeId, nodeType: block.nodeType })),
700
+ },
701
+ });
702
+ }
703
+ const rangeBlocks = blocks.slice(startIndex, endIndex + 1).filter((block) => textOfListedBlock(block).length > 0);
704
+ if (rangeBlocks.length === 0) {
705
+ return workflowStepFailure({
706
+ status: 'failed',
707
+ phase: 'execute',
708
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_EMPTY',
709
+ message: 'move_range resolved a range with no movable (non-empty) blocks.',
710
+ details: { startNodeId: plan.startNodeId, endNodeId: resolvedEndNodeId },
711
+ });
712
+ }
713
+ if (plan.changeMode === 'tracked') {
714
+ return workflowStepFailure({
715
+ status: 'failed',
716
+ phase: 'execute',
717
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_TRACKED_UNSUPPORTED',
718
+ message: 'move_range is direct-only today. Tracked mode would require a tracked block-range move. Nothing was changed. Use move_text for tracked text-span moves or run move_range without changeMode.',
719
+ details: { startNodeId: plan.startNodeId, endNodeId: resolvedEndNodeId },
720
+ });
721
+ }
722
+ const destinationIndex = findBlockIndexByNodeId(blocks, plan.destinationNodeId);
723
+ if (destinationIndex < 0) {
724
+ return workflowStepFailure({
725
+ status: 'failed',
726
+ phase: 'execute',
727
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_DESTINATION_NOT_FOUND',
728
+ message: 'move_range could not find the destination anchor block in the current document.',
729
+ details: { destinationNodeId: plan.destinationNodeId },
730
+ });
731
+ }
732
+ if (destinationIndex >= startIndex && destinationIndex <= endIndex) {
733
+ return workflowStepFailure({
734
+ status: 'failed',
735
+ phase: 'execute',
736
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_DESTINATION_INSIDE',
737
+ message: 'move_range destination falls inside the range being moved.',
738
+ details: {
739
+ destinationNodeId: plan.destinationNodeId,
740
+ startNodeId: plan.startNodeId,
741
+ endNodeId: resolvedEndNodeId,
742
+ },
743
+ });
744
+ }
745
+ // "after a section": if the destination anchor is itself heading-like and the
746
+ // caller asked to land AFTER it, place the range after that anchor's WHOLE
747
+ // visual section (its section end) — so "afterText: SCHEDULE A" lands after
748
+ // the entire SCHEDULE A section, not just its title line.
749
+ const destinationBlock = blocks[destinationIndex];
750
+ let anchorIndex = destinationIndex;
751
+ let destinationExtendedToSectionEnd = false;
752
+ if (plan.position === 'after' && isHeadingLikeBlock(destinationBlock)) {
753
+ anchorIndex = computeVisualSectionEndIndex(blocks, destinationIndex);
754
+ destinationExtendedToSectionEnd = anchorIndex !== destinationIndex;
755
+ }
756
+ // The extended anchor must not reach into the source range.
757
+ if (anchorIndex >= startIndex && anchorIndex <= endIndex) {
758
+ return workflowStepFailure({
759
+ status: 'failed',
760
+ phase: 'execute',
761
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_DESTINATION_INSIDE',
762
+ message: 'move_range destination section overlaps the range being moved.',
763
+ details: {
764
+ destinationNodeId: plan.destinationNodeId,
765
+ startNodeId: plan.startNodeId,
766
+ endNodeId: resolvedEndNodeId,
767
+ },
768
+ });
769
+ }
770
+ const anchorBlock = blocks[anchorIndex] ?? destinationBlock;
771
+ const insertedBlockNodeIds = [];
772
+ // First copy lands relative to the anchor with the requested position; every
773
+ // subsequent copy lands right after the previous one, preserving range order.
774
+ let insertionAt = toCreatePlacementFromListedBlock(anchorBlock, plan.position);
775
+ let previousInserted;
776
+ for (const block of rangeBlocks) {
777
+ if (previousInserted != null) {
778
+ insertionAt = {
779
+ kind: 'after',
780
+ target: {
781
+ kind: 'block',
782
+ nodeType: previousInserted.nodeType,
783
+ nodeId: previousInserted.nodeId,
784
+ },
785
+ };
786
+ }
787
+ previousInserted = await createCopiedSectionBlock({ context, block, at: insertionAt, changeMode: plan.changeMode });
788
+ insertedBlockNodeIds.push(previousInserted.nodeId);
789
+ }
790
+ const sourceStart = blocks[startIndex];
791
+ const sourceEnd = blocks[endIndex];
792
+ if (sourceStart == null || sourceEnd == null) {
793
+ return workflowStepFailure({
794
+ status: 'failed',
795
+ phase: 'execute',
796
+ code: 'WORKFLOW_STRUCTURE_INSERT_RANGE_NOT_FOUND',
797
+ message: 'move_range lost the source range while preparing deletion.',
798
+ });
799
+ }
800
+ // Delete the ORIGINAL source range by nodeId. The freshly-created copies have
801
+ // new nodeIds, so the delete cannot touch them (the copy is preserved).
802
+ const deleteResult = await context.documentHandle.blocks.deleteRange({
803
+ start: toListedBlockTarget(sourceStart),
804
+ end: toListedBlockTarget(sourceEnd),
805
+ force: true,
806
+ }, withChangeModeOption(context.invokeOptions, plan.changeMode));
807
+ const afterInfo = await context.documentHandle.info({}, context.invokeOptions);
808
+ return workflowStepSuccess({
809
+ action: plan.action,
810
+ revision: {
811
+ before: beforeRevision,
812
+ after: afterInfo.revision,
813
+ unchanged: beforeRevision === afterInfo.revision,
814
+ },
815
+ position: plan.position,
816
+ changeMode: plan.changeMode,
817
+ startNodeId: plan.startNodeId,
818
+ endNodeId: resolvedEndNodeId,
819
+ destinationNodeId: plan.destinationNodeId,
820
+ anchorNodeId: anchorBlock.nodeId,
821
+ destinationExtendedToSectionEnd,
822
+ movedBlockCount: rangeBlocks.length,
823
+ movedTexts: rangeBlocks.map((block) => textOfListedBlock(block)),
824
+ insertedBlockNodeIds,
825
+ deletedCount: deleteResult.deletedCount,
826
+ });
827
+ }
828
+ async function executeStep(context, _resolved, plan) {
829
+ const beforeRevision = context.info.revision;
830
+ if (plan.action === 'move_range') {
831
+ return executeMoveRangeStep(context, plan);
832
+ }
833
+ if (plan.action === 'insert_toc') {
834
+ let titleNodeId;
835
+ if (plan.titleParagraphParams != null) {
836
+ const titleResult = await context.documentHandle.create.paragraph(plan.titleParagraphParams, context.invokeOptions);
837
+ titleNodeId = titleResult.paragraph.nodeId;
838
+ }
839
+ const tocParams = titleNodeId == null
840
+ ? plan.tocParams
841
+ : {
842
+ ...plan.tocParams,
843
+ at: {
844
+ kind: 'after',
845
+ target: {
846
+ kind: 'block',
847
+ nodeType: 'paragraph',
848
+ nodeId: titleNodeId,
849
+ },
850
+ },
851
+ };
852
+ const tocResult = await context.documentHandle.create.tableOfContents(tocParams, context.invokeOptions);
853
+ const afterInfo = await context.documentHandle.info({}, context.invokeOptions);
854
+ return workflowStepSuccess({
855
+ action: plan.action,
856
+ placement: summarizePlacement(plan.placement),
857
+ revision: {
858
+ before: beforeRevision,
859
+ after: afterInfo.revision,
860
+ unchanged: beforeRevision === afterInfo.revision,
861
+ },
862
+ tocNodeId: tocResult.toc.nodeId,
863
+ title: plan.title,
864
+ titleNodeId,
865
+ });
866
+ }
867
+ if (plan.action === 'insert_paragraph') {
868
+ const paragraphResult = await context.documentHandle.create.paragraph(plan.paragraphParams, context.invokeOptions);
869
+ const afterInfo = await context.documentHandle.info({}, context.invokeOptions);
870
+ return workflowStepSuccess({
871
+ action: plan.action,
872
+ placement: summarizePlacement(plan.placement),
873
+ revision: {
874
+ before: beforeRevision,
875
+ after: afterInfo.revision,
876
+ unchanged: beforeRevision === afterInfo.revision,
877
+ },
878
+ text: plan.text,
879
+ changeMode: plan.changeMode,
880
+ paragraphNodeId: paragraphResult.paragraph.nodeId,
881
+ });
882
+ }
883
+ if (plan.action === 'insert_paragraphs') {
884
+ const paragraphNodeIds = [];
885
+ let previousNodeType = 'paragraph';
886
+ if (plan.firstHeadingParams != null) {
887
+ const firstResult = await context.documentHandle.create.heading(plan.firstHeadingParams, context.invokeOptions);
888
+ paragraphNodeIds.push(firstResult.heading.nodeId);
889
+ previousNodeType = 'heading';
890
+ }
891
+ else {
892
+ const firstResult = await context.documentHandle.create.paragraph(plan.firstParagraphParams, context.invokeOptions);
893
+ paragraphNodeIds.push(firstResult.paragraph.nodeId);
894
+ }
895
+ for (const text of plan.texts.slice(1)) {
896
+ const previousNodeId = paragraphNodeIds[paragraphNodeIds.length - 1];
897
+ if (previousNodeId == null)
898
+ break;
899
+ const result = await context.documentHandle.create.paragraph({
900
+ text,
901
+ changeMode: plan.changeMode,
902
+ at: {
903
+ kind: 'after',
904
+ target: {
905
+ kind: 'block',
906
+ nodeType: previousNodeType,
907
+ nodeId: previousNodeId,
908
+ },
909
+ },
910
+ }, context.invokeOptions);
911
+ paragraphNodeIds.push(result.paragraph.nodeId);
912
+ previousNodeType = 'paragraph';
913
+ }
914
+ const afterInfo = await context.documentHandle.info({}, context.invokeOptions);
915
+ return workflowStepSuccess({
916
+ action: plan.action,
917
+ placement: summarizePlacement(plan.placement),
918
+ revision: {
919
+ before: beforeRevision,
920
+ after: afterInfo.revision,
921
+ unchanged: beforeRevision === afterInfo.revision,
922
+ },
923
+ texts: plan.texts,
924
+ headingLevel: plan.headingLevel,
925
+ changeMode: plan.changeMode,
926
+ paragraphNodeIds,
927
+ });
928
+ }
929
+ let sectionResult;
930
+ try {
931
+ sectionResult = await context.documentHandle.create.sectionBreak(plan.sectionBreakParams, context.invokeOptions);
932
+ }
933
+ catch {
934
+ // Some runtimes cannot materialize section breaks yet. Treat this as a
935
+ // non-fatal planning marker so unrelated multi-step workflows can proceed.
936
+ return workflowStepSuccess({
937
+ action: plan.action,
938
+ placement: summarizePlacement(plan.placement),
939
+ revision: {
940
+ before: beforeRevision,
941
+ after: beforeRevision,
942
+ unchanged: true,
943
+ },
944
+ breakType: plan.breakType,
945
+ sectionId: 'section-break-fallback',
946
+ });
947
+ }
948
+ const sectionId = isObjectRecord(sectionResult.section) && typeof sectionResult.section.sectionId === 'string'
949
+ ? sectionResult.section.sectionId
950
+ : undefined;
951
+ const afterInfo = await context.documentHandle.info({}, context.invokeOptions);
952
+ if (sectionId == null) {
953
+ return workflowStepSuccess({
954
+ action: plan.action,
955
+ placement: summarizePlacement(plan.placement),
956
+ revision: {
957
+ before: beforeRevision,
958
+ after: afterInfo.revision,
959
+ unchanged: beforeRevision === afterInfo.revision,
960
+ },
961
+ breakType: plan.breakType,
962
+ sectionId: beforeRevision === afterInfo.revision ? 'section-break-fallback' : 'section-break-created',
963
+ breakParagraphNodeId: sectionResult.breakParagraph?.nodeId,
964
+ });
965
+ }
966
+ return workflowStepSuccess({
967
+ action: plan.action,
968
+ placement: summarizePlacement(plan.placement),
969
+ revision: {
970
+ before: beforeRevision,
971
+ after: afterInfo.revision,
972
+ unchanged: beforeRevision === afterInfo.revision,
973
+ },
974
+ breakType: plan.breakType,
975
+ sectionId,
976
+ breakParagraphNodeId: sectionResult.breakParagraph?.nodeId,
977
+ });
978
+ }
979
+ function findBlockOrdinalByNodeId(index, nodeId) {
980
+ if (nodeId == null) {
981
+ return undefined;
982
+ }
983
+ return index.blocks.find((block) => block.nodeId === nodeId)?.ordinal;
984
+ }
985
+ function findTargetBlockOrdinal(index, target) {
986
+ if (target.entity.kind === 'table') {
987
+ return index.tables.find((table) => table.nodeId === target.entity.nodeId)?.blockOrdinal;
988
+ }
989
+ if (target.entity.kind === 'listItem') {
990
+ return undefined;
991
+ }
992
+ return index.blocks.find((block) => block.nodeId === target.entity.nodeId)?.ordinal;
993
+ }
994
+ function verifyTocPlacement(input) {
995
+ const tocOrdinal = findBlockOrdinalByNodeId(input.postIndex, input.tocNodeId);
996
+ const titleOrdinal = findBlockOrdinalByNodeId(input.postIndex, input.titleNodeId);
997
+ if (tocOrdinal == null) {
998
+ return {
999
+ placementVerified: true,
1000
+ placementSatisfied: false,
1001
+ tocOrdinal,
1002
+ titleOrdinal,
1003
+ };
1004
+ }
1005
+ if (input.placement.mode === 'document') {
1006
+ const blockCount = input.postIndex.blocks.length;
1007
+ const placementSatisfied = input.placement.at === 'document_start'
1008
+ ? input.titleNodeId == null
1009
+ ? tocOrdinal === 0
1010
+ : titleOrdinal === 0 && tocOrdinal === 1
1011
+ : input.titleNodeId == null
1012
+ ? tocOrdinal === blockCount - 1
1013
+ : titleOrdinal === blockCount - 2 && tocOrdinal === blockCount - 1;
1014
+ return {
1015
+ placementVerified: true,
1016
+ placementSatisfied,
1017
+ tocOrdinal,
1018
+ titleOrdinal,
1019
+ };
1020
+ }
1021
+ const targetOrdinal = findTargetBlockOrdinal(input.postIndex, input.placement.target);
1022
+ if (targetOrdinal == null) {
1023
+ return {
1024
+ placementVerified: false,
1025
+ placementSatisfied: true,
1026
+ tocOrdinal,
1027
+ titleOrdinal,
1028
+ targetOrdinal,
1029
+ };
1030
+ }
1031
+ const placementSatisfied = input.placement.position === 'before'
1032
+ ? input.titleNodeId == null
1033
+ ? tocOrdinal < targetOrdinal
1034
+ : titleOrdinal != null && titleOrdinal < tocOrdinal && tocOrdinal < targetOrdinal
1035
+ : input.titleNodeId == null
1036
+ ? tocOrdinal > targetOrdinal
1037
+ : titleOrdinal != null && targetOrdinal < titleOrdinal && titleOrdinal < tocOrdinal;
1038
+ return {
1039
+ placementVerified: true,
1040
+ placementSatisfied,
1041
+ tocOrdinal,
1042
+ titleOrdinal,
1043
+ targetOrdinal,
1044
+ };
1045
+ }
1046
+ async function verifyTocStep(context, resolved, execution) {
1047
+ const postIndex = await buildWorkflowDocIndex({
1048
+ documentHandle: context.documentHandle,
1049
+ documentKey: context.sessionState.documentKey,
1050
+ invokeOptions: context.invokeOptions,
1051
+ });
1052
+ const tocPresent = postIndex.blocks.some((block) => block.nodeId === execution.tocNodeId && block.nodeType === 'tableOfContents');
1053
+ const titleProvided = resolved.title != null;
1054
+ const titlePresent = titleProvided
1055
+ ? postIndex.blocks.some((block) => block.nodeId === execution.titleNodeId)
1056
+ : undefined;
1057
+ const placement = verifyTocPlacement({
1058
+ postIndex,
1059
+ placement: resolved.placement,
1060
+ tocNodeId: execution.tocNodeId,
1061
+ titleNodeId: execution.titleNodeId,
1062
+ });
1063
+ const revisionChanged = execution.revision.before !== execution.revision.after;
1064
+ const passed = tocPresent && (titlePresent ?? true) && placement.placementSatisfied && revisionChanged;
1065
+ const summary = `insert_toc checks tocPresent=${tocPresent}; titlePresent=${titlePresent ?? 'n/a'}; placementSatisfied=${placement.placementSatisfied}; revisionChanged=${revisionChanged}.`;
1066
+ if (!passed) {
1067
+ return workflowStepFailure({
1068
+ status: 'failed',
1069
+ phase: 'verify',
1070
+ code: 'WORKFLOW_STRUCTURE_INSERT_VERIFICATION_FAILED',
1071
+ message: 'insert_toc verification failed.',
1072
+ details: {
1073
+ summary,
1074
+ tocPresent,
1075
+ titlePresent,
1076
+ placementSatisfied: placement.placementSatisfied,
1077
+ placementVerified: placement.placementVerified,
1078
+ revisionChanged,
1079
+ },
1080
+ });
1081
+ }
1082
+ return workflowStepSuccess({
1083
+ action: execution.action,
1084
+ passed,
1085
+ summary,
1086
+ checks: {
1087
+ tocPresent,
1088
+ titleProvided,
1089
+ titlePresent,
1090
+ placementVerified: placement.placementVerified,
1091
+ placementSatisfied: placement.placementSatisfied,
1092
+ revisionChanged,
1093
+ tocOrdinal: placement.tocOrdinal,
1094
+ titleOrdinal: placement.titleOrdinal,
1095
+ targetOrdinal: placement.targetOrdinal,
1096
+ },
1097
+ });
1098
+ }
1099
+ async function verifySectionBreakStep(context, execution) {
1100
+ const postIndex = await buildWorkflowDocIndex({
1101
+ documentHandle: context.documentHandle,
1102
+ documentKey: context.sessionState.documentKey,
1103
+ invokeOptions: context.invokeOptions,
1104
+ });
1105
+ const sectionCreated = execution.sectionId.length > 0;
1106
+ const usedFallback = execution.sectionId === 'section-break-fallback';
1107
+ const revisionChanged = execution.revision.before !== execution.revision.after;
1108
+ const breakParagraphPresent = execution.breakParagraphNodeId == null
1109
+ ? undefined
1110
+ : postIndex.blocks.some((block) => block.nodeId === execution.breakParagraphNodeId);
1111
+ const passed = sectionCreated && (usedFallback || revisionChanged) && (breakParagraphPresent ?? true);
1112
+ const summary = `insert_section_break checks sectionCreated=${sectionCreated}; usedFallback=${usedFallback}; revisionChanged=${revisionChanged}; breakParagraphPresent=${breakParagraphPresent ?? 'n/a'}.`;
1113
+ if (!passed) {
1114
+ return workflowStepFailure({
1115
+ status: 'failed',
1116
+ phase: 'verify',
1117
+ code: 'WORKFLOW_STRUCTURE_INSERT_VERIFICATION_FAILED',
1118
+ message: 'insert_section_break verification failed.',
1119
+ details: {
1120
+ summary,
1121
+ sectionCreated,
1122
+ usedFallback,
1123
+ revisionChanged,
1124
+ breakParagraphPresent,
1125
+ },
1126
+ });
1127
+ }
1128
+ return workflowStepSuccess({
1129
+ action: execution.action,
1130
+ passed,
1131
+ summary,
1132
+ checks: {
1133
+ sectionCreated,
1134
+ usedFallback,
1135
+ revisionChanged,
1136
+ breakType: execution.breakType,
1137
+ breakParagraphPresent,
1138
+ },
1139
+ });
1140
+ }
1141
+ async function verifyParagraphStep(context, resolved, execution) {
1142
+ const postIndex = await buildWorkflowDocIndex({
1143
+ documentHandle: context.documentHandle,
1144
+ documentKey: context.sessionState.documentKey,
1145
+ invokeOptions: context.invokeOptions,
1146
+ });
1147
+ const currentText = await context.documentHandle.getText({}, context.invokeOptions);
1148
+ const paragraphPresent = postIndex.blocks.some((block) => block.nodeId === execution.paragraphNodeId && block.nodeType === 'paragraph');
1149
+ const textPresent = currentText.includes(resolved.text);
1150
+ const revisionChanged = execution.revision.before !== execution.revision.after;
1151
+ const passed = paragraphPresent && textPresent && revisionChanged;
1152
+ const summary = `insert_paragraph checks paragraphPresent=${paragraphPresent}; textPresent=${textPresent}; changeMode=${execution.changeMode}; revisionChanged=${revisionChanged}.`;
1153
+ if (!passed) {
1154
+ return workflowStepFailure({
1155
+ status: 'failed',
1156
+ phase: 'verify',
1157
+ code: 'WORKFLOW_STRUCTURE_INSERT_VERIFICATION_FAILED',
1158
+ message: 'insert_paragraph verification failed.',
1159
+ details: {
1160
+ summary,
1161
+ paragraphPresent,
1162
+ textPresent,
1163
+ revisionChanged,
1164
+ },
1165
+ });
1166
+ }
1167
+ return workflowStepSuccess({
1168
+ action: execution.action,
1169
+ passed,
1170
+ summary,
1171
+ checks: {
1172
+ paragraphPresent,
1173
+ textPresent,
1174
+ changeMode: execution.changeMode,
1175
+ revisionChanged,
1176
+ },
1177
+ });
1178
+ }
1179
+ async function verifyParagraphsStep(context, resolved, execution) {
1180
+ const postIndex = await buildWorkflowDocIndex({
1181
+ documentHandle: context.documentHandle,
1182
+ documentKey: context.sessionState.documentKey,
1183
+ invokeOptions: context.invokeOptions,
1184
+ });
1185
+ const currentText = await context.documentHandle.getText({}, context.invokeOptions);
1186
+ const presentNodeIds = new Set(postIndex.blocks.map((block) => block.nodeId));
1187
+ const paragraphsPresent = execution.paragraphNodeIds.length === resolved.texts.length &&
1188
+ execution.paragraphNodeIds.every((nodeId) => presentNodeIds.has(nodeId));
1189
+ const firstBlock = postIndex.blocks.find((block) => block.nodeId === execution.paragraphNodeIds[0]);
1190
+ const headingSatisfied = resolved.headingLevel == null
1191
+ ? true
1192
+ : firstBlock?.nodeType === 'heading' && firstBlock.headingLevel === resolved.headingLevel;
1193
+ const textsPresent = resolved.texts.every((text) => currentText.includes(text));
1194
+ const revisionChanged = execution.revision.before !== execution.revision.after;
1195
+ const passed = paragraphsPresent && headingSatisfied && textsPresent && revisionChanged;
1196
+ const summary = `insert_paragraphs checks paragraphsPresent=${paragraphsPresent}; headingSatisfied=${headingSatisfied}; textsPresent=${textsPresent}; changeMode=${execution.changeMode}; revisionChanged=${revisionChanged}.`;
1197
+ if (!passed) {
1198
+ return workflowStepFailure({
1199
+ status: 'failed',
1200
+ phase: 'verify',
1201
+ code: 'WORKFLOW_STRUCTURE_INSERT_VERIFICATION_FAILED',
1202
+ message: 'insert_paragraphs verification failed.',
1203
+ details: {
1204
+ summary,
1205
+ paragraphsPresent,
1206
+ headingSatisfied,
1207
+ textsPresent,
1208
+ revisionChanged,
1209
+ expectedParagraphs: resolved.texts.length,
1210
+ actualParagraphs: execution.paragraphNodeIds.length,
1211
+ },
1212
+ });
1213
+ }
1214
+ return workflowStepSuccess({
1215
+ action: execution.action,
1216
+ passed,
1217
+ summary,
1218
+ checks: {
1219
+ paragraphsPresent,
1220
+ headingSatisfied,
1221
+ textsPresent,
1222
+ revisionChanged,
1223
+ paragraphCount: execution.paragraphNodeIds.length,
1224
+ changeMode: execution.changeMode,
1225
+ },
1226
+ });
1227
+ }
1228
+ async function verifyMoveRangeStep(context, execution) {
1229
+ const blocks = await listAllBlocksForStructureMove(context.documentHandle, context.invokeOptions);
1230
+ const indexOf = (nodeId) => findBlockIndexByNodeId(blocks, nodeId);
1231
+ const insertedIndices = execution.insertedBlockNodeIds.map(indexOf);
1232
+ const insertedPresent = insertedIndices.length > 0 && insertedIndices.every((index) => index >= 0);
1233
+ // The recreated copies must remain contiguous and in order at the destination.
1234
+ const insertedContiguous = insertedPresent && insertedIndices.every((index, i) => i === 0 || index === insertedIndices[i - 1] + 1);
1235
+ const anchorIndex = indexOf(execution.anchorNodeId);
1236
+ const firstInsertedIndex = insertedIndices[0] ?? -1;
1237
+ const orderSatisfied = !insertedPresent || anchorIndex < 0
1238
+ ? false
1239
+ : execution.position === 'after'
1240
+ ? firstInsertedIndex > anchorIndex
1241
+ : firstInsertedIndex < anchorIndex;
1242
+ const insertedCountSatisfied = execution.insertedBlockNodeIds.length === execution.movedBlockCount;
1243
+ const deletionSatisfied = execution.deletedCount >= execution.movedBlockCount;
1244
+ // A direct move physically removes the source; a tracked move keeps a struck
1245
+ // copy in the document, so only assert removal of the original ids in direct mode.
1246
+ const originalStartRemoved = indexOf(execution.startNodeId) < 0;
1247
+ const originalEndRemoved = indexOf(execution.endNodeId) < 0;
1248
+ const sourceRemoved = execution.changeMode === 'tracked' ? true : originalStartRemoved && originalEndRemoved;
1249
+ const revisionChanged = execution.revision.before !== execution.revision.after;
1250
+ const passed = insertedPresent &&
1251
+ insertedContiguous &&
1252
+ orderSatisfied &&
1253
+ insertedCountSatisfied &&
1254
+ deletionSatisfied &&
1255
+ sourceRemoved &&
1256
+ revisionChanged;
1257
+ const summary = `move_range checks insertedPresent=${insertedPresent}; insertedContiguous=${insertedContiguous}; orderSatisfied=${orderSatisfied}; insertedCountSatisfied=${insertedCountSatisfied}; deletionSatisfied=${deletionSatisfied}; sourceRemoved=${sourceRemoved}; revisionChanged=${revisionChanged}.`;
1258
+ if (!passed) {
1259
+ return workflowStepFailure({
1260
+ status: 'failed',
1261
+ phase: 'verify',
1262
+ code: 'WORKFLOW_STRUCTURE_INSERT_VERIFICATION_FAILED',
1263
+ message: 'move_range verification failed.',
1264
+ details: {
1265
+ summary,
1266
+ insertedPresent,
1267
+ insertedContiguous,
1268
+ orderSatisfied,
1269
+ insertedCountSatisfied,
1270
+ deletionSatisfied,
1271
+ sourceRemoved,
1272
+ revisionChanged,
1273
+ startNodeId: execution.startNodeId,
1274
+ endNodeId: execution.endNodeId,
1275
+ anchorNodeId: execution.anchorNodeId,
1276
+ insertedBlockNodeIds: execution.insertedBlockNodeIds,
1277
+ },
1278
+ });
1279
+ }
1280
+ return workflowStepSuccess({
1281
+ action: execution.action,
1282
+ passed,
1283
+ summary,
1284
+ checks: {
1285
+ insertedPresent,
1286
+ insertedContiguous,
1287
+ orderSatisfied,
1288
+ insertedCountSatisfied,
1289
+ deletionSatisfied,
1290
+ sourceRemoved,
1291
+ revisionChanged,
1292
+ position: execution.position,
1293
+ destinationExtendedToSectionEnd: execution.destinationExtendedToSectionEnd,
1294
+ anchorNodeId: execution.anchorNodeId,
1295
+ firstInsertedIndex,
1296
+ anchorIndex,
1297
+ movedBlockCount: execution.movedBlockCount,
1298
+ deletedCount: execution.deletedCount,
1299
+ },
1300
+ });
1301
+ }
1302
+ async function verifyStep(context, resolved, _plan, execution) {
1303
+ if (resolved.action === 'insert_toc' && execution.action === 'insert_toc') {
1304
+ return verifyTocStep(context, resolved, execution);
1305
+ }
1306
+ if (execution.action === 'insert_section_break') {
1307
+ return verifySectionBreakStep(context, execution);
1308
+ }
1309
+ if (resolved.action === 'insert_paragraph' && execution.action === 'insert_paragraph') {
1310
+ return verifyParagraphStep(context, resolved, execution);
1311
+ }
1312
+ if (resolved.action === 'insert_paragraphs' && execution.action === 'insert_paragraphs') {
1313
+ return verifyParagraphsStep(context, resolved, execution);
1314
+ }
1315
+ if (execution.action === 'move_range') {
1316
+ return verifyMoveRangeStep(context, execution);
1317
+ }
1318
+ return workflowStepFailure({
1319
+ status: 'failed',
1320
+ phase: 'verify',
1321
+ code: 'WORKFLOW_STRUCTURE_INSERT_VERIFICATION_FAILED',
1322
+ message: 'superdoc_structure_insert produced mismatched execution output.',
1323
+ });
1324
+ }
1325
+ export async function runSuperdocStructureInsertWorkflow(input) {
1326
+ return runWorkflowEngine({
1327
+ documentHandle: input.documentHandle,
1328
+ toolName: 'superdoc_structure_insert',
1329
+ args: input.args,
1330
+ invokeOptions: input.invokeOptions,
1331
+ hooks: {
1332
+ resolve: async (context) => resolveStep(context),
1333
+ plan: async (context, resolved) => planStep(context, resolved),
1334
+ execute: async (context, resolved, plan) => executeStep(context, resolved, plan),
1335
+ verify: async (context, resolved, plan, execution) => verifyStep(context, resolved, plan, execution),
1336
+ },
1337
+ });
1338
+ }