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