@superdoc/sdk 2.20.1-next.9 → 2.21.0-next.2

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.
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var contract = require('../../generated/contract.cjs');
3
4
  var engine = require('../engine.cjs');
4
5
  var resolve = require('../resolve.cjs');
5
6
 
@@ -41,9 +42,19 @@ function parseListKind(raw, fallback) {
41
42
  }
42
43
  return LIST_KINDS.find((kind) => kind === raw);
43
44
  }
45
+ function isListCreatePreset(value) {
46
+ const schema = contract.CONTRACT.operations['doc.lists.create'].params.find((parameter) => parameter.name === 'preset')?.schema;
47
+ if (schema == null || typeof schema !== 'object')
48
+ return false;
49
+ if ('enum' in schema && Array.isArray(schema.enum))
50
+ return schema.enum.includes(value);
51
+ return ('oneOf' in schema &&
52
+ Array.isArray(schema.oneOf) &&
53
+ schema.oneOf.some((variant) => variant != null && typeof variant === 'object' && 'const' in variant && variant.const === value));
54
+ }
44
55
  function parsePreset(raw, kind) {
45
56
  if (typeof raw === 'string' && raw.length > 0) {
46
- return raw;
57
+ return isListCreatePreset(raw) ? raw : undefined;
47
58
  }
48
59
  return kind === 'bullet' ? 'disc' : 'decimal';
49
60
  }
@@ -253,6 +264,15 @@ function resolveStep(context) {
253
264
  });
254
265
  }
255
266
  const preset = parsePreset(context.args.preset, listKind);
267
+ if (preset == null) {
268
+ return engine.workflowStepFailure({
269
+ status: 'failed',
270
+ phase: 'resolve',
271
+ code: 'WORKFLOW_LIST_PRESET_INVALID',
272
+ message: 'preset must be a supported list preset.',
273
+ details: { received: context.args.preset },
274
+ });
275
+ }
256
276
  const headingText = parseOptionalText(context.args.headingText);
257
277
  const headingLevel = parseHeadingLevel(context.args.headingLevel);
