@superdoc/sdk 2.12.1-next.1 → 2.13.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/actions.cjs +36 -121
- package/dist/agent/actions.js +36 -121
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.cjs +13 -0
- package/dist/generated/client.d.ts +721 -0
- package/dist/generated/client.js +13 -0
- package/dist/generated/contract.cjs +5507 -687
- package/dist/generated/contract.js +5507 -687
- package/dist/generated/intent-dispatch.generated.cjs +2 -0
- package/dist/generated/intent-dispatch.generated.js +2 -0
- package/dist/runtime/sdk-version.generated.cjs +1 -1
- package/dist/runtime/sdk-version.generated.d.ts +1 -1
- package/dist/runtime/sdk-version.generated.js +1 -1
- package/package.json +6 -6
- package/tools/catalog.json +142 -97
- package/tools/intent_dispatch_generated.py +4 -0
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +127 -97
- package/tools/tools.generic.json +130 -98
- package/tools/tools.openai.json +127 -97
- package/tools/tools.vercel.json +127 -97
package/dist/agent/actions.cjs
CHANGED
|
@@ -65,7 +65,7 @@ const ACTION_HINTS = {
|
|
|
65
65
|
delete_blocks: '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)',
|
|
66
66
|
append_list: 'items[], kind?: ordered|bullet, headingText?, headingLevel?, placement? {at:"after"|"before",selector} builds the list at that block instead of document end',
|
|
67
67
|
create_table: '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',
|
|
68
|
-
rewrite_block: "selector, text, changeMode? — REPLACES the whole block text
|
|
68
|
+
rewrite_block: "selector, text, changeMode? — REPLACES the whole block text using the document API's formatting preservation. For appending a NEW paragraph/clause use insert_paragraphs, and for small wording edits prefer replace_text with a minimal find span",
|
|
69
69
|
fill_placeholders: 'values[] and/or fields[{label?,value}], changeMode?',
|
|
70
70
|
move_range: '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). Direct-only today: changeMode:"tracked" fails with no mutation because block-range deletion cannot be tracked. 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.',
|
|
71
71
|
comment_paragraphs: 'commentText, scope?: all|body, excludeBlockQuotes?',
|
|
@@ -1133,7 +1133,7 @@ async function runInsertHeading(doc, args) {
|
|
|
1133
1133
|
return failedReceipt('insert_heading', err, pre);
|
|
1134
1134
|
}
|
|
1135
1135
|
}
|
|
1136
|
-
async function
|
|
1136
|
+
async function runReplaceTextUnchecked(doc, args) {
|
|
1137
1137
|
const selectorDomains = args.selector ? snapshotDomainsForSelector(args.selector) : null;
|
|
1138
1138
|
const requiresBlockSnapshot = args.selector != null;
|
|
1139
1139
|
const preIdentity = requiresBlockSnapshot ? null : await readDocumentIdentity(doc);
|
|
@@ -1149,11 +1149,6 @@ async function runReplaceText(doc, args) {
|
|
|
1149
1149
|
let skippedEdits = [];
|
|
1150
1150
|
let steps;
|
|
1151
1151
|
let applyExpectedRevision;
|
|
1152
|
-
// Blocks whose text this call rewrites, with their pre-mutation run layout:
|
|
1153
|
-
// the engine replace lane renders the replaced range as uniform runs, so a
|
|
1154
|
-
// whole-paragraph find/replace flattens intra-paragraph formatting (bold
|
|
1155
|
-
// lead-ins). Restore it positionally after the mutation.
|
|
1156
|
-
const preserveTargets = [];
|
|
1157
1152
|
if (args.selector) {
|
|
1158
1153
|
const target = selectorToBlockTarget(args.selector, pre);
|
|
1159
1154
|
if (!target) {
|
|
@@ -1192,13 +1187,6 @@ async function runReplaceText(doc, args) {
|
|
|
1192
1187
|
];
|
|
1193
1188
|
applyExpectedRevision = pre.revision;
|
|
1194
1189
|
selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
1195
|
-
preserveTargets.push({
|
|
1196
|
-
nodeId: target.nodeId,
|
|
1197
|
-
nodeType: target.nodeType ?? 'paragraph',
|
|
1198
|
-
oldText: target.text,
|
|
1199
|
-
newText: rewrittenText,
|
|
1200
|
-
runs: await captureBlockRuns(doc, target.nodeId, target.text),
|
|
1201
|
-
});
|
|
1202
1190
|
}
|
|
1203
1191
|
else {
|
|
1204
1192
|
let candidates = args.edits.map((edit, editIndex) => ({
|
|
@@ -1256,12 +1244,6 @@ async function runReplaceText(doc, args) {
|
|
|
1256
1244
|
steps = candidates.map((candidate) => candidate.step);
|
|
1257
1245
|
}
|
|
1258
1246
|
const result = await executeMutations(doc, steps, args.changeMode, applyExpectedRevision);
|
|
1259
|
-
const preserved = [];
|
|
1260
|
-
for (const entry of preserveTargets) {
|
|
1261
|
-
const restored = await preserveRunPatternAfterRewrite(doc, entry.nodeId, entry.oldText, entry.runs, entry.newText);
|
|
1262
|
-
if (restored)
|
|
1263
|
-
preserved.push({ nodeId: entry.nodeId, ...restored });
|
|
1264
|
-
}
|
|
1265
1247
|
if (args.selector && selectedTargets[0]) {
|
|
1266
1248
|
const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] });
|
|
1267
1249
|
const blockId = selectedTargets[0].matched[0];
|
|
@@ -1290,10 +1272,7 @@ async function runReplaceText(doc, args) {
|
|
|
1290
1272
|
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1291
1273
|
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
1292
1274
|
selectedTargets,
|
|
1293
|
-
executedOperations: [
|
|
1294
|
-
{ operationId: 'doc.mutations.apply', result },
|
|
1295
|
-
...(preserved.length > 0 ? [{ operationId: 'doc.format.apply', result: { preserved } }] : []),
|
|
1296
|
-
],
|
|
1275
|
+
executedOperations: [{ operationId: 'doc.mutations.apply', result }],
|
|
1297
1276
|
verification,
|
|
1298
1277
|
};
|
|
1299
1278
|
}
|
|
@@ -1317,7 +1296,6 @@ async function runReplaceText(doc, args) {
|
|
|
1317
1296
|
...(skippedEditRationale ? { rationale: skippedEditRationale } : {}),
|
|
1318
1297
|
result,
|
|
1319
1298
|
},
|
|
1320
|
-
...(preserved.length > 0 ? [{ operationId: 'doc.format.apply', result: { preserved } }] : []),
|
|
1321
1299
|
],
|
|
1322
1300
|
verification,
|
|
1323
1301
|
};
|
|
@@ -1326,6 +1304,13 @@ async function runReplaceText(doc, args) {
|
|
|
1326
1304
|
return failedReceipt('replace_text', err, pre);
|
|
1327
1305
|
}
|
|
1328
1306
|
}
|
|
1307
|
+
async function runReplaceText(doc, args) {
|
|
1308
|
+
const changeMode = args.changeMode ?? 'direct';
|
|
1309
|
+
return {
|
|
1310
|
+
...(await runReplaceTextUnchecked(doc, { ...args, changeMode })),
|
|
1311
|
+
changeMode,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1329
1314
|
async function runDeleteText(doc, args) {
|
|
1330
1315
|
const caseSensitive = args.caseSensitive === true;
|
|
1331
1316
|
// Scoped form: delete finds within ONE block (mirrors replace_text's selector
|
|
@@ -2583,7 +2568,6 @@ async function runRewriteBlock(doc, args) {
|
|
|
2583
2568
|
throw new Error('selector did not resolve to a body block');
|
|
2584
2569
|
context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
2585
2570
|
const text = normalizeTitleLikeRewriteText(target.text, args.text);
|
|
2586
|
-
const runs = await captureBlockRuns(doc, target.nodeId, target.text);
|
|
2587
2571
|
await executeMutations(context.document, [
|
|
2588
2572
|
{
|
|
2589
2573
|
id: 'rewrite-block-1',
|
|
@@ -2592,7 +2576,6 @@ async function runRewriteBlock(doc, args) {
|
|
|
2592
2576
|
args: { replacement: { text }, style: preserveRewriteStyle() },
|
|
2593
2577
|
},
|
|
2594
2578
|
], args.changeMode, pre.revision);
|
|
2595
|
-
await preserveRunPatternAfterRewrite(context.document, target.nodeId, target.text, runs, text);
|
|
2596
2579
|
const post = await context.finish();
|
|
2597
2580
|
const rewritten = await post.block(target.nodeId);
|
|
2598
2581
|
const verification = [
|
|
@@ -2665,23 +2648,8 @@ async function captureBlockRuns(doc, nodeId, text) {
|
|
|
2665
2648
|
}
|
|
2666
2649
|
}
|
|
2667
2650
|
/**
|
|
2668
|
-
*
|
|
2669
|
-
*
|
|
2670
|
-
* drops intra-paragraph formatting AND can smear the first run's marks over
|
|
2671
|
-
* everything. Marks are restored positionally over the parts of the text the
|
|
2672
|
-
* rewrite did NOT change (the common prefix and suffix of old vs new text),
|
|
2673
|
-
* and the changed middle takes the marks of the run it continues from —
|
|
2674
|
-
* Word's "typing continues formatting" rule. Best-effort: never fails the
|
|
2675
|
-
* rewrite.
|
|
2676
|
-
*/
|
|
2677
|
-
/**
|
|
2678
|
-
* Span-aware run-pattern restore for scoped replaces. The generic
|
|
2679
|
-
* preserveRunPatternAfterRewrite diffs prefix/suffix and treats everything
|
|
2680
|
-
* between the FIRST and LAST change as one modified middle — with two
|
|
2681
|
-
* separated replacements that flattens independently-styled runs between
|
|
2682
|
-
* them. Here the exact spans are known, so every untouched interval keeps its
|
|
2683
|
-
* original run formatting at the offset-mapped position, and each replacement
|
|
2684
|
-
* inherits the marks of the run it starts in.
|
|
2651
|
+
* Restore scoped replacements from their known spans: each untouched interval
|
|
2652
|
+
* keeps its run formatting, and replacements inherit the marks at their start.
|
|
2685
2653
|
* `spans` are FULL-TEXT coordinates, ascending, non-overlapping.
|
|
2686
2654
|
*/
|
|
2687
2655
|
async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
|
|
@@ -2774,74 +2742,6 @@ async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
|
|
|
2774
2742
|
return { skipped: reasonOf(err) };
|
|
2775
2743
|
}
|
|
2776
2744
|
}
|
|
2777
|
-
async function preserveRunPatternAfterRewrite(doc, nodeId, oldText, oldRuns, newText) {
|
|
2778
|
-
// Nothing to restore when the block was uniformly plain; a single STYLED run
|
|
2779
|
-
// still needs restoring (whole-block replaces drop even uniform marks).
|
|
2780
|
-
if (!oldRuns || oldRuns.length === 0)
|
|
2781
|
-
return null;
|
|
2782
|
-
if (!oldRuns.some((run) => Object.keys(inlineFromSnapshotRun(run, new Set())).length > 0))
|
|
2783
|
-
return null;
|
|
2784
|
-
const formatFn = maybeMethod(doc, ['format', 'apply']);
|
|
2785
|
-
if (!formatFn)
|
|
2786
|
-
return null;
|
|
2787
|
-
// Boolean marks any old run carried: plain ranges must explicitly clear
|
|
2788
|
-
// these, because the rewritten single run may have smeared them everywhere.
|
|
2789
|
-
const normalize = new Set(RUN_BOOL_MARKS.filter((key) => oldRuns.some((run) => run[key])));
|
|
2790
|
-
// query.match run offsets are relative to the trimmed text used to match.
|
|
2791
|
-
const oldTrim = oldText.trim();
|
|
2792
|
-
const newTrimStart = newText.length - newText.trimStart().length;
|
|
2793
|
-
const newTrim = newText.trim();
|
|
2794
|
-
let prefix = 0;
|
|
2795
|
-
const maxCommon = Math.min(oldTrim.length, newTrim.length);
|
|
2796
|
-
while (prefix < maxCommon && oldTrim[prefix] === newTrim[prefix])
|
|
2797
|
-
prefix += 1;
|
|
2798
|
-
let suffix = 0;
|
|
2799
|
-
while (suffix < maxCommon - prefix && oldTrim[oldTrim.length - 1 - suffix] === newTrim[newTrim.length - 1 - suffix])
|
|
2800
|
-
suffix += 1;
|
|
2801
|
-
const delta = newTrim.length - oldTrim.length;
|
|
2802
|
-
let applied = 0;
|
|
2803
|
-
const applyRange = async (start, end, inline) => {
|
|
2804
|
-
if (end <= start || Object.keys(inline).length === 0)
|
|
2805
|
-
return;
|
|
2806
|
-
// Always direct, even under tracked mode. This pass RESTORES the marks the
|
|
2807
|
-
// rewrite flattened; it is not new authorial formatting, and the text
|
|
2808
|
-
// revision already owns the span's accept/reject fate. Applying it tracked
|
|
2809
|
-
// adds a second reviewable change per rewrite (SD-4309's conformance matrix
|
|
2810
|
-
// counts one per action), and rejecting that change alone would strand the
|
|
2811
|
-
// text in the flattened look the rewrite produced.
|
|
2812
|
-
const changed = await applyInlineQuiet(formatFn, {
|
|
2813
|
-
blockId: nodeId,
|
|
2814
|
-
start: newTrimStart + start,
|
|
2815
|
-
end: newTrimStart + end,
|
|
2816
|
-
inline,
|
|
2817
|
-
});
|
|
2818
|
-
if (changed)
|
|
2819
|
-
applied += 1;
|
|
2820
|
-
};
|
|
2821
|
-
try {
|
|
2822
|
-
for (const run of oldRuns) {
|
|
2823
|
-
const inline = inlineFromSnapshotRun(run, normalize);
|
|
2824
|
-
// Portion of the run inside the unchanged prefix.
|
|
2825
|
-
await applyRange(run.start, Math.min(run.end, prefix), inline);
|
|
2826
|
-
// Portion inside the unchanged suffix, shifted by the length delta.
|
|
2827
|
-
const sufStart = Math.max(run.start, oldTrim.length - suffix);
|
|
2828
|
-
if (run.end > sufStart)
|
|
2829
|
-
await applyRange(sufStart + delta, run.end + delta, inline);
|
|
2830
|
-
}
|
|
2831
|
-
// The CHANGED middle keeps whatever the engine smeared; give it the marks
|
|
2832
|
-
// of the run the change continues from (the one containing the last
|
|
2833
|
-
// unchanged prefix character).
|
|
2834
|
-
const middleEnd = newTrim.length - suffix;
|
|
2835
|
-
if (middleEnd > prefix) {
|
|
2836
|
-
const anchorRun = oldRuns.find((run) => prefix > run.start && prefix <= run.end) ?? oldRuns[oldRuns.length - 1];
|
|
2837
|
-
await applyRange(prefix, middleEnd, inlineFromSnapshotRun(anchorRun, normalize));
|
|
2838
|
-
}
|
|
2839
|
-
return applied > 0 ? { restoredRunRanges: applied } : null;
|
|
2840
|
-
}
|
|
2841
|
-
catch (err) {
|
|
2842
|
-
return { skipped: reasonOf(err) };
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
2745
|
const NAMED_COLORS = {
|
|
2846
2746
|
black: '000000',
|
|
2847
2747
|
blue: '0000FF',
|
|
@@ -4613,7 +4513,7 @@ function compactOpResult(result) {
|
|
|
4613
4513
|
compact.stepCount = result.steps.length;
|
|
4614
4514
|
return compact;
|
|
4615
4515
|
}
|
|
4616
|
-
function parseScopedReplaceArgs(args) {
|
|
4516
|
+
function parseScopedReplaceArgs(args, changeMode) {
|
|
4617
4517
|
if (args.action !== 'replace_text')
|
|
4618
4518
|
return null;
|
|
4619
4519
|
if (!isRecord(args.selector))
|
|
@@ -4630,7 +4530,7 @@ function parseScopedReplaceArgs(args) {
|
|
|
4630
4530
|
edits,
|
|
4631
4531
|
selector: args.selector,
|
|
4632
4532
|
caseSensitive: args.caseSensitive === true,
|
|
4633
|
-
changeMode
|
|
4533
|
+
changeMode,
|
|
4634
4534
|
};
|
|
4635
4535
|
}
|
|
4636
4536
|
async function runScopedReplaceUnchecked(doc, args) {
|
|
@@ -4726,9 +4626,8 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4726
4626
|
},
|
|
4727
4627
|
args: { replacement: { text: span.replace } },
|
|
4728
4628
|
}));
|
|
4729
|
-
//
|
|
4730
|
-
//
|
|
4731
|
-
// applies directly, so restoring marks adds no second reviewable change.
|
|
4629
|
+
// Restore the known edit spans directly so the formatting pass adds no
|
|
4630
|
+
// separate tracked change.
|
|
4732
4631
|
const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
|
|
4733
4632
|
try {
|
|
4734
4633
|
result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
|
|
@@ -4796,11 +4695,15 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4796
4695
|
};
|
|
4797
4696
|
}
|
|
4798
4697
|
async function runScopedReplace(doc, args) {
|
|
4698
|
+
const changeMode = args.changeMode ?? 'direct';
|
|
4799
4699
|
try {
|
|
4800
|
-
return
|
|
4700
|
+
return {
|
|
4701
|
+
...(await runScopedReplaceUnchecked(doc, { ...args, changeMode })),
|
|
4702
|
+
changeMode,
|
|
4703
|
+
};
|
|
4801
4704
|
}
|
|
4802
4705
|
catch (error) {
|
|
4803
|
-
return failedReceipt('replace_text', error);
|
|
4706
|
+
return { ...failedReceipt('replace_text', error), changeMode };
|
|
4804
4707
|
}
|
|
4805
4708
|
}
|
|
4806
4709
|
/**
|
|
@@ -6526,11 +6429,12 @@ async function dispatchSuperdocPerformAction(doc, args) {
|
|
|
6526
6429
|
code: 'INVALID_ARGUMENT',
|
|
6527
6430
|
});
|
|
6528
6431
|
}
|
|
6432
|
+
const changeMode = parseReplaceTextChangeMode(args.changeMode);
|
|
6529
6433
|
// Selector-scoped replaces take the span-targeted path: per-find
|
|
6530
6434
|
// text.rewrite spans inside the one selected block, so tabs/breaks/
|
|
6531
6435
|
// images outside the spans survive and the receipt reports per-edit
|
|
6532
6436
|
// applied/skipped truth.
|
|
6533
|
-
const scoped = parseScopedReplaceArgs(args);
|
|
6437
|
+
const scoped = parseScopedReplaceArgs(args, changeMode);
|
|
6534
6438
|
if (scoped) {
|
|
6535
6439
|
return runScopedReplace(doc, scoped);
|
|
6536
6440
|
}
|
|
@@ -6543,7 +6447,7 @@ async function dispatchSuperdocPerformAction(doc, args) {
|
|
|
6543
6447
|
edits,
|
|
6544
6448
|
selector,
|
|
6545
6449
|
caseSensitive: args.caseSensitive === true,
|
|
6546
|
-
changeMode
|
|
6450
|
+
changeMode,
|
|
6547
6451
|
});
|
|
6548
6452
|
}
|
|
6549
6453
|
case 'delete_text': {
|
|
@@ -7171,6 +7075,17 @@ function parseChangeMode(value) {
|
|
|
7171
7075
|
return value;
|
|
7172
7076
|
return undefined;
|
|
7173
7077
|
}
|
|
7078
|
+
function parseReplaceTextChangeMode(value) {
|
|
7079
|
+
if (value === undefined)
|
|
7080
|
+
return 'direct';
|
|
7081
|
+
const changeMode = parseChangeMode(value);
|
|
7082
|
+
if (changeMode)
|
|
7083
|
+
return changeMode;
|
|
7084
|
+
throw new errors.SuperDocCliError('replace_text: "changeMode" must be "direct" or "tracked"', {
|
|
7085
|
+
code: 'INVALID_ARGUMENT',
|
|
7086
|
+
details: { changeMode: value },
|
|
7087
|
+
});
|
|
7088
|
+
}
|
|
7174
7089
|
function parseTrackedChangeKind(value) {
|
|
7175
7090
|
if (value === 'insert' || value === 'delete' || value === 'replacement' || value === 'format')
|
|
7176
7091
|
return value;
|
package/dist/agent/actions.js
CHANGED
|
@@ -62,7 +62,7 @@ export const ACTION_HINTS = {
|
|
|
62
62
|
delete_blocks: '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)',
|
|
63
63
|
append_list: 'items[], kind?: ordered|bullet, headingText?, headingLevel?, placement? {at:"after"|"before",selector} builds the list at that block instead of document end',
|
|
64
64
|
create_table: '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',
|
|
65
|
-
rewrite_block: "selector, text, changeMode? — REPLACES the whole block text
|
|
65
|
+
rewrite_block: "selector, text, changeMode? — REPLACES the whole block text using the document API's formatting preservation. For appending a NEW paragraph/clause use insert_paragraphs, and for small wording edits prefer replace_text with a minimal find span",
|
|
66
66
|
fill_placeholders: 'values[] and/or fields[{label?,value}], changeMode?',
|
|
67
67
|
move_range: '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). Direct-only today: changeMode:"tracked" fails with no mutation because block-range deletion cannot be tracked. 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.',
|
|
68
68
|
comment_paragraphs: 'commentText, scope?: all|body, excludeBlockQuotes?',
|
|
@@ -1130,7 +1130,7 @@ async function runInsertHeading(doc, args) {
|
|
|
1130
1130
|
return failedReceipt('insert_heading', err, pre);
|
|
1131
1131
|
}
|
|
1132
1132
|
}
|
|
1133
|
-
async function
|
|
1133
|
+
async function runReplaceTextUnchecked(doc, args) {
|
|
1134
1134
|
const selectorDomains = args.selector ? snapshotDomainsForSelector(args.selector) : null;
|
|
1135
1135
|
const requiresBlockSnapshot = args.selector != null;
|
|
1136
1136
|
const preIdentity = requiresBlockSnapshot ? null : await readDocumentIdentity(doc);
|
|
@@ -1146,11 +1146,6 @@ async function runReplaceText(doc, args) {
|
|
|
1146
1146
|
let skippedEdits = [];
|
|
1147
1147
|
let steps;
|
|
1148
1148
|
let applyExpectedRevision;
|
|
1149
|
-
// Blocks whose text this call rewrites, with their pre-mutation run layout:
|
|
1150
|
-
// the engine replace lane renders the replaced range as uniform runs, so a
|
|
1151
|
-
// whole-paragraph find/replace flattens intra-paragraph formatting (bold
|
|
1152
|
-
// lead-ins). Restore it positionally after the mutation.
|
|
1153
|
-
const preserveTargets = [];
|
|
1154
1149
|
if (args.selector) {
|
|
1155
1150
|
const target = selectorToBlockTarget(args.selector, pre);
|
|
1156
1151
|
if (!target) {
|
|
@@ -1189,13 +1184,6 @@ async function runReplaceText(doc, args) {
|
|
|
1189
1184
|
];
|
|
1190
1185
|
applyExpectedRevision = pre.revision;
|
|
1191
1186
|
selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
1192
|
-
preserveTargets.push({
|
|
1193
|
-
nodeId: target.nodeId,
|
|
1194
|
-
nodeType: target.nodeType ?? 'paragraph',
|
|
1195
|
-
oldText: target.text,
|
|
1196
|
-
newText: rewrittenText,
|
|
1197
|
-
runs: await captureBlockRuns(doc, target.nodeId, target.text),
|
|
1198
|
-
});
|
|
1199
1187
|
}
|
|
1200
1188
|
else {
|
|
1201
1189
|
let candidates = args.edits.map((edit, editIndex) => ({
|
|
@@ -1253,12 +1241,6 @@ async function runReplaceText(doc, args) {
|
|
|
1253
1241
|
steps = candidates.map((candidate) => candidate.step);
|
|
1254
1242
|
}
|
|
1255
1243
|
const result = await executeMutations(doc, steps, args.changeMode, applyExpectedRevision);
|
|
1256
|
-
const preserved = [];
|
|
1257
|
-
for (const entry of preserveTargets) {
|
|
1258
|
-
const restored = await preserveRunPatternAfterRewrite(doc, entry.nodeId, entry.oldText, entry.runs, entry.newText);
|
|
1259
|
-
if (restored)
|
|
1260
|
-
preserved.push({ nodeId: entry.nodeId, ...restored });
|
|
1261
|
-
}
|
|
1262
1244
|
if (args.selector && selectedTargets[0]) {
|
|
1263
1245
|
const post = await buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] });
|
|
1264
1246
|
const blockId = selectedTargets[0].matched[0];
|
|
@@ -1287,10 +1269,7 @@ async function runReplaceText(doc, args) {
|
|
|
1287
1269
|
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1288
1270
|
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
1289
1271
|
selectedTargets,
|
|
1290
|
-
executedOperations: [
|
|
1291
|
-
{ operationId: 'doc.mutations.apply', result },
|
|
1292
|
-
...(preserved.length > 0 ? [{ operationId: 'doc.format.apply', result: { preserved } }] : []),
|
|
1293
|
-
],
|
|
1272
|
+
executedOperations: [{ operationId: 'doc.mutations.apply', result }],
|
|
1294
1273
|
verification,
|
|
1295
1274
|
};
|
|
1296
1275
|
}
|
|
@@ -1314,7 +1293,6 @@ async function runReplaceText(doc, args) {
|
|
|
1314
1293
|
...(skippedEditRationale ? { rationale: skippedEditRationale } : {}),
|
|
1315
1294
|
result,
|
|
1316
1295
|
},
|
|
1317
|
-
...(preserved.length > 0 ? [{ operationId: 'doc.format.apply', result: { preserved } }] : []),
|
|
1318
1296
|
],
|
|
1319
1297
|
verification,
|
|
1320
1298
|
};
|
|
@@ -1323,6 +1301,13 @@ async function runReplaceText(doc, args) {
|
|
|
1323
1301
|
return failedReceipt('replace_text', err, pre);
|
|
1324
1302
|
}
|
|
1325
1303
|
}
|
|
1304
|
+
async function runReplaceText(doc, args) {
|
|
1305
|
+
const changeMode = args.changeMode ?? 'direct';
|
|
1306
|
+
return {
|
|
1307
|
+
...(await runReplaceTextUnchecked(doc, { ...args, changeMode })),
|
|
1308
|
+
changeMode,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1326
1311
|
async function runDeleteText(doc, args) {
|
|
1327
1312
|
const caseSensitive = args.caseSensitive === true;
|
|
1328
1313
|
// Scoped form: delete finds within ONE block (mirrors replace_text's selector
|
|
@@ -2580,7 +2565,6 @@ async function runRewriteBlock(doc, args) {
|
|
|
2580
2565
|
throw new Error('selector did not resolve to a body block');
|
|
2581
2566
|
context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
2582
2567
|
const text = normalizeTitleLikeRewriteText(target.text, args.text);
|
|
2583
|
-
const runs = await captureBlockRuns(doc, target.nodeId, target.text);
|
|
2584
2568
|
await executeMutations(context.document, [
|
|
2585
2569
|
{
|
|
2586
2570
|
id: 'rewrite-block-1',
|
|
@@ -2589,7 +2573,6 @@ async function runRewriteBlock(doc, args) {
|
|
|
2589
2573
|
args: { replacement: { text }, style: preserveRewriteStyle() },
|
|
2590
2574
|
},
|
|
2591
2575
|
], args.changeMode, pre.revision);
|
|
2592
|
-
await preserveRunPatternAfterRewrite(context.document, target.nodeId, target.text, runs, text);
|
|
2593
2576
|
const post = await context.finish();
|
|
2594
2577
|
const rewritten = await post.block(target.nodeId);
|
|
2595
2578
|
const verification = [
|
|
@@ -2662,23 +2645,8 @@ async function captureBlockRuns(doc, nodeId, text) {
|
|
|
2662
2645
|
}
|
|
2663
2646
|
}
|
|
2664
2647
|
/**
|
|
2665
|
-
*
|
|
2666
|
-
*
|
|
2667
|
-
* drops intra-paragraph formatting AND can smear the first run's marks over
|
|
2668
|
-
* everything. Marks are restored positionally over the parts of the text the
|
|
2669
|
-
* rewrite did NOT change (the common prefix and suffix of old vs new text),
|
|
2670
|
-
* and the changed middle takes the marks of the run it continues from —
|
|
2671
|
-
* Word's "typing continues formatting" rule. Best-effort: never fails the
|
|
2672
|
-
* rewrite.
|
|
2673
|
-
*/
|
|
2674
|
-
/**
|
|
2675
|
-
* Span-aware run-pattern restore for scoped replaces. The generic
|
|
2676
|
-
* preserveRunPatternAfterRewrite diffs prefix/suffix and treats everything
|
|
2677
|
-
* between the FIRST and LAST change as one modified middle — with two
|
|
2678
|
-
* separated replacements that flattens independently-styled runs between
|
|
2679
|
-
* them. Here the exact spans are known, so every untouched interval keeps its
|
|
2680
|
-
* original run formatting at the offset-mapped position, and each replacement
|
|
2681
|
-
* inherits the marks of the run it starts in.
|
|
2648
|
+
* Restore scoped replacements from their known spans: each untouched interval
|
|
2649
|
+
* keeps its run formatting, and replacements inherit the marks at their start.
|
|
2682
2650
|
* `spans` are FULL-TEXT coordinates, ascending, non-overlapping.
|
|
2683
2651
|
*/
|
|
2684
2652
|
async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
|
|
@@ -2771,74 +2739,6 @@ async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
|
|
|
2771
2739
|
return { skipped: reasonOf(err) };
|
|
2772
2740
|
}
|
|
2773
2741
|
}
|
|
2774
|
-
async function preserveRunPatternAfterRewrite(doc, nodeId, oldText, oldRuns, newText) {
|
|
2775
|
-
// Nothing to restore when the block was uniformly plain; a single STYLED run
|
|
2776
|
-
// still needs restoring (whole-block replaces drop even uniform marks).
|
|
2777
|
-
if (!oldRuns || oldRuns.length === 0)
|
|
2778
|
-
return null;
|
|
2779
|
-
if (!oldRuns.some((run) => Object.keys(inlineFromSnapshotRun(run, new Set())).length > 0))
|
|
2780
|
-
return null;
|
|
2781
|
-
const formatFn = maybeMethod(doc, ['format', 'apply']);
|
|
2782
|
-
if (!formatFn)
|
|
2783
|
-
return null;
|
|
2784
|
-
// Boolean marks any old run carried: plain ranges must explicitly clear
|
|
2785
|
-
// these, because the rewritten single run may have smeared them everywhere.
|
|
2786
|
-
const normalize = new Set(RUN_BOOL_MARKS.filter((key) => oldRuns.some((run) => run[key])));
|
|
2787
|
-
// query.match run offsets are relative to the trimmed text used to match.
|
|
2788
|
-
const oldTrim = oldText.trim();
|
|
2789
|
-
const newTrimStart = newText.length - newText.trimStart().length;
|
|
2790
|
-
const newTrim = newText.trim();
|
|
2791
|
-
let prefix = 0;
|
|
2792
|
-
const maxCommon = Math.min(oldTrim.length, newTrim.length);
|
|
2793
|
-
while (prefix < maxCommon && oldTrim[prefix] === newTrim[prefix])
|
|
2794
|
-
prefix += 1;
|
|
2795
|
-
let suffix = 0;
|
|
2796
|
-
while (suffix < maxCommon - prefix && oldTrim[oldTrim.length - 1 - suffix] === newTrim[newTrim.length - 1 - suffix])
|
|
2797
|
-
suffix += 1;
|
|
2798
|
-
const delta = newTrim.length - oldTrim.length;
|
|
2799
|
-
let applied = 0;
|
|
2800
|
-
const applyRange = async (start, end, inline) => {
|
|
2801
|
-
if (end <= start || Object.keys(inline).length === 0)
|
|
2802
|
-
return;
|
|
2803
|
-
// Always direct, even under tracked mode. This pass RESTORES the marks the
|
|
2804
|
-
// rewrite flattened; it is not new authorial formatting, and the text
|
|
2805
|
-
// revision already owns the span's accept/reject fate. Applying it tracked
|
|
2806
|
-
// adds a second reviewable change per rewrite (SD-4309's conformance matrix
|
|
2807
|
-
// counts one per action), and rejecting that change alone would strand the
|
|
2808
|
-
// text in the flattened look the rewrite produced.
|
|
2809
|
-
const changed = await applyInlineQuiet(formatFn, {
|
|
2810
|
-
blockId: nodeId,
|
|
2811
|
-
start: newTrimStart + start,
|
|
2812
|
-
end: newTrimStart + end,
|
|
2813
|
-
inline,
|
|
2814
|
-
});
|
|
2815
|
-
if (changed)
|
|
2816
|
-
applied += 1;
|
|
2817
|
-
};
|
|
2818
|
-
try {
|
|
2819
|
-
for (const run of oldRuns) {
|
|
2820
|
-
const inline = inlineFromSnapshotRun(run, normalize);
|
|
2821
|
-
// Portion of the run inside the unchanged prefix.
|
|
2822
|
-
await applyRange(run.start, Math.min(run.end, prefix), inline);
|
|
2823
|
-
// Portion inside the unchanged suffix, shifted by the length delta.
|
|
2824
|
-
const sufStart = Math.max(run.start, oldTrim.length - suffix);
|
|
2825
|
-
if (run.end > sufStart)
|
|
2826
|
-
await applyRange(sufStart + delta, run.end + delta, inline);
|
|
2827
|
-
}
|
|
2828
|
-
// The CHANGED middle keeps whatever the engine smeared; give it the marks
|
|
2829
|
-
// of the run the change continues from (the one containing the last
|
|
2830
|
-
// unchanged prefix character).
|
|
2831
|
-
const middleEnd = newTrim.length - suffix;
|
|
2832
|
-
if (middleEnd > prefix) {
|
|
2833
|
-
const anchorRun = oldRuns.find((run) => prefix > run.start && prefix <= run.end) ?? oldRuns[oldRuns.length - 1];
|
|
2834
|
-
await applyRange(prefix, middleEnd, inlineFromSnapshotRun(anchorRun, normalize));
|
|
2835
|
-
}
|
|
2836
|
-
return applied > 0 ? { restoredRunRanges: applied } : null;
|
|
2837
|
-
}
|
|
2838
|
-
catch (err) {
|
|
2839
|
-
return { skipped: reasonOf(err) };
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
2742
|
const NAMED_COLORS = {
|
|
2843
2743
|
black: '000000',
|
|
2844
2744
|
blue: '0000FF',
|
|
@@ -4610,7 +4510,7 @@ function compactOpResult(result) {
|
|
|
4610
4510
|
compact.stepCount = result.steps.length;
|
|
4611
4511
|
return compact;
|
|
4612
4512
|
}
|
|
4613
|
-
function parseScopedReplaceArgs(args) {
|
|
4513
|
+
function parseScopedReplaceArgs(args, changeMode) {
|
|
4614
4514
|
if (args.action !== 'replace_text')
|
|
4615
4515
|
return null;
|
|
4616
4516
|
if (!isRecord(args.selector))
|
|
@@ -4627,7 +4527,7 @@ function parseScopedReplaceArgs(args) {
|
|
|
4627
4527
|
edits,
|
|
4628
4528
|
selector: args.selector,
|
|
4629
4529
|
caseSensitive: args.caseSensitive === true,
|
|
4630
|
-
changeMode
|
|
4530
|
+
changeMode,
|
|
4631
4531
|
};
|
|
4632
4532
|
}
|
|
4633
4533
|
async function runScopedReplaceUnchecked(doc, args) {
|
|
@@ -4723,9 +4623,8 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4723
4623
|
},
|
|
4724
4624
|
args: { replacement: { text: span.replace } },
|
|
4725
4625
|
}));
|
|
4726
|
-
//
|
|
4727
|
-
//
|
|
4728
|
-
// applies directly, so restoring marks adds no second reviewable change.
|
|
4626
|
+
// Restore the known edit spans directly so the formatting pass adds no
|
|
4627
|
+
// separate tracked change.
|
|
4729
4628
|
const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
|
|
4730
4629
|
try {
|
|
4731
4630
|
result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
|
|
@@ -4793,11 +4692,15 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4793
4692
|
};
|
|
4794
4693
|
}
|
|
4795
4694
|
async function runScopedReplace(doc, args) {
|
|
4695
|
+
const changeMode = args.changeMode ?? 'direct';
|
|
4796
4696
|
try {
|
|
4797
|
-
return
|
|
4697
|
+
return {
|
|
4698
|
+
...(await runScopedReplaceUnchecked(doc, { ...args, changeMode })),
|
|
4699
|
+
changeMode,
|
|
4700
|
+
};
|
|
4798
4701
|
}
|
|
4799
4702
|
catch (error) {
|
|
4800
|
-
return failedReceipt('replace_text', error);
|
|
4703
|
+
return { ...failedReceipt('replace_text', error), changeMode };
|
|
4801
4704
|
}
|
|
4802
4705
|
}
|
|
4803
4706
|
/**
|
|
@@ -6523,11 +6426,12 @@ async function dispatchSuperdocPerformAction(doc, args) {
|
|
|
6523
6426
|
code: 'INVALID_ARGUMENT',
|
|
6524
6427
|
});
|
|
6525
6428
|
}
|
|
6429
|
+
const changeMode = parseReplaceTextChangeMode(args.changeMode);
|
|
6526
6430
|
// Selector-scoped replaces take the span-targeted path: per-find
|
|
6527
6431
|
// text.rewrite spans inside the one selected block, so tabs/breaks/
|
|
6528
6432
|
// images outside the spans survive and the receipt reports per-edit
|
|
6529
6433
|
// applied/skipped truth.
|
|
6530
|
-
const scoped = parseScopedReplaceArgs(args);
|
|
6434
|
+
const scoped = parseScopedReplaceArgs(args, changeMode);
|
|
6531
6435
|
if (scoped) {
|
|
6532
6436
|
return runScopedReplace(doc, scoped);
|
|
6533
6437
|
}
|
|
@@ -6540,7 +6444,7 @@ async function dispatchSuperdocPerformAction(doc, args) {
|
|
|
6540
6444
|
edits,
|
|
6541
6445
|
selector,
|
|
6542
6446
|
caseSensitive: args.caseSensitive === true,
|
|
6543
|
-
changeMode
|
|
6447
|
+
changeMode,
|
|
6544
6448
|
});
|
|
6545
6449
|
}
|
|
6546
6450
|
case 'delete_text': {
|
|
@@ -7170,6 +7074,17 @@ function parseChangeMode(value) {
|
|
|
7170
7074
|
return value;
|
|
7171
7075
|
return undefined;
|
|
7172
7076
|
}
|
|
7077
|
+
function parseReplaceTextChangeMode(value) {
|
|
7078
|
+
if (value === undefined)
|
|
7079
|
+
return 'direct';
|
|
7080
|
+
const changeMode = parseChangeMode(value);
|
|
7081
|
+
if (changeMode)
|
|
7082
|
+
return changeMode;
|
|
7083
|
+
throw new SuperDocCliError('replace_text: "changeMode" must be "direct" or "tracked"', {
|
|
7084
|
+
code: 'INVALID_ARGUMENT',
|
|
7085
|
+
details: { changeMode: value },
|
|
7086
|
+
});
|
|
7087
|
+
}
|
|
7173
7088
|
function parseTrackedChangeKind(value) {
|
|
7174
7089
|
if (value === 'insert' || value === 'delete' || value === 'replacement' || value === 'format')
|
|
7175
7090
|
return value;
|