@superdoc/sdk 2.13.0-next.1 → 2.13.0-next.11

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.
@@ -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; the block's intra-paragraph run pattern is preserved best-effort over the unchanged prefix/suffix. For appending a NEW paragraph/clause use insert_paragraphs, and for small wording edits prefer replace_text with a minimal find span",
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?',
@@ -1149,11 +1149,6 @@ async function runReplaceTextUnchecked(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 runReplaceTextUnchecked(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 runReplaceTextUnchecked(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 runReplaceTextUnchecked(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 runReplaceTextUnchecked(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
  };
@@ -2590,7 +2568,6 @@ async function runRewriteBlock(doc, args) {
2590
2568
  throw new Error('selector did not resolve to a body block');
2591
2569
  context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
2592
2570
  const text = normalizeTitleLikeRewriteText(target.text, args.text);
2593
- const runs = await captureBlockRuns(doc, target.nodeId, target.text);
2594
2571
  await executeMutations(context.document, [
2595
2572
  {
2596
2573
  id: 'rewrite-block-1',
@@ -2599,7 +2576,6 @@ async function runRewriteBlock(doc, args) {
2599
2576
  args: { replacement: { text }, style: preserveRewriteStyle() },
2600
2577
  },
2601
2578
  ], args.changeMode, pre.revision);
2602
- await preserveRunPatternAfterRewrite(context.document, target.nodeId, target.text, runs, text);
2603
2579
  const post = await context.finish();
2604
2580
  const rewritten = await post.block(target.nodeId);
2605
2581
  const verification = [
@@ -2672,23 +2648,8 @@ async function captureBlockRuns(doc, nodeId, text) {
2672
2648
  }
2673
2649
  }
2674
2650
  /**
2675
- * Re-apply a block's pre-rewrite run pattern after a whole-block text.rewrite.
2676
- * The engine renders the rewritten block as a single uniform run, which both
2677
- * drops intra-paragraph formatting AND can smear the first run's marks over
2678
- * everything. Marks are restored positionally over the parts of the text the
2679
- * rewrite did NOT change (the common prefix and suffix of old vs new text),
2680
- * and the changed middle takes the marks of the run it continues from —
2681
- * Word's "typing continues formatting" rule. Best-effort: never fails the
2682
- * rewrite.
2683
- */
2684
- /**
2685
- * Span-aware run-pattern restore for scoped replaces. The generic
2686
- * preserveRunPatternAfterRewrite diffs prefix/suffix and treats everything
2687
- * between the FIRST and LAST change as one modified middle — with two
2688
- * separated replacements that flattens independently-styled runs between
2689
- * them. Here the exact spans are known, so every untouched interval keeps its
2690
- * original run formatting at the offset-mapped position, and each replacement
2691
- * 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.
2692
2653
  * `spans` are FULL-TEXT coordinates, ascending, non-overlapping.
2693
2654
  */
2694
2655
  async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
@@ -2781,74 +2742,6 @@ async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
2781
2742
  return { skipped: reasonOf(err) };
2782
2743
  }
2783
2744
  }
2784
- async function preserveRunPatternAfterRewrite(doc, nodeId, oldText, oldRuns, newText) {
2785
- // Nothing to restore when the block was uniformly plain; a single STYLED run
2786
- // still needs restoring (whole-block replaces drop even uniform marks).
2787
- if (!oldRuns || oldRuns.length === 0)
2788
- return null;
2789
- if (!oldRuns.some((run) => Object.keys(inlineFromSnapshotRun(run, new Set())).length > 0))
2790
- return null;
2791
- const formatFn = maybeMethod(doc, ['format', 'apply']);
2792
- if (!formatFn)
2793
- return null;
2794
- // Boolean marks any old run carried: plain ranges must explicitly clear
2795
- // these, because the rewritten single run may have smeared them everywhere.
2796
- const normalize = new Set(RUN_BOOL_MARKS.filter((key) => oldRuns.some((run) => run[key])));
2797
- // query.match run offsets are relative to the trimmed text used to match.
2798
- const oldTrim = oldText.trim();
2799
- const newTrimStart = newText.length - newText.trimStart().length;
2800
- const newTrim = newText.trim();
2801
- let prefix = 0;
2802
- const maxCommon = Math.min(oldTrim.length, newTrim.length);
2803
- while (prefix < maxCommon && oldTrim[prefix] === newTrim[prefix])
2804
- prefix += 1;
2805
- let suffix = 0;
2806
- while (suffix < maxCommon - prefix && oldTrim[oldTrim.length - 1 - suffix] === newTrim[newTrim.length - 1 - suffix])
2807
- suffix += 1;
2808
- const delta = newTrim.length - oldTrim.length;
2809
- let applied = 0;
2810
- const applyRange = async (start, end, inline) => {
2811
- if (end <= start || Object.keys(inline).length === 0)
2812
- return;
2813
- // Always direct, even under tracked mode. This pass RESTORES the marks the
2814
- // rewrite flattened; it is not new authorial formatting, and the text
2815
- // revision already owns the span's accept/reject fate. Applying it tracked
2816
- // adds a second reviewable change per rewrite (SD-4309's conformance matrix
2817
- // counts one per action), and rejecting that change alone would strand the
2818
- // text in the flattened look the rewrite produced.
2819
- const changed = await applyInlineQuiet(formatFn, {
2820
- blockId: nodeId,
2821
- start: newTrimStart + start,
2822
- end: newTrimStart + end,
2823
- inline,
2824
- });
2825
- if (changed)
2826
- applied += 1;
2827
- };
2828
- try {
2829
- for (const run of oldRuns) {
2830
- const inline = inlineFromSnapshotRun(run, normalize);
2831
- // Portion of the run inside the unchanged prefix.
2832
- await applyRange(run.start, Math.min(run.end, prefix), inline);
2833
- // Portion inside the unchanged suffix, shifted by the length delta.
2834
- const sufStart = Math.max(run.start, oldTrim.length - suffix);
2835
- if (run.end > sufStart)
2836
- await applyRange(sufStart + delta, run.end + delta, inline);
2837
- }
2838
- // The CHANGED middle keeps whatever the engine smeared; give it the marks
2839
- // of the run the change continues from (the one containing the last
2840
- // unchanged prefix character).
2841
- const middleEnd = newTrim.length - suffix;
2842
- if (middleEnd > prefix) {
2843
- const anchorRun = oldRuns.find((run) => prefix > run.start && prefix <= run.end) ?? oldRuns[oldRuns.length - 1];
2844
- await applyRange(prefix, middleEnd, inlineFromSnapshotRun(anchorRun, normalize));
2845
- }
2846
- return applied > 0 ? { restoredRunRanges: applied } : null;
2847
- }
2848
- catch (err) {
2849
- return { skipped: reasonOf(err) };
2850
- }
2851
- }
2852
2745
  const NAMED_COLORS = {
2853
2746
  black: '000000',
2854
2747
  blue: '0000FF',
@@ -4733,9 +4626,8 @@ async function runScopedReplaceUnchecked(doc, args) {
4733
4626
  },
4734
4627
  args: { replacement: { text: span.replace } },
4735
4628
  }));
4736
- // Select rewrites flatten the block's runs, so the tracked lane needs the
4737
- // same restoration the whole-block lane below performs. The preserve pass
4738
- // 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.
4739
4631
  const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
4740
4632
  try {
4741
4633
  result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
@@ -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; the block's intra-paragraph run pattern is preserved best-effort over the unchanged prefix/suffix. For appending a NEW paragraph/clause use insert_paragraphs, and for small wording edits prefer replace_text with a minimal find span",
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?',
@@ -1146,11 +1146,6 @@ async function runReplaceTextUnchecked(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 runReplaceTextUnchecked(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 runReplaceTextUnchecked(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 runReplaceTextUnchecked(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 runReplaceTextUnchecked(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
  };
@@ -2587,7 +2565,6 @@ async function runRewriteBlock(doc, args) {
2587
2565
  throw new Error('selector did not resolve to a body block');
2588
2566
  context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
2589
2567
  const text = normalizeTitleLikeRewriteText(target.text, args.text);
2590
- const runs = await captureBlockRuns(doc, target.nodeId, target.text);
2591
2568
  await executeMutations(context.document, [
2592
2569
  {
2593
2570
  id: 'rewrite-block-1',
@@ -2596,7 +2573,6 @@ async function runRewriteBlock(doc, args) {
2596
2573
  args: { replacement: { text }, style: preserveRewriteStyle() },
2597
2574
  },
2598
2575
  ], args.changeMode, pre.revision);
2599
- await preserveRunPatternAfterRewrite(context.document, target.nodeId, target.text, runs, text);
2600
2576
  const post = await context.finish();
2601
2577
  const rewritten = await post.block(target.nodeId);
2602
2578
  const verification = [
@@ -2669,23 +2645,8 @@ async function captureBlockRuns(doc, nodeId, text) {
2669
2645
  }
2670
2646
  }
2671
2647
  /**
2672
- * Re-apply a block's pre-rewrite run pattern after a whole-block text.rewrite.
2673
- * The engine renders the rewritten block as a single uniform run, which both
2674
- * drops intra-paragraph formatting AND can smear the first run's marks over
2675
- * everything. Marks are restored positionally over the parts of the text the
2676
- * rewrite did NOT change (the common prefix and suffix of old vs new text),
2677
- * and the changed middle takes the marks of the run it continues from —
2678
- * Word's "typing continues formatting" rule. Best-effort: never fails the
2679
- * rewrite.
2680
- */
2681
- /**
2682
- * Span-aware run-pattern restore for scoped replaces. The generic
2683
- * preserveRunPatternAfterRewrite diffs prefix/suffix and treats everything
2684
- * between the FIRST and LAST change as one modified middle — with two
2685
- * separated replacements that flattens independently-styled runs between
2686
- * them. Here the exact spans are known, so every untouched interval keeps its
2687
- * original run formatting at the offset-mapped position, and each replacement
2688
- * 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.
2689
2650
  * `spans` are FULL-TEXT coordinates, ascending, non-overlapping.
2690
2651
  */
2691
2652
  async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
@@ -2778,74 +2739,6 @@ async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
2778
2739
  return { skipped: reasonOf(err) };
2779
2740
  }
2780
2741
  }
2781
- async function preserveRunPatternAfterRewrite(doc, nodeId, oldText, oldRuns, newText) {
2782
- // Nothing to restore when the block was uniformly plain; a single STYLED run
2783
- // still needs restoring (whole-block replaces drop even uniform marks).
2784
- if (!oldRuns || oldRuns.length === 0)
2785
- return null;
2786
- if (!oldRuns.some((run) => Object.keys(inlineFromSnapshotRun(run, new Set())).length > 0))
2787
- return null;
2788
- const formatFn = maybeMethod(doc, ['format', 'apply']);
2789
- if (!formatFn)
2790
- return null;
2791
- // Boolean marks any old run carried: plain ranges must explicitly clear
2792
- // these, because the rewritten single run may have smeared them everywhere.
2793
- const normalize = new Set(RUN_BOOL_MARKS.filter((key) => oldRuns.some((run) => run[key])));
2794
- // query.match run offsets are relative to the trimmed text used to match.
2795
- const oldTrim = oldText.trim();
2796
- const newTrimStart = newText.length - newText.trimStart().length;
2797
- const newTrim = newText.trim();
2798
- let prefix = 0;
2799
- const maxCommon = Math.min(oldTrim.length, newTrim.length);
2800
- while (prefix < maxCommon && oldTrim[prefix] === newTrim[prefix])
2801
- prefix += 1;
2802
- let suffix = 0;
2803
- while (suffix < maxCommon - prefix && oldTrim[oldTrim.length - 1 - suffix] === newTrim[newTrim.length - 1 - suffix])
2804
- suffix += 1;
2805
- const delta = newTrim.length - oldTrim.length;
2806
- let applied = 0;
2807
- const applyRange = async (start, end, inline) => {
2808
- if (end <= start || Object.keys(inline).length === 0)
2809
- return;
2810
- // Always direct, even under tracked mode. This pass RESTORES the marks the
2811
- // rewrite flattened; it is not new authorial formatting, and the text
2812
- // revision already owns the span's accept/reject fate. Applying it tracked
2813
- // adds a second reviewable change per rewrite (SD-4309's conformance matrix
2814
- // counts one per action), and rejecting that change alone would strand the
2815
- // text in the flattened look the rewrite produced.
2816
- const changed = await applyInlineQuiet(formatFn, {
2817
- blockId: nodeId,
2818
- start: newTrimStart + start,
2819
- end: newTrimStart + end,
2820
- inline,
2821
- });
2822
- if (changed)
2823
- applied += 1;
2824
- };
2825
- try {
2826
- for (const run of oldRuns) {
2827
- const inline = inlineFromSnapshotRun(run, normalize);
2828
- // Portion of the run inside the unchanged prefix.
2829
- await applyRange(run.start, Math.min(run.end, prefix), inline);
2830
- // Portion inside the unchanged suffix, shifted by the length delta.
2831
- const sufStart = Math.max(run.start, oldTrim.length - suffix);
2832
- if (run.end > sufStart)
2833
- await applyRange(sufStart + delta, run.end + delta, inline);
2834
- }
2835
- // The CHANGED middle keeps whatever the engine smeared; give it the marks
2836
- // of the run the change continues from (the one containing the last
2837
- // unchanged prefix character).
2838
- const middleEnd = newTrim.length - suffix;
2839
- if (middleEnd > prefix) {
2840
- const anchorRun = oldRuns.find((run) => prefix > run.start && prefix <= run.end) ?? oldRuns[oldRuns.length - 1];
2841
- await applyRange(prefix, middleEnd, inlineFromSnapshotRun(anchorRun, normalize));
2842
- }
2843
- return applied > 0 ? { restoredRunRanges: applied } : null;
2844
- }
2845
- catch (err) {
2846
- return { skipped: reasonOf(err) };
2847
- }
2848
- }
2849
2742
  const NAMED_COLORS = {
2850
2743
  black: '000000',
2851
2744
  blue: '0000FF',
@@ -4730,9 +4623,8 @@ async function runScopedReplaceUnchecked(doc, args) {
4730
4623
  },
4731
4624
  args: { replacement: { text: span.replace } },
4732
4625
  }));
4733
- // Select rewrites flatten the block's runs, so the tracked lane needs the
4734
- // same restoration the whole-block lane below performs. The preserve pass
4735
- // 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.
4736
4628
  const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
4737
4629
  try {
4738
4630
  result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
@@ -345,6 +345,12 @@ function createDocApi(runtime) {
345
345
  delete: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.headerFooters.parts.delete"], params, options),
346
346
  },
347
347
  },
348
+ watermarks: {
349
+ list: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.watermarks.list"], params, options),
350
+ insert: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.watermarks.insert"], params, options),
351
+ replace: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.watermarks.replace"], params, options),
352
+ remove: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.watermarks.remove"], params, options),
353
+ },
348
354
  contentControls: {
349
355
  list: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.contentControls.list"], params, options),
350
356
  get: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.contentControls.get"], params, options),
@@ -550,6 +556,7 @@ function createDocApi(runtime) {
550
556
  insertLineBreak: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.insertLineBreak"], params, options),
551
557
  executeCode: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.executeCode"], params, options),
552
558
  status: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.status"], params, options),
559
+ discover: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.discover"], params, options),
553
560
  describe: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.describe"], params, options),
554
561
  describeCommand: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.describeCommand"], params, options),
555
562
  session: {
@@ -909,6 +916,12 @@ function createBoundDocApi(runtime) {
909
916
  delete: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.headerFooters.parts.delete"], params, options),
910
917
  },
911
918
  },
919
+ watermarks: {
920
+ list: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.watermarks.list"], params, options),
921
+ insert: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.watermarks.insert"], params, options),
922
+ replace: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.watermarks.replace"], params, options),
923
+ remove: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.watermarks.remove"], params, options),
924
+ },
912
925
  contentControls: {
913
926
  list: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.contentControls.list"], params, options),
914
927
  get: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.contentControls.get"], params, options),