258
278
  if (Number.isNaN(headingLevel)) {
@@ -12,6 +12,7 @@ type SuperdocListTransformPosition = (typeof POSITIONS)[number];
12
12
  type SuperdocListKind = (typeof LIST_KINDS)[number];
13
13
  type ListInsertParams = NonNullable<Parameters<BoundDocApi['lists']['insert']>[0]>;
14
14
  type ListInsertTarget = NonNullable<ListInsertParams['target']>;
15
+ type ListCreatePreset = NonNullable<NonNullable<Parameters<BoundDocApi['lists']['create']>[0]>['preset']>;
15
16
  type TrackedChangeRef = NonNullable<DocListsInsertResult['trackedChangeRefs']>[number];
16
17
  type SuperdocListTransformResolved = {
17
18
  action: SuperdocListTransformAction;
@@ -28,7 +29,7 @@ type SuperdocListTransformResolved = {
28
29
  targetSource: 'provided' | 'auto_single_list' | 'apparent_textual_list' | 'document_end';
29
30
  deterministicTarget: boolean;
30
31
  listKind: SuperdocListKind;
31
- preset: string;
32
+ preset: ListCreatePreset;
32
33
  headingText?: string;
33
34
  headingLevel?: number;
34
35
  listId?: string;
@@ -47,7 +48,7 @@ type SuperdocListTransformPlan = {
47
48
  targetSource: SuperdocListTransformResolved['targetSource'];
48
49
  deterministicTarget: boolean;
49
50
  listKind: SuperdocListKind;
50
- preset: string;
51
+ preset: ListCreatePreset;
51
52
  headingText?: string;
52
53
  headingLevel?: number;
53
54
  listId?: string;
@@ -59,7 +60,7 @@ type SuperdocListTransformExecution = {
59
60
  targetSource: SuperdocListTransformResolved['targetSource'];
60
61
  deterministicTarget: boolean;
61
62
  listKind: SuperdocListKind;
62
- preset: string;
63
+ preset: ListCreatePreset;
63
64
  headingText?: string;
64
65
  headingLevel?: number;
65
66
  headingNodeId?: string;
@@ -1,3 +1,4 @@
1
+ import { CONTRACT } from '../../generated/contract.js';
1
2
  import { runWorkflowEngine, workflowStepFailure, workflowStepSuccess, } from '../engine.js';
2
3
  import { resolveWorkflowTargetFromUnknown, } from '../resolve.js';
3
4
  const LIST_TRANSFORM_ACTIONS = ['insert_many', 'append_new_list'];
@@ -38,9 +39,19 @@ function parseListKind(raw, fallback) {
38
39
  }
39
40
  return LIST_KINDS.find((kind) => kind === raw);
40
41
  }
42
+ function isListCreatePreset(value) {
43
+ const schema = CONTRACT.operations['doc.lists.create'].params.find((parameter) => parameter.name === 'preset')?.schema;
44
+ if (schema == null || typeof schema !== 'object')
45
+ return false;
46
+ if ('enum' in schema && Array.isArray(schema.enum))
47
+ return schema.enum.includes(value);
48
+ return ('oneOf' in schema &&
49
+ Array.isArray(schema.oneOf) &&
50
+ schema.oneOf.some((variant) => variant != null && typeof variant === 'object' && 'const' in variant && variant.const === value));
51
+ }
41
52
  function parsePreset(raw, kind) {
42
53
  if (typeof raw === 'string' && raw.length > 0) {
43
- return raw;
54
+ return isListCreatePreset(raw) ? raw : undefined;
44
55
  }
45
56
  return kind === 'bullet' ? 'disc' : 'decimal';
46
57
  }
@@ -250,6 +261,15 @@ function resolveStep(context) {
250
261
  });
251
262
  }
252
263
  const preset = parsePreset(context.args.preset, listKind);
264
+ if (preset == null) {
265
+ return workflowStepFailure({
266
+ status: 'failed',
267
+ phase: 'resolve',
268
+ code: 'WORKFLOW_LIST_PRESET_INVALID',
269
+ message: 'preset must be a supported list preset.',
270
+ details: { received: context.args.preset },
271
+ });
272
+ }
253
273
  const headingText = parseOptionalText(context.args.headingText);
254
274
  const headingLevel = parseHeadingLevel(context.args.headingLevel);
255
275
  if (Number.isNaN(headingLevel)) {
@@ -0,0 +1,308 @@
1
+ {
2
+ "insert_paragraphs": {
3
+ "hint": "texts[] (or text for a single paragraph), placement?, headingLevel? (first item as heading 1-6), changeMode? — THE way to ADD a new paragraph/clause; never replace_text/rewrite_block an existing paragraph to append content. Each texts entry is a plain string OR a STYLED object {runs:[{text,marks}], marks?} (marks: bold/italic/underline/strikethrough/fontFamily/fontSize(points)/color(hex)/highlight). MATCH THE SECTION'S PATTERN: when siblings share intra-paragraph formatting (e.g. lettered clauses with a bold '(m)' lead-in), inspect a narrow block window with includeBlockRuns:true, read the sibling runs, and reproduce the pattern with runs on the new text IN THIS CALL — e.g. runs:[{text:'(n)',marks:{bold:true}},{text:' rest of the clause…'}]. Do not add plain and format_text afterward",
4
+ "group": "Text/structure actions",
5
+ "groupOrder": 0,
6
+ "args": ["text", "texts", "headingLevel", "placement", "changeMode", "evidence"],
7
+ "operations": ["create.paragraph"]
8
+ },
9
+ "insert_heading": {
10
+ "hint": "text, level, placement?, changeMode?",
11
+ "group": "Text/structure actions",
12
+ "groupOrder": 1,
13
+ "args": ["text", "level", "placement", "changeMode"],
14
+ "operations": ["create.heading"]
15
+ },
16
+ "replace_text": {
17
+ "hint": "edits[{find,replace}], optional selector to scope replacements to one inspected block, caseSensitive?, changeMode? — for EDITING existing wording only; the replaced span is rewritten as PLAIN text (intra-span bold/italic is dropped), so keep find/replace spans MINIMAL (the words that change, not the whole paragraph) and NEVER use it to append a new sentence/clause/paragraph (use insert_paragraphs)",
18
+ "group": "Text/structure actions",
19
+ "groupOrder": 2,
20
+ "args": ["edits", "selector", "caseSensitive", "changeMode"],
21
+ "operations": ["replace"]
22
+ },
23
+ "delete_text": {
24
+ "hint": "finds[], optional selector to scope deletions to ONE inspected block (required to delete stray whitespace — an unscoped whitespace find matches document-wide), caseSensitive?, changeMode? — deletes TEXT ONLY, leaving the block (and a list item's bullet/number) in place. To remove a whole list item, paragraph or heading use delete_blocks",
25
+ "group": "Text/structure actions",
26
+ "groupOrder": 3,
27
+ "args": ["finds", "selector", "caseSensitive", "changeMode"],
28
+ "operations": ["delete"]
29
+ },
30
+ "delete_blocks": {
31
+ "hint": "selectors[] (each resolving to ONE inspected block: list item, paragraph or heading), changeMode? — THE way to DELETE a whole LIST ITEM, paragraph or heading, bullet/number included. Use for \"remove the first item under Article II\" / \"delete that clause\". delete_text only strikes the text and leaves an empty numbered item behind. Deletes several blocks in ONE call; use delete_table for a whole table and delete_text to clear a table cell (a cell must keep a paragraph)",
32
+ "group": "Text/structure actions",
33
+ "groupOrder": 4,
34
+ "args": ["selectors", "selector", "changeMode"],
35
+ "operations": ["blocks.delete"]
36
+ },
37
+ "append_list": {
38
+ "hint": "items[], kind?: ordered|bullet, headingText?, headingLevel?, placement? {at:\"after\"|\"before\",selector} builds the list at that block instead of document end",
39
+ "group": "Text/structure actions",
40
+ "groupOrder": 5,
41
+ "args": ["items", "kind", "headingText", "headingLevel", "placement", "changeMode"],
42
+ "operations": ["lists.create", "lists.insert"],
43
+ "trackedOverrideReason": "The V2 list adapter guarantees native tracked creation and insertion."
44
+ },
45
+ "create_table": {
46
+ "hint": "rows, columns, cellTexts? (2D: rows of cells; each cell is a plain string OR a STYLED object {runs:[{text,marks}], marks?} to format the cell — marks: bold/italic/underline/strikethrough/fontFamily/fontSize(points)/color(hex)/highlight), placement?, changeMode? — style cells HERE, do not format_text afterward; changeMode:\"tracked\" makes the table insertion itself a tracked change",
47
+ "group": "Text/structure actions",
48
+ "groupOrder": 6,
49
+ "args": ["rows", "columns", "cellTexts", "placement", "changeMode", "evidence"],
50
+ "operations": ["create.table"]
51
+ },
52
+ "comment_paragraphs": {
53
+ "hint": "commentText, scope?: all|body, excludeBlockQuotes?",
54
+ "group": "Comment actions",
55
+ "groupOrder": 0,
56
+ "args": ["commentText", "scope", "excludeBlockQuotes"],
57
+ "operations": ["comments.create"]
58
+ },
59
+ "add_comments": {
60
+ "hint": "commentText, selector (one block) OR selectors[] to comment MANY blocks in ONE call (same text) — use selectors[] for \"comment every heading/section/clause\"; NEVER emit multiple add_comments calls, batch the targets into selectors[]",
61
+ "group": "Comment actions",
62
+ "groupOrder": 1,
63
+ "args": ["commentText", "selector", "selectors"],
64
+ "operations": ["comments.create"]
65
+ },
66
+ "resolve_comments": {
67
+ "hint": "anchorText? (resolve only comments anchored on text containing this; omit to resolve ALL open comments), reopen? (true = reopen resolved comments instead) — THE way to resolve (or reopen) comments. Use for \"resolve the comment(s)\" / \"mark comments resolved\".",
68
+ "group": "Comment actions",
69
+ "groupOrder": 2,
70
+ "args": ["anchorText", "reopen"],
71
+ "operations": ["comments.patch"]
72
+ },
73
+ "reply_to_comment": {
74
+ "hint": "commentText (the reply body), anchorText (text the target comment is anchored on / mentions) OR commentId — THE way to REPLY to an existing comment thread. Use for \"reply to the comment about X\" / \"respond to Reviewer's comment\". Adds a threaded reply, not a new top-level comment.",
75
+ "group": "Comment actions",
76
+ "groupOrder": 3,
77
+ "args": ["commentText", "anchorText", "commentId"],
78
+ "operations": ["comments.create"]
79
+ },
80
+ "rewrite_block": {
81
+ "hint": "selector, text, changeMode? — REPLACES the whole block text. Omitted tab controls are deleted: for a tab-aligned paragraph, use replace_text with a minimal find span to change a name/placeholder instead. For appending a NEW paragraph/clause use insert_paragraphs",
82
+ "group": "Text/structure actions",
83
+ "groupOrder": 7,
84
+ "args": ["text", "selector", "changeMode", "evidence"],
85
+ "operations": ["replace"]
86
+ },
87
+ "accept_tracked_changes": {
88
+ "hint": "id? (string or string[]) OR author?/changeType?: insert|delete|replacement|format — a string[] decides that exact set atomically; e.g. changeType:\"format\" accepts ONLY formatting revisions (bold/italic/color), leaving text edits pending",
89
+ "group": "Tracked-change review",
90
+ "groupOrder": 0,
91
+ "args": ["id", "author", "changeType"],
92
+ "operations": ["trackChanges.decide"]
93
+ },
94
+ "reject_tracked_changes": {
95
+ "hint": "id? (string or string[]) OR author?/changeType?: insert|delete|replacement|format — a string[] decides that exact set atomically",
96
+ "group": "Tracked-change review",
97
+ "groupOrder": 1,
98
+ "args": ["id", "author", "changeType"],
99
+ "operations": ["trackChanges.decide"]
100
+ },
101
+ "normalize_body_font_size": {
102
+ "hint": "fontSize, changeMode?",
103
+ "group": "Formatting actions",
104
+ "groupOrder": 4,
105
+ "args": ["fontSize", "changeMode"],
106
+ "operations": ["format.apply"]
107
+ },
108
+ "set_font_family": {
109
+ "hint": "fontFamily (e.g. \"Arial\"), selector? (one block) OR targetText/targetTexts[] (occurrences) — omit both to set the WHOLE body font, caseSensitive?, changeMode? — THE way to change the typeface. Use for \"change the font to X\" / \"set the heading font to Y\".",
110
+ "group": "Formatting actions",
111
+ "groupOrder": 5,
112
+ "args": ["fontFamily", "selector", "targetText", "targetTexts", "caseSensitive", "changeMode"],
113
+ "operations": ["format.apply"]
114
+ },
115
+ "apply_letter_spacing": {
116
+ "hint": "selector, letterSpacing, changeMode?",
117
+ "group": "Formatting actions",
118
+ "groupOrder": 6,
119
+ "args": ["selector", "letterSpacing", "changeMode"],
120
+ "operations": ["format.apply"]
121
+ },
122
+ "fill_placeholders": {
123
+ "hint": "values[] and/or fields[{label?,value}], changeMode?",
124
+ "group": "Text/structure actions",
125
+ "groupOrder": 8,
126
+ "args": ["values", "fields", "changeMode"],
127
+ "operations": ["replace"]
128
+ },
129
+ "move_range": {
130
+ "hint": "fromText (text in the FIRST block of the range to move), toText? (text in the LAST block — omit to auto-extend across the whole VISUAL SECTION: from fromText up to the next heading-like/ALL-CAPS/bold title), then exactly ONE destination: afterText OR beforeText (text in the block to land after/before). changeMode:\"tracked\" records both the copies and source deletion for review. Moves plain paragraph/heading text only; a range containing a table, list, or image is refused with nothing changed. THE way to MOVE a contiguous block range or a visual \"section\" identified BY TEXT (works on styled-paragraph sections like PREAMBLE / SCHEDULE A that are NOT Word heading nodes). afterText on a heading-like block lands the range after that block's WHOLE section.",
131
+ "group": "Text/structure actions",
132
+ "groupOrder": 9,
133
+ "args": ["fromText", "toText", "afterText", "beforeText", "changeMode"],
134
+ "operations": ["blocks.move"]
135
+ },
136
+ "insert_toc": {
137
+ "hint": "title?, placement?, changeMode?",
138
+ "group": "Media / TOC",
139
+ "groupOrder": 0,
140
+ "args": ["title", "placement", "changeMode"],
141
+ "operations": ["create.tableOfContents"],
142
+ "trackedOverrideReason": "The V2 TOC adapter guarantees native tracked creation."
143
+ },
144
+ "insert_table_row": {
145
+ "hint": "tableOrdinal?, rowIndex?, position?: before|after|above|below, cellTexts? (one per column; each a plain string OR a STYLED {runs, marks?} cell to reproduce the table's cell formatting), changeMode? — to match existing cell styling, inspect the table with includeTableCellRuns:true first, then style each new cell HERE. PLACEMENT: when the user does not say WHERE the row goes, append it after the LAST existing row (rowIndex = last row's index, position: below); never pick a middle position on your own",
146
+ "group": "Table edits",
147
+ "groupOrder": 3,
148
+ "args": ["tableOrdinal", "rowIndex", "position", "cellTexts", "changeMode"],
149
+ "operations": ["tables.insertRow"]
150
+ },
151
+ "insert_table_column": {
152
+ "hint": "tableOrdinal?, columnIndex?, position?: left|right, headerText? OR cellTexts? (one per row; each a plain string OR a STYLED {runs, marks?} cell), changeMode? — style the new column cells HERE, not with format_text",
153
+ "group": "Table edits",
154
+ "groupOrder": 4,
155
+ "args": ["tableOrdinal", "columnIndex", "position", "headerText", "cellTexts", "changeMode"],
156
+ "operations": ["tables.insertColumn"]
157
+ },
158
+ "delete_table_row": {
159
+ "hint": "tableOrdinal?, rowIndex, changeMode?",
160
+ "group": "Table edits",
161
+ "groupOrder": 5,
162
+ "args": ["tableOrdinal", "rowIndex", "changeMode"],
163
+ "operations": ["tables.deleteRow"]
164
+ },
165
+ "delete_table_column": {
166
+ "hint": "tableOrdinal?, columnIndex, changeMode?",
167
+ "group": "Table edits",
168
+ "groupOrder": 6,
169
+ "args": ["tableOrdinal", "columnIndex", "changeMode"],
170
+ "operations": ["tables.deleteColumn"]
171
+ },
172
+ "split_table": {
173
+ "hint": "tableOrdinal?, rowIndex, separatorText?, changeMode?",
174
+ "group": "Table edits",
175
+ "groupOrder": 7,
176
+ "args": ["tableOrdinal", "rowIndex", "separatorText", "changeMode"],
177
+ "operations": ["tables.split"],
178
+ "trackedOverrideReason": "The V2 table adapter guarantees native tracked table splitting."
179
+ },
180
+ "convert_list": {
181
+ "hint": "kind: ordered|bullet, listOrdinal? 1-based or anchorText? text inside an item of the target list, OR fromMarker+toMarker rendered clause numbers like \"2.1.\"/\"2.3.\" to convert a numbered-clause range, OR fromText+toText exact text inside the first/last of consecutive plain paragraphs to convert them into a list IN PLACE, changeMode? — converts lists, clause ranges, or plain paragraphs to a list without touching text; never recreate-and-delete to make existing paragraphs a list",
182
+ "group": "List/numbering actions",
183
+ "groupOrder": 0,
184
+ "args": ["kind", "fromMarker", "toMarker", "fromText", "toText", "listOrdinal", "anchorText", "changeMode"],
185
+ "operations": ["lists.apply"]
186
+ },
187
+ "split_list": {
188
+ "hint": "anchorText (text inside the item that should START the second list), restartNumbering? (default true — the new list restarts at 1; false keeps continuous numbering) — THE way to SPLIT one list into two at an item (\"split the list starting at item 7 into a new list with reset numbering\"). Nested sub-items stay with their parent. changeMode? records the change for review.",
189
+ "group": "List/numbering actions",
190
+ "groupOrder": 3,
191
+ "args": ["anchorText", "restartNumbering", "changeMode"],
192
+ "operations": ["lists.separate"],
193
+ "trackedOverrideReason": "The V2 list adapter guarantees native tracked list separation."
194
+ },
195
+ "undo_changes": {
196
+ "hint": "untilMarker? rendered marker to restore e.g. \"2.1.\", steps? 1-25 — deterministic revert that steps document history back until the marker reappears",
197
+ "group": "History",
198
+ "groupOrder": 0,
199
+ "args": ["untilMarker", "steps"],
200
+ "operations": ["history.undo"]
201
+ },
202
+ "redo_changes": {
203
+ "hint": "steps? 1-25 (default 1) — the inverse of undo_changes: steps history FORWARD to re-apply edits a prior undo removed. THE recovery when an undo overshot. Only works right after an undo, before any new edit.",
204
+ "group": "History",
205
+ "groupOrder": 1,
206
+ "args": ["steps"],
207
+ "operations": ["history.redo"]
208
+ },
209
+ "attach_numbering": {
210
+ "hint": "anchorText|nodeId, likeMarker e.g. \"10.\", changeMode? — make an existing block a numbered clause at the same scheme/level as the sibling rendering likeMarker; changeMode:\"tracked\" records the former unnumbered state as a w:pPrChange",
211
+ "group": "List/numbering actions",
212
+ "groupOrder": 1,
213
+ "args": ["likeMarker", "nodeId", "anchorText", "changeMode"],
214
+ "operations": ["lists.attach"]
215
+ },
216
+ "add_list_items": {
217
+ "hint": "THE way to ADD items into an EXISTING list (reusing its numbering + markers) — NOT append_list (which starts a brand-new list). Locate the list with anchorText (text inside an existing item) or listOrdinal (from inspect). Each item in entries/items is EITHER a plain string / {text, level?}, OR a STYLED object {runs, marks?, level?}. STYLING IN THIS ONE CALL — if any part of the new item needs inline formatting (bold/italic/underline/color/font/size on some or all of it), you MUST express it with `runs` and/or `marks` HERE. Do NOT add the item plain and then call format_text/format_paragraph on it afterward — that is the wrong tool for a just-added item and risks matching stray text elsewhere. `runs` = ordered segments [{text, marks?}] whose texts concatenate to the item text; put the pattern on each segment (marks: bold/italic/underline/strikethrough/fontFamily/fontSize(points)/color(hex e.g. #1a1a1a)/highlight). `marks` on the item = a whole-item default (e.g. Arial 16pt) applied under the per-run marks. Example — pattern 'first word bold, rest italic': runs:[{text:\"Term:\",marks:{bold:true}},{text:\" the rest\",marks:{italic:true}}]. MATCHING THE LIST'S FORMATTING IS THE DEFAULT EXPECTATION, and it means per-run inline formatting — NOT numbering (numbering is always reused automatically). superdoc_inspect returns each list item's `runs` (its inline segments with marks) by default: READ the sibling items' runs, infer the rule (which segment is bold/italic/etc., the font/size), and REPRODUCE that run structure with `runs` on the new item's OWN words IN THIS CALL. If the siblings are uniformly styled, a plain add already inherits their font/size/colour, so no marks are needed; but if any sibling has intra-item formatting, you MUST supply `runs`. level is RELATIVE to the anchor (0 = same level, positive nests deeper, NEGATIVE promotes toward the top e.g. anchor \"12(e)\" + level:-1 → new top-level item 13); only anchorText can set explicit level (listOrdinal appends at the trailing level). PLACEMENT: anchorText inserts the new item RIGHT AFTER the item containing that text — this is how you position within a list. One Word list often spans SEVERAL visual groups (the same numbering id continues past e.g. \"Label: value\" entries into unrelated paragraphs), so when the new item belongs to a visible GROUP of similar siblings, anchor the LAST item OF THAT GROUP — tail-append (listOrdinal, or anchoring the list's final item) can land it in the wrong region. Only when there is no such group and the user gives no placement signal append at the END of the list. The receipt's note/addedItems echo the rendered marker AND the immediate neighbor items — verify placement from the receipt; do NOT re-inspect wide block windows after a successful add. receipt.formattingMatched reports what was applied.",
218
+ "group": "List/numbering actions",
219
+ "groupOrder": 2,
220
+ "args": ["anchorText", "listOrdinal", "entries", "items", "changeMode"],
221
+ "operations": ["lists.insert"]
222
+ },
223
+ "format_text": {
224
+ "hint": "bold?/italic?/underline?/strike? booleans, highlight? color name, color?, fontSize?, applied to EVERY occurrence of targetText (or targetTexts[] for several phrases in one call, or a selector'd block), caseSensitive?, changeMode? — THE way to bold/italicize/underline/highlight text, tracked-safe",
225
+ "group": "Formatting actions",
226
+ "groupOrder": 0,
227
+ "args": [
228
+ "targetText",
229
+ "targetTexts",
230
+ "selector",
231
+ "caseSensitive",
232
+ "bold",
233
+ "italic",
234
+ "underline",
235
+ "strike",
236
+ "highlight",
237
+ "color",
238
+ "fontSize",
239
+ "changeMode"
240
+ ],
241
+ "operations": ["format.apply"]
242
+ },
243
+ "apply_style": {
244
+ "hint": "selector (block to restyle), then ONE of: styleId (e.g. \"Heading2\"), headingLevel (1-6), or likeText (text inside another block whose style AND effective look to copy) — THE way to restyle an EXISTING block (\"make Summary match the Parties heading\"). Never delete-and-recreate a block to change its style",
245
+ "group": "Formatting actions",
246
+ "groupOrder": 1,
247
+ "args": ["selector", "styleId", "headingLevel", "likeText", "changeMode"],
248
+ "operations": ["styles.paragraph.setStyle"]
249
+ },
250
+ "format_paragraph": {
251
+ "hint": "selector (block to align), alignment: left|center|right|justify, changeMode? — THE way to set paragraph alignment; changeMode:\"tracked\" records the former alignment as a w:pPrChange so accept/reject toggles it (never apply alignment untracked when tracking is on)",
252
+ "group": "Formatting actions",
253
+ "groupOrder": 2,
254
+ "args": ["selector", "alignment", "changeMode", "evidence"],
255
+ "operations": ["format.paragraph.setAlignment"]
256
+ },
257
+ "move_text": {
258
+ "hint": "text (exact clause/phrase to relocate), afterText? (relocate to immediately after this text), changeMode? — THE way to MOVE a text span. Direct by default (physically relocates; REQUIRES afterText). changeMode:\"tracked\" records it as a redline (tracked delete of the source + tracked insert at the destination; accept keeps the move, reject restores the order) and may omit afterText (lands right after the struck source).",
259
+ "group": "Tracked move",
260
+ "groupOrder": 0,
261
+ "args": ["text", "afterText", "changeMode"],
262
+ "operations": ["blocks.move"]
263
+ },
264
+ "style_table": {
265
+ "hint": "tableOrdinal? (1-based, default 1), accentColor? (header fill hex, default dark navy) — THE way to make a table look professional: filled accent header row with white bold text, bold first-column labels, banded rows, clean borders. Use after create_table or on any existing table.",
266
+ "group": "Table edits",
267
+ "groupOrder": 0,
268
+ "args": ["tableOrdinal", "accentColor", "changeMode"],
269
+ "operations": ["tables.applyPreset"],
270
+ "trackedOverrideReason": "The V2 table adapter guarantees native tracked preset application."
271
+ },
272
+ "move_table": {
273
+ "hint": "tableOrdinal? (1-based, default 1), placement {at:\"document_end\"|\"document_start\"|\"after\"|\"before\", selector?} — THE way to MOVE a whole table (with all its content) to a new position IN ONE call. Use for \"move the table to the end / under section X\". NEVER delete-and-recreate a table to relocate it, and never chain inserts/undos to fake a move.",
274
+ "group": "Table edits",
275
+ "groupOrder": 1,
276
+ "args": ["tableOrdinal", "placement", "changeMode"],
277
+ "operations": ["tables.move"],
278
+ "trackedOverrideReason": "The V2 table adapter guarantees native tracked table moves."
279
+ },
280
+ "delete_table": {
281
+ "hint": "tableOrdinal? (1-based, default 1), changeMode? — THE way to DELETE an entire table in one call. Use for \"remove/delete the table\". Do NOT delete rows one by one or hijack another table.",
282
+ "group": "Table edits",
283
+ "groupOrder": 2,
284
+ "args": ["tableOrdinal", "changeMode"],
285
+ "operations": ["tables.delete"]
286
+ },
287
+ "set_paragraph_spacing": {
288
+ "hint": "selector (block/blocks to space), lineSpacing? (multiplier e.g. 1.5 or 2), spaceBefore? / spaceAfter? (points) — THE way to set line spacing or space before/after paragraphs. Use this for \"add spacing\" / \"1.5 line spacing\" / \"more room between paragraphs\" — NEVER insert blank paragraphs for spacing. changeMode? records the change for review.",
289
+ "group": "Formatting actions",
290
+ "groupOrder": 3,
291
+ "args": ["selector", "lineSpacing", "spaceBefore", "spaceAfter", "changeMode"],
292
+ "operations": ["format.paragraph.setSpacing"]
293
+ },
294
+ "insert_page_break": {
295
+ "hint": "selector (block that should start on a new page) — THE way to \"start X on a new page\" / add a page break before a block (sets pageBreakBefore). Use instead of inserting empty paragraphs to push content down. changeMode? records the change for review.",
296
+ "group": "Layout",
297
+ "groupOrder": 0,
298
+ "args": ["selector", "changeMode"],
299
+ "operations": ["format.paragraph.setFlowOptions"]
300
+ },
301
+ "add_hyperlink": {
302
+ "hint": "text (exact text to turn into a link), url, tooltip? — THE way to make existing text a clickable hyperlink to a URL. changeMode? records the change for review.",
303
+ "group": "Links",
304
+ "groupOrder": 0,
305
+ "args": ["text", "url", "tooltip", "changeMode"],
306
+ "operations": ["hyperlinks.insert"]
307
+ }
308
+ }