@superdoc/sdk 2.9.1-next.2 → 2.10.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/README.md +45 -0
- package/dist/action-primitives/doc-index.cjs +6 -4
- package/dist/action-primitives/doc-index.js +6 -4
- package/dist/action-primitives/engine.cjs +6 -2
- package/dist/action-primitives/engine.js +6 -2
- package/dist/action-primitives/receipt.d.ts +4 -0
- package/dist/action-primitives/tools/structure-insert.d.ts +1 -1
- package/dist/agent/actions.cjs +384 -373
- package/dist/agent/actions.d.ts +5 -0
- package/dist/agent/actions.js +385 -374
- package/dist/agent/catalog.cjs +15 -0
- package/dist/agent/catalog.js +15 -0
- package/dist/agent/doc-snapshot.cjs +205 -86
- package/dist/agent/doc-snapshot.d.ts +11 -0
- package/dist/agent/doc-snapshot.js +203 -86
- package/dist/agent/execution-context.cjs +385 -0
- package/dist/agent/execution-context.d.ts +97 -0
- package/dist/agent/execution-context.js +376 -0
- package/dist/agent/runtime.cjs +123 -117
- package/dist/agent/runtime.d.ts +3 -0
- package/dist/agent/runtime.js +124 -118
- package/dist/agent/v2-preset-compat.cjs +5 -1
- package/dist/agent/v2-preset-compat.js +4 -1
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.cjs +2 -0
- package/dist/generated/client.d.ts +85 -0
- package/dist/generated/client.js +2 -0
- package/dist/generated/contract.cjs +1796 -1297
- package/dist/generated/contract.js +1797 -1297
- package/dist/index.cjs +23 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +23 -0
- package/dist/presets/core.cjs +1 -1
- package/dist/presets/core.js +1 -1
- package/dist/runtime/document-evidence.cjs +40 -0
- package/dist/runtime/document-evidence.d.ts +13 -0
- package/dist/runtime/document-evidence.js +30 -0
- package/dist/runtime/document-rpc.cjs +27 -0
- package/dist/runtime/document-rpc.d.ts +2 -0
- package/dist/runtime/document-rpc.js +25 -0
- package/dist/runtime/host.cjs +65 -4
- package/dist/runtime/host.d.ts +3 -0
- package/dist/runtime/host.js +66 -5
- package/dist/runtime/process.cjs +38 -0
- package/dist/runtime/process.d.ts +16 -0
- package/dist/runtime/process.js +38 -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 +10 -8
- package/tools/catalog.json +89 -0
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +89 -0
- package/tools/tools.generic.json +89 -0
- package/tools/tools.openai.json +89 -0
- package/tools/tools.vercel.json +89 -0
package/dist/agent/actions.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { ExecutionContext, evaluateFactChecks } from './execution-context.js';
|
|
1
2
|
import { SuperDocCliError } from '../runtime/errors.js';
|
|
2
3
|
import { runSuperdocListTransformWorkflow } from '../action-primitives/tools/list-transform.js';
|
|
3
4
|
import { runSuperdocStructureInsertWorkflow } from '../action-primitives/tools/structure-insert.js';
|
|
4
5
|
import { runSuperdocTextTransformWorkflow } from '../action-primitives/tools/text-transform.js';
|
|
5
|
-
import { buildDocumentSnapshot, matchRunsForBlock, resolveSnapshotSelector, } from './doc-snapshot.js';
|
|
6
|
+
import { buildDocumentSnapshot, buildMutationSnapshot, matchRunsForBlock, MutationSnapshotError, resolveSnapshotSelector, } from './doc-snapshot.js';
|
|
6
7
|
const ACTION_NAMES = [
|
|
7
8
|
'insert_paragraphs',
|
|
8
9
|
'insert_heading',
|
|
@@ -167,18 +168,18 @@ export const ACTION_GROUPS = [
|
|
|
167
168
|
* action at compile time.
|
|
168
169
|
*/
|
|
169
170
|
export const ACTION_ARGS = {
|
|
170
|
-
insert_paragraphs: ['text', 'texts', 'headingLevel', 'placement', 'changeMode'],
|
|
171
|
+
insert_paragraphs: ['text', 'texts', 'headingLevel', 'placement', 'changeMode', 'evidence'],
|
|
171
172
|
insert_heading: ['text', 'level', 'placement', 'changeMode'],
|
|
172
173
|
replace_text: ['edits', 'selector', 'caseSensitive', 'changeMode'],
|
|
173
174
|
delete_text: ['finds', 'selector', 'caseSensitive', 'changeMode'],
|
|
174
175
|
delete_blocks: ['selectors', 'selector', 'changeMode'],
|
|
175
176
|
append_list: ['items', 'kind', 'headingText', 'headingLevel', 'placement', 'changeMode'],
|
|
176
|
-
create_table: ['rows', 'columns', 'cellTexts', 'placement', 'changeMode'],
|
|
177
|
+
create_table: ['rows', 'columns', 'cellTexts', 'placement', 'changeMode', 'evidence'],
|
|
177
178
|
comment_paragraphs: ['commentText', 'scope', 'excludeBlockQuotes'],
|
|
178
179
|
add_comments: ['commentText', 'selector', 'selectors'],
|
|
179
180
|
resolve_comments: ['anchorText', 'reopen'],
|
|
180
181
|
reply_to_comment: ['commentText', 'anchorText', 'commentId'],
|
|
181
|
-
rewrite_block: ['text', 'selector', 'changeMode'],
|
|
182
|
+
rewrite_block: ['text', 'selector', 'changeMode', 'evidence'],
|
|
182
183
|
accept_tracked_changes: ['author', 'changeType'],
|
|
183
184
|
reject_tracked_changes: ['author', 'changeType'],
|
|
184
185
|
normalize_body_font_size: ['fontSize', 'changeMode'],
|
|
@@ -213,7 +214,7 @@ export const ACTION_ARGS = {
|
|
|
213
214
|
'changeMode',
|
|
214
215
|
],
|
|
215
216
|
apply_style: ['selector', 'styleId', 'headingLevel', 'likeText'],
|
|
216
|
-
format_paragraph: ['selector', 'alignment', 'changeMode'],
|
|
217
|
+
format_paragraph: ['selector', 'alignment', 'changeMode', 'evidence'],
|
|
217
218
|
move_text: ['text', 'afterText', 'changeMode'],
|
|
218
219
|
style_table: ['tableOrdinal', 'accentColor'],
|
|
219
220
|
move_table: ['tableOrdinal', 'placement'],
|
|
@@ -316,10 +317,6 @@ function findSnapshotTextByNodeId(snapshot, nodeId) {
|
|
|
316
317
|
}
|
|
317
318
|
return null;
|
|
318
319
|
}
|
|
319
|
-
function lastBlock(snapshot) {
|
|
320
|
-
const block = snapshot.blocks[snapshot.blocks.length - 1];
|
|
321
|
-
return block ? { nodeId: block.nodeId, nodeType: block.nodeType } : null;
|
|
322
|
-
}
|
|
323
320
|
function createdBlockTarget(result) {
|
|
324
321
|
const rec = asRecord(result);
|
|
325
322
|
const paragraph = asRecord(rec?.paragraph);
|
|
@@ -511,6 +508,8 @@ function revisionVerification(preRevision, postRevision, expectChanged) {
|
|
|
511
508
|
}
|
|
512
509
|
function failedReceipt(intent, err, preSnapshot) {
|
|
513
510
|
const message = reasonOf(err);
|
|
511
|
+
const snapshotError = err instanceof MutationSnapshotError ? err : null;
|
|
512
|
+
const errorCode = snapshotError?.code ?? asString(asRecord(err)?.code) ?? 'ACTION_FAILED';
|
|
514
513
|
return {
|
|
515
514
|
status: 'failed',
|
|
516
515
|
intent,
|
|
@@ -520,7 +519,13 @@ function failedReceipt(intent, err, preSnapshot) {
|
|
|
520
519
|
selectedTargets: [],
|
|
521
520
|
executedOperations: [],
|
|
522
521
|
verification: [],
|
|
523
|
-
errors: [
|
|
522
|
+
errors: [
|
|
523
|
+
{
|
|
524
|
+
code: errorCode,
|
|
525
|
+
message,
|
|
526
|
+
...(snapshotError || errorCode === 'REVISION_CONFLICT' ? { recovery: { kind: 'reinspect' } } : {}),
|
|
527
|
+
},
|
|
528
|
+
],
|
|
524
529
|
};
|
|
525
530
|
}
|
|
526
531
|
async function receiptFromWorkflowResult(doc, intent, pre, workflowResult, selectedTargets = [], checks = [{ kind: 'revision-changed' }]) {
|
|
@@ -564,14 +569,16 @@ async function receiptFromWorkflowResult(doc, intent, pre, workflowResult, selec
|
|
|
564
569
|
}
|
|
565
570
|
function buildFullBlockTextTarget(snapshot, blockId) {
|
|
566
571
|
const block = snapshot.blocks.find((entry) => entry.nodeId === blockId);
|
|
567
|
-
|
|
572
|
+
const cell = block ? null : snapshot.tables.flatMap((table) => table.cells).find((entry) => entry.nodeId === blockId);
|
|
573
|
+
const text = block?.text ?? cell?.text;
|
|
574
|
+
if (text == null)
|
|
568
575
|
return null;
|
|
569
576
|
return {
|
|
570
577
|
kind: 'text',
|
|
571
578
|
blockId,
|
|
572
579
|
range: {
|
|
573
580
|
start: 0,
|
|
574
|
-
end:
|
|
581
|
+
end: text.length,
|
|
575
582
|
},
|
|
576
583
|
};
|
|
577
584
|
}
|
|
@@ -624,8 +631,8 @@ function evaluateChecks(pre, post, checks) {
|
|
|
624
631
|
else if (check.kind === 'comment-count-delta') {
|
|
625
632
|
results.push({
|
|
626
633
|
check,
|
|
627
|
-
passed: post.comments
|
|
628
|
-
detail: `pre=${pre.comments
|
|
634
|
+
passed: post.counts.comments - pre.counts.comments === check.delta,
|
|
635
|
+
detail: `pre=${pre.counts.comments} post=${post.counts.comments}`,
|
|
629
636
|
});
|
|
630
637
|
}
|
|
631
638
|
else if (check.kind === 'tracked-change-count-delta') {
|
|
@@ -772,13 +779,28 @@ async function executeCreateHeading(doc, text, level, placement, changeMode) {
|
|
|
772
779
|
// Dual dialect — see executeCreateParagraph.
|
|
773
780
|
return fn(params, changeMode ? { changeMode } : undefined);
|
|
774
781
|
}
|
|
775
|
-
async function executeMutations(doc, steps, changeMode) {
|
|
782
|
+
async function executeMutations(doc, steps, changeMode, expectedRevision) {
|
|
776
783
|
const fn = maybeMethod(doc, ['mutations', 'apply']);
|
|
777
784
|
if (!fn)
|
|
778
785
|
throw new SuperDocCliError('doc.mutations.apply is not available on the document handle.', {
|
|
779
786
|
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
780
787
|
});
|
|
781
|
-
return fn({
|
|
788
|
+
return fn({
|
|
789
|
+
atomic: true,
|
|
790
|
+
changeMode: changeMode ?? 'direct',
|
|
791
|
+
steps,
|
|
792
|
+
...(expectedRevision ? { expectedRevision } : {}),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
async function previewMutations(doc, steps, changeMode, expectedRevision) {
|
|
796
|
+
const fn = maybeMethod(doc, ['mutations', 'preview']);
|
|
797
|
+
if (!fn) {
|
|
798
|
+
throw new SuperDocCliError('doc.mutations.preview is not available on the document handle.', {
|
|
799
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
const result = await fn({ atomic: true, changeMode: changeMode ?? 'direct', expectedRevision, steps });
|
|
803
|
+
return isRecord(result) ? result : {};
|
|
782
804
|
}
|
|
783
805
|
async function executeCreateTable(doc, args, placement) {
|
|
784
806
|
const fn = maybeMethod(doc, ['create', 'table']);
|
|
@@ -832,7 +854,7 @@ forceDirectFill = false) {
|
|
|
832
854
|
});
|
|
833
855
|
}
|
|
834
856
|
const extracted = asRecord(await extractFn({}));
|
|
835
|
-
const blocks = (Array.isArray(extracted?.blocks) ? extracted
|
|
857
|
+
const blocks = (Array.isArray(extracted?.blocks) ? extracted.blocks : [])
|
|
836
858
|
.map((block) => asRecord(block))
|
|
837
859
|
.filter((block) => block != null)
|
|
838
860
|
.flatMap((block) => {
|
|
@@ -1021,97 +1043,74 @@ async function styleTableCells(doc, cells, changeMode) {
|
|
|
1021
1043
|
await applyItemStyling(doc, nodeIds, specs, changeMode);
|
|
1022
1044
|
return styled.length;
|
|
1023
1045
|
}
|
|
1024
|
-
async function runInsertParagraphs(doc, args) {
|
|
1025
|
-
const
|
|
1046
|
+
async function runInsertParagraphs(doc, args, formatArgs) {
|
|
1047
|
+
const context = new ExecutionContext(doc, { evidence: args.evidence, selectors: placementSelectors(args.placement) });
|
|
1026
1048
|
try {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
// For subsequent items: append after the previously inserted block by
|
|
1038
|
-
// using `documentEnd` (the SDK keeps blocks contiguous) so order is
|
|
1039
|
-
// preserved.
|
|
1049
|
+
const pre = await context.start();
|
|
1050
|
+
const placement = await resolveFactPlacement(args.placement, pre);
|
|
1051
|
+
const isNewDocument = (await pre.count()) <= 1;
|
|
1052
|
+
const anchor = placement.kind === 'before' || placement.kind === 'after'
|
|
1053
|
+
? await pre.block(placement.target.nodeId)
|
|
1054
|
+
: await pre.target({
|
|
1055
|
+
kind: 'placement',
|
|
1056
|
+
at: placement.kind === 'documentStart' ? 'document_start' : 'document_end',
|
|
1057
|
+
});
|
|
1058
|
+
const preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
|
|
1040
1059
|
let currentPlacement = placement;
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
return
|
|
1060
|
+
const nodeIds = [];
|
|
1061
|
+
const specs = [];
|
|
1062
|
+
const advance = async (result) => {
|
|
1063
|
+
const target = createdBlockTarget(result);
|
|
1064
|
+
if (target) {
|
|
1065
|
+
currentPlacement = {
|
|
1066
|
+
kind: 'after',
|
|
1067
|
+
target: { kind: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
|
|
1068
|
+
};
|
|
1069
|
+
return target.nodeId;
|
|
1051
1070
|
}
|
|
1052
|
-
const
|
|
1053
|
-
const last =
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1071
|
+
const current = await context.finishRevision();
|
|
1072
|
+
const last = await current.target({ kind: 'placement', at: 'document_end' });
|
|
1073
|
+
if (!last)
|
|
1074
|
+
throw new Error('Created paragraph identity is unavailable.');
|
|
1075
|
+
currentPlacement = { kind: 'after', target: { kind: 'block', nodeType: last.nodeType, nodeId: last.nodeId } };
|
|
1076
|
+
return last.nodeId;
|
|
1057
1077
|
};
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
currentPlacement = await advanceAfter(result);
|
|
1081
|
-
// New-document drafts get a blank paragraph after each item for spacing;
|
|
1082
|
-
// inserts into an existing document do not (would scatter blank lines).
|
|
1083
|
-
if (isNewDocument) {
|
|
1084
|
-
const spacer = await executeCreateParagraph(doc, '', currentPlacement, args.changeMode);
|
|
1085
|
-
executedOperations.push({ operationId: 'doc.create.paragraph', result: spacer, rationale: 'spacing' });
|
|
1086
|
-
currentPlacement = await advanceAfter(spacer);
|
|
1078
|
+
const texts = args.texts ?? (args.text ? [args.text] : []);
|
|
1079
|
+
for (let i = 0; i < texts.length; i++) {
|
|
1080
|
+
const result = i === 0 && args.headingLevel
|
|
1081
|
+
? await executeCreateHeading(context.document, texts[i], args.headingLevel, currentPlacement, args.changeMode)
|
|
1082
|
+
: await executeCreateParagraph(context.document, texts[i], currentPlacement, args.changeMode);
|
|
1083
|
+
const id = await advance(result);
|
|
1084
|
+
nodeIds.push(id);
|
|
1085
|
+
specs.push({ ...args.textSpecs?.[i], text: texts[i] });
|
|
1086
|
+
if (isNewDocument)
|
|
1087
|
+
await advance(await executeCreateParagraph(context.document, '', currentPlacement, args.changeMode));
|
|
1088
|
+
}
|
|
1089
|
+
if (specs.some((spec) => spec.runs?.length || spec.marks))
|
|
1090
|
+
await applyItemStyling(context.document, nodeIds, specs, args.changeMode);
|
|
1091
|
+
let post = await context.finishRevision();
|
|
1092
|
+
let formatted;
|
|
1093
|
+
if (formatArgs) {
|
|
1094
|
+
let localRows;
|
|
1095
|
+
if (!pre.complete) {
|
|
1096
|
+
const created = await post.page({ nodeIds, limit: Math.max(nodeIds.length, 1) });
|
|
1097
|
+
localRows = [...(anchor ? [anchor] : []), ...created].sort((a, b) => a.ordinal - b.ordinal);
|
|
1087
1098
|
}
|
|
1099
|
+
const count = context.executedOperations.length;
|
|
1100
|
+
formatted = await matchInsertedBlockFormatting(context.document, preRows, context.receipt('insert_paragraphs', []), formatArgs, localRows);
|
|
1101
|
+
if (context.executedOperations.length !== count)
|
|
1102
|
+
post = await context.finishRevision();
|
|
1088
1103
|
}
|
|
1089
|
-
|
|
1090
|
-
const styling = await applyItemStyling(doc, styledNodeIds, styledSpecs, args.changeMode);
|
|
1091
|
-
executedOperations.push({
|
|
1092
|
-
operationId: 'doc.format.apply',
|
|
1093
|
-
result: styling,
|
|
1094
|
-
rationale: `Styled ${styledNodeIds.length} inserted paragraph(s)`,
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
const postIdentity = await readDocumentIdentity(doc);
|
|
1098
|
-
const verification = [revisionVerification(pre.revision, postIdentity.revision, true)];
|
|
1099
|
-
return {
|
|
1100
|
-
status: verification.every((v) => v.passed) ? 'ok' : 'failed',
|
|
1101
|
-
intent: 'insert_paragraphs',
|
|
1102
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1103
|
-
postSnapshot: postIdentity,
|
|
1104
|
-
selectedTargets: [],
|
|
1105
|
-
executedOperations,
|
|
1106
|
-
verification,
|
|
1107
|
-
};
|
|
1104
|
+
return context.receipt('insert_paragraphs', await evaluateFactChecks(pre, post, [{ kind: 'revision-changed' }]), formatted?.contextualFormatting ? { contextualFormatting: formatted.contextualFormatting } : {});
|
|
1108
1105
|
}
|
|
1109
|
-
catch (
|
|
1110
|
-
return
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
return context.failure('insert_paragraphs', error);
|
|
1111
1108
|
}
|
|
1112
1109
|
}
|
|
1113
1110
|
async function runInsertHeading(doc, args) {
|
|
1114
|
-
const pre =
|
|
1111
|
+
const pre = args.placement?.at === 'before' || args.placement?.at === 'after'
|
|
1112
|
+
? await buildMutationSnapshot(doc, { includeDomains: ['blocks'] })
|
|
1113
|
+
: await buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
|
|
1115
1114
|
try {
|
|
1116
1115
|
const placement = resolvePlacement(args.placement, pre);
|
|
1117
1116
|
const result = await executeCreateHeading(doc, args.text, args.level, placement, args.changeMode);
|
|
@@ -1133,10 +1132,10 @@ async function runInsertHeading(doc, args) {
|
|
|
1133
1132
|
}
|
|
1134
1133
|
async function runReplaceText(doc, args) {
|
|
1135
1134
|
const selectorDomains = args.selector ? snapshotDomainsForSelector(args.selector) : null;
|
|
1136
|
-
const requiresBlockSnapshot = args.selector != null
|
|
1135
|
+
const requiresBlockSnapshot = args.selector != null;
|
|
1137
1136
|
const preIdentity = requiresBlockSnapshot ? null : await readDocumentIdentity(doc);
|
|
1138
1137
|
const pre = requiresBlockSnapshot
|
|
1139
|
-
? await
|
|
1138
|
+
? await buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
|
|
1140
1139
|
: snapshotFromIdentity(preIdentity);
|
|
1141
1140
|
try {
|
|
1142
1141
|
if (args.edits.length === 0) {
|
|
@@ -1146,6 +1145,7 @@ async function runReplaceText(doc, args) {
|
|
|
1146
1145
|
const selectedTargets = [];
|
|
1147
1146
|
let skippedEdits = [];
|
|
1148
1147
|
let steps;
|
|
1148
|
+
let applyExpectedRevision;
|
|
1149
1149
|
// Blocks whose text this call rewrites, with their pre-mutation run layout:
|
|
1150
1150
|
// the engine replace lane renders the replaced range as uniform runs, so a
|
|
1151
1151
|
// whole-paragraph find/replace flattens intra-paragraph formatting (bold
|
|
@@ -1187,6 +1187,7 @@ async function runReplaceText(doc, args) {
|
|
|
1187
1187
|
},
|
|
1188
1188
|
},
|
|
1189
1189
|
];
|
|
1190
|
+
applyExpectedRevision = pre.revision;
|
|
1190
1191
|
selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
1191
1192
|
preserveTargets.push({
|
|
1192
1193
|
nodeId: target.nodeId,
|
|
@@ -1197,98 +1198,9 @@ async function runReplaceText(doc, args) {
|
|
|
1197
1198
|
});
|
|
1198
1199
|
}
|
|
1199
1200
|
else {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
:
|
|
1203
|
-
skippedEdits = args.edits.filter((edit) => !matchingEdits.includes(edit));
|
|
1204
|
-
if (matchingEdits.length === 0) {
|
|
1205
|
-
return {
|
|
1206
|
-
status: 'failed',
|
|
1207
|
-
intent: 'replace_text',
|
|
1208
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1209
|
-
selectedTargets: [],
|
|
1210
|
-
executedOperations: [],
|
|
1211
|
-
verification: [],
|
|
1212
|
-
errors: [
|
|
1213
|
-
{
|
|
1214
|
-
code: 'ACTION_FAILED',
|
|
1215
|
-
message: 'none of the requested text replacements matched the current document',
|
|
1216
|
-
},
|
|
1217
|
-
],
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
// Rewrite affected BODY blocks with block-targeted rewrites. The select
|
|
1221
|
-
// (matched-range) rewrite lane flattens the WHOLE paragraph's runs in
|
|
1222
|
-
// collaborative sessions and leaves format.apply inert on the block
|
|
1223
|
-
// afterwards, so per-block rewrites + the preserve pass below are the
|
|
1224
|
-
// only path that keeps intra-paragraph formatting alive. Bounded; edits
|
|
1225
|
-
// that touch no scanned body block (e.g. header/footer text) fall back
|
|
1226
|
-
// to a select-rewrite step.
|
|
1227
|
-
// Tracked mode keeps the matched-range lane. A block rewrite asks the
|
|
1228
|
-
// engine to delete and re-insert the WHOLE paragraph, so a reviewer sees
|
|
1229
|
-
// the entire block struck and re-added instead of the words that changed,
|
|
1230
|
-
// and each block contributes its own replacement/deletion pair. The
|
|
1231
|
-
// per-block lane exists to keep intra-paragraph formatting alive, which
|
|
1232
|
-
// is a direct-mode concern; under tracked mode the revision itself is the
|
|
1233
|
-
// reviewable unit and must stay scoped to the edit.
|
|
1234
|
-
const scanBlocks = args.changeMode === 'tracked'
|
|
1235
|
-
? []
|
|
1236
|
-
: pre.blocks.length > 0
|
|
1237
|
-
? pre.blocks
|
|
1238
|
-
: (await buildDocumentSnapshot(doc, { includeDomains: ['blocks'] })).blocks;
|
|
1239
|
-
// An edit counts as covered only when EVERY block holding it got a
|
|
1240
|
-
// rewrite target. The per-block lane is bounded (run capture costs a read
|
|
1241
|
-
// each), so a find present in more blocks than the cap must fall through
|
|
1242
|
-
// to the select lane below, which carries `require: 'all'`. Marking such
|
|
1243
|
-
// an edit covered after its first match would replace only the first
|
|
1244
|
-
// PRESERVE_TARGET_CAP blocks and still report success.
|
|
1245
|
-
const blocksHoldingEdit = matchingEdits.map((edit) => scanBlocks.filter((block) => block.nodeId && block.text && textIncludes(block.text, edit.find, caseSensitive))
|
|
1246
|
-
.length);
|
|
1247
|
-
const blocksCoveredForEdit = Array.from({ length: matchingEdits.length }, () => 0);
|
|
1248
|
-
for (const block of scanBlocks) {
|
|
1249
|
-
if (preserveTargets.length >= PRESERVE_TARGET_CAP)
|
|
1250
|
-
break;
|
|
1251
|
-
if (!block.nodeId || !block.text)
|
|
1252
|
-
continue;
|
|
1253
|
-
let newText = block.text;
|
|
1254
|
-
const touched = [];
|
|
1255
|
-
matchingEdits.forEach((edit, editIndex) => {
|
|
1256
|
-
if (textIncludes(newText, edit.find, caseSensitive)) {
|
|
1257
|
-
newText = replaceAllText(newText, edit.find, edit.replace, caseSensitive);
|
|
1258
|
-
touched.push(editIndex);
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
if (newText === block.text)
|
|
1262
|
-
continue;
|
|
1263
|
-
preserveTargets.push({
|
|
1264
|
-
nodeId: block.nodeId,
|
|
1265
|
-
nodeType: block.nodeType ?? 'paragraph',
|
|
1266
|
-
oldText: block.text,
|
|
1267
|
-
newText,
|
|
1268
|
-
runs: await captureBlockRuns(doc, block.nodeId, block.text),
|
|
1269
|
-
});
|
|
1270
|
-
for (const editIndex of touched)
|
|
1271
|
-
blocksCoveredForEdit[editIndex] += 1;
|
|
1272
|
-
}
|
|
1273
|
-
const coveredEdits = new Set(matchingEdits
|
|
1274
|
-
.map((_edit, editIndex) => editIndex)
|
|
1275
|
-
.filter((editIndex) => blocksHoldingEdit[editIndex] > 0 && blocksCoveredForEdit[editIndex] >= blocksHoldingEdit[editIndex]));
|
|
1276
|
-
steps = [
|
|
1277
|
-
...preserveTargets.map((entry, index) => ({
|
|
1278
|
-
id: `replace-block-${index + 1}`,
|
|
1279
|
-
op: 'text.rewrite',
|
|
1280
|
-
where: { by: 'block', nodeType: entry.nodeType, nodeId: entry.nodeId },
|
|
1281
|
-
args: {
|
|
1282
|
-
replacement: { text: entry.newText },
|
|
1283
|
-
style: preserveRewriteStyle(),
|
|
1284
|
-
},
|
|
1285
|
-
})),
|
|
1286
|
-
// Edits that touched no scanned body block (header/footer text, or a
|
|
1287
|
-
// doc larger than the scan window) keep the original select-rewrite.
|
|
1288
|
-
...matchingEdits
|
|
1289
|
-
.map((edit, editIndex) => ({ edit, editIndex }))
|
|
1290
|
-
.filter(({ editIndex }) => !coveredEdits.has(editIndex))
|
|
1291
|
-
.map(({ edit, editIndex }) => ({
|
|
1201
|
+
let candidates = args.edits.map((edit, editIndex) => ({
|
|
1202
|
+
edit,
|
|
1203
|
+
step: {
|
|
1292
1204
|
id: `replace-${editIndex + 1}`,
|
|
1293
1205
|
op: 'text.rewrite',
|
|
1294
1206
|
where: {
|
|
@@ -1305,10 +1217,42 @@ async function runReplaceText(doc, args) {
|
|
|
1305
1217
|
replacement: { text: edit.replace },
|
|
1306
1218
|
style: preserveRewriteStyle(),
|
|
1307
1219
|
},
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1220
|
+
},
|
|
1221
|
+
}));
|
|
1222
|
+
if (candidates.length > 1) {
|
|
1223
|
+
applyExpectedRevision = pre.revision;
|
|
1224
|
+
while (candidates.length > 0) {
|
|
1225
|
+
const preview = await previewMutations(doc, candidates.map((candidate) => candidate.step), args.changeMode, pre.revision);
|
|
1226
|
+
if (preview.valid === true)
|
|
1227
|
+
break;
|
|
1228
|
+
const failures = Array.isArray(preview.failures) ? preview.failures.filter(isRecord) : [];
|
|
1229
|
+
const absent = failures.find((failure) => failure.code === 'MATCH_NOT_FOUND' && typeof failure.stepId === 'string');
|
|
1230
|
+
if (!absent) {
|
|
1231
|
+
throw new Error(typeof failures[0]?.message === 'string' ? failures[0].message : 'replacement plan could not be prepared');
|
|
1232
|
+
}
|
|
1233
|
+
const absentIndex = candidates.findIndex((candidate) => candidate.step.id === absent.stepId);
|
|
1234
|
+
if (absentIndex < 0)
|
|
1235
|
+
throw new Error('replacement preview returned an unknown step id');
|
|
1236
|
+
skippedEdits.push({ find: candidates[absentIndex].edit.find });
|
|
1237
|
+
candidates = candidates.filter((_candidate, index) => index !== absentIndex);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (candidates.length === 0) {
|
|
1241
|
+
return {
|
|
1242
|
+
status: 'failed',
|
|
1243
|
+
intent: 'replace_text',
|
|
1244
|
+
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1245
|
+
selectedTargets: [],
|
|
1246
|
+
executedOperations: [],
|
|
1247
|
+
verification: [],
|
|
1248
|
+
errors: [
|
|
1249
|
+
{ code: 'ACTION_FAILED', message: 'none of the requested text replacements matched the current document' },
|
|
1250
|
+
],
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
steps = candidates.map((candidate) => candidate.step);
|
|
1310
1254
|
}
|
|
1311
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
1255
|
+
const result = await executeMutations(doc, steps, args.changeMode, applyExpectedRevision);
|
|
1312
1256
|
const preserved = [];
|
|
1313
1257
|
for (const entry of preserveTargets) {
|
|
1314
1258
|
const restored = await preserveRunPatternAfterRewrite(doc, entry.nodeId, entry.oldText, entry.runs, entry.newText);
|
|
@@ -1316,7 +1260,7 @@ async function runReplaceText(doc, args) {
|
|
|
1316
1260
|
preserved.push({ nodeId: entry.nodeId, ...restored });
|
|
1317
1261
|
}
|
|
1318
1262
|
if (args.selector && selectedTargets[0]) {
|
|
1319
|
-
const post = await
|
|
1263
|
+
const post = await buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] });
|
|
1320
1264
|
const blockId = selectedTargets[0].matched[0];
|
|
1321
1265
|
const postTarget = findSnapshotTextByNodeId(post, blockId);
|
|
1322
1266
|
const preTarget = findSnapshotTextByNodeId(pre, blockId);
|
|
@@ -1385,7 +1329,7 @@ async function runDeleteText(doc, args) {
|
|
|
1385
1329
|
// path). Rewriting the block minus the finds keeps the deletion local — without
|
|
1386
1330
|
// this a short/whitespace find matches document-wide and blows the target cap.
|
|
1387
1331
|
if (args.selector) {
|
|
1388
|
-
const scopedPre = await
|
|
1332
|
+
const scopedPre = await buildMutationSnapshot(doc, { includeDomains: snapshotDomainsForSelector(args.selector) });
|
|
1389
1333
|
try {
|
|
1390
1334
|
if (args.finds.length === 0) {
|
|
1391
1335
|
return failedReceipt('delete_text', new Error('finds must be non-empty'), scopedPre);
|
|
@@ -1428,7 +1372,7 @@ async function runDeleteText(doc, args) {
|
|
|
1428
1372
|
args: { replacement: { text: rewritten }, style: preserveRewriteStyle() },
|
|
1429
1373
|
},
|
|
1430
1374
|
];
|
|
1431
|
-
const scopedResult = await executeMutations(doc, scopedSteps, args.changeMode);
|
|
1375
|
+
const scopedResult = await executeMutations(doc, scopedSteps, args.changeMode, scopedPre.revision);
|
|
1432
1376
|
const scopedPost = await buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
|
|
1433
1377
|
const verification = evaluateChecks(scopedPre, scopedPost, [{ kind: 'revision-changed' }]);
|
|
1434
1378
|
return {
|
|
@@ -1490,7 +1434,7 @@ async function runDeleteText(doc, args) {
|
|
|
1490
1434
|
},
|
|
1491
1435
|
args: {},
|
|
1492
1436
|
}));
|
|
1493
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
1437
|
+
const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
1494
1438
|
const revision = asRecord(asRecord(result)?.revision);
|
|
1495
1439
|
const postIdentity = args.changeMode === 'tracked'
|
|
1496
1440
|
? await readDocumentIdentity(doc)
|
|
@@ -1533,7 +1477,7 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1533
1477
|
for (const domain of snapshotDomainsForSelector(selector))
|
|
1534
1478
|
domains.add(domain);
|
|
1535
1479
|
}
|
|
1536
|
-
const pre = await
|
|
1480
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: [...domains] });
|
|
1537
1481
|
try {
|
|
1538
1482
|
if (args.selectors.length === 0) {
|
|
1539
1483
|
return failedReceipt('delete_blocks', new Error('selectors must be non-empty'), pre);
|
|
@@ -1626,7 +1570,7 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1626
1570
|
});
|
|
1627
1571
|
}
|
|
1628
1572
|
}
|
|
1629
|
-
const post = await
|
|
1573
|
+
const post = await buildMutationSnapshot(doc, { includeDomains: [...domains] });
|
|
1630
1574
|
// Tracked deletions leave the block in place until the revision is decided,
|
|
1631
1575
|
// so the block count cannot move — count the structural revisions instead.
|
|
1632
1576
|
// Direct deletions must show one fewer block of each deleted node type.
|
|
@@ -1744,7 +1688,7 @@ async function runInsertListItems(doc, args) {
|
|
|
1744
1688
|
}
|
|
1745
1689
|
}
|
|
1746
1690
|
async function runAddListItems(doc, args, opts) {
|
|
1747
|
-
const pre = await
|
|
1691
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
1748
1692
|
try {
|
|
1749
1693
|
const needle = args.anchorText?.trim();
|
|
1750
1694
|
const anchorNodeId = args.anchorNodeId?.trim();
|
|
@@ -2277,56 +2221,79 @@ async function runSplitList(doc, args) {
|
|
|
2277
2221
|
return failedReceipt('split_list', err, pre);
|
|
2278
2222
|
}
|
|
2279
2223
|
}
|
|
2224
|
+
async function resolveFactPlacement(placement, facts) {
|
|
2225
|
+
if (!placement || placement.at === 'document_end')
|
|
2226
|
+
return { kind: 'documentEnd' };
|
|
2227
|
+
if (placement.at === 'document_start')
|
|
2228
|
+
return { kind: 'documentStart' };
|
|
2229
|
+
const target = await facts.target(placement.selector);
|
|
2230
|
+
if (!target)
|
|
2231
|
+
throw new SuperDocCliError('placement selector did not resolve to a unique body block', {
|
|
2232
|
+
code: 'INVALID_ARGUMENT',
|
|
2233
|
+
});
|
|
2234
|
+
return { kind: placement.at, target: { kind: 'block', nodeType: target.nodeType, nodeId: target.nodeId } };
|
|
2235
|
+
}
|
|
2236
|
+
function placementSelectors(placement) {
|
|
2237
|
+
return placement?.at === 'before' || placement?.at === 'after' ? [placement.selector] : [];
|
|
2238
|
+
}
|
|
2280
2239
|
async function runCreateTable(doc, args) {
|
|
2281
|
-
const
|
|
2240
|
+
const checks = [
|
|
2241
|
+
{ kind: 'revision-changed' },
|
|
2242
|
+
{ kind: 'block-count-delta', nodeType: 'table', delta: 1 },
|
|
2243
|
+
];
|
|
2244
|
+
const context = new ExecutionContext(doc, {
|
|
2245
|
+
evidence: args.evidence,
|
|
2246
|
+
selectors: placementSelectors(args.placement),
|
|
2247
|
+
checks,
|
|
2248
|
+
});
|
|
2282
2249
|
try {
|
|
2283
|
-
if (!Number.isInteger(args.rows) || args.rows < 1 || !Number.isInteger(args.columns) || args.columns < 1)
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
const placement =
|
|
2287
|
-
const
|
|
2288
|
-
const
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
const appliedCells = await applyTableCellTexts(doc, createdTableNodeId, insertedTableOrdinal, flattenCellTexts(args.cellTexts), args.changeMode);
|
|
2295
|
-
if (appliedCells.length > 0) {
|
|
2296
|
-
executedOperations.push({
|
|
2297
|
-
operationId: 'doc.mutations.apply',
|
|
2298
|
-
rationale: `Populated ${appliedCells.length} table cells.`,
|
|
2299
|
-
});
|
|
2250
|
+
if (!Number.isInteger(args.rows) || args.rows < 1 || !Number.isInteger(args.columns) || args.columns < 1)
|
|
2251
|
+
throw new Error('rows and columns must be positive integers');
|
|
2252
|
+
const pre = await context.start();
|
|
2253
|
+
const placement = await resolveFactPlacement(args.placement, pre);
|
|
2254
|
+
const result = await executeCreateTable(context.document, args, placement);
|
|
2255
|
+
const tableNodeId = asString(asRecord(asRecord(result)?.table)?.nodeId);
|
|
2256
|
+
if (args.cellTexts && tableNodeId) {
|
|
2257
|
+
const cells = flattenCellTexts(args.cellTexts).filter((cell) => cell.text.trim());
|
|
2258
|
+
if (pre.complete) {
|
|
2259
|
+
const applied = await applyTableCellTexts(context.document, tableNodeId, estimateInsertedTableOrdinal(pre.complete, placement), cells, args.changeMode);
|
|
2260
|
+
await styleTableCells(context.document, applied, args.changeMode);
|
|
2300
2261
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2262
|
+
else {
|
|
2263
|
+
for (const cell of cells)
|
|
2264
|
+
await context.document.tables.setCellText({
|
|
2265
|
+
target: { kind: 'block', nodeType: 'table', nodeId: tableNodeId },
|
|
2266
|
+
rowIndex: cell.rowIndex,
|
|
2267
|
+
columnIndex: cell.columnIndex,
|
|
2268
|
+
text: cell.text,
|
|
2269
|
+
changeMode: args.changeMode,
|
|
2270
|
+
});
|
|
2271
|
+
const styled = cells.filter((cell) => cell.runs?.length || cell.marks);
|
|
2272
|
+
if (styled.length) {
|
|
2273
|
+
const identities = await doc.tables.getCells({ nodeId: tableNodeId });
|
|
2274
|
+
const resolved = styled.map((cell) => {
|
|
2275
|
+
const identity = identities.cells.find((item) => item.rowIndex === cell.rowIndex &&
|
|
2276
|
+
item.columnIndex <= cell.columnIndex &&
|
|
2277
|
+
item.columnIndex + item.colspan > cell.columnIndex);
|
|
2278
|
+
if (!identity?.firstParagraphNodeId)
|
|
2279
|
+
throw new Error('Required cell paragraph identity is unavailable.');
|
|
2280
|
+
return { ...cell, nodeId: identity.firstParagraphNodeId };
|
|
2281
|
+
});
|
|
2282
|
+
const styling = await applyItemStyling(context.document, resolved.map((cell) => cell.nodeId), resolved, args.changeMode);
|
|
2283
|
+
if (styling.skipped)
|
|
2284
|
+
throw new Error(`Requested cell styling failed: ${styling.skipped}`);
|
|
2285
|
+
}
|
|
2307
2286
|
}
|
|
2308
2287
|
}
|
|
2309
|
-
const post = await
|
|
2310
|
-
|
|
2311
|
-
{ kind: 'revision-changed' },
|
|
2312
|
-
{ kind: 'block-count-delta', nodeType: 'table', delta: 1 },
|
|
2313
|
-
]);
|
|
2314
|
-
return {
|
|
2315
|
-
status: verification.every((v) => v.passed) ? 'ok' : 'failed',
|
|
2316
|
-
intent: 'create_table',
|
|
2317
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
2318
|
-
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
2319
|
-
selectedTargets: [],
|
|
2320
|
-
executedOperations,
|
|
2321
|
-
verification,
|
|
2322
|
-
};
|
|
2288
|
+
const post = await context.finish();
|
|
2289
|
+
return context.receipt('create_table', await evaluateFactChecks(pre, post, checks));
|
|
2323
2290
|
}
|
|
2324
|
-
catch (
|
|
2325
|
-
return
|
|
2291
|
+
catch (error) {
|
|
2292
|
+
return context.failure('create_table', error);
|
|
2326
2293
|
}
|
|
2327
2294
|
}
|
|
2328
2295
|
async function runCommentParagraphs(doc, args) {
|
|
2329
|
-
const pre = await
|
|
2296
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
2330
2297
|
try {
|
|
2331
2298
|
const blocks = pre.blocks.filter((b) => {
|
|
2332
2299
|
if (args.scope === 'all') {
|
|
@@ -2368,13 +2335,18 @@ async function runCommentParagraphs(doc, args) {
|
|
|
2368
2335
|
}
|
|
2369
2336
|
}
|
|
2370
2337
|
async function runAddComments(doc, args) {
|
|
2371
|
-
const
|
|
2338
|
+
const selectorList = args.selectors?.length ? args.selectors : args.selector ? [args.selector] : [];
|
|
2339
|
+
const domains = new Set(['blocks']);
|
|
2340
|
+
for (const selector of selectorList) {
|
|
2341
|
+
for (const domain of snapshotDomainsForSelector(selector))
|
|
2342
|
+
domains.add(domain);
|
|
2343
|
+
}
|
|
2344
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: [...domains] });
|
|
2372
2345
|
try {
|
|
2373
2346
|
// Batch form: `selectors` comments many blocks in ONE action call so the
|
|
2374
2347
|
// model never fans out N concurrent add_comments tool calls (which race the
|
|
2375
2348
|
// shared document and the comment-count verification). A single `selector`
|
|
2376
2349
|
// is the one-target shorthand. Comments are applied sequentially.
|
|
2377
|
-
const selectorList = args.selectors?.length ? args.selectors : args.selector ? [args.selector] : [];
|
|
2378
2350
|
if (!selectorList.length) {
|
|
2379
2351
|
return failedReceipt('add_comments', new Error('add_comments requires a "selector" or a non-empty "selectors" array'), pre);
|
|
2380
2352
|
}
|
|
@@ -2388,7 +2360,21 @@ async function runAddComments(doc, args) {
|
|
|
2388
2360
|
unresolved.push(sel);
|
|
2389
2361
|
}
|
|
2390
2362
|
if (!resolved.length) {
|
|
2391
|
-
return
|
|
2363
|
+
return {
|
|
2364
|
+
status: 'failed',
|
|
2365
|
+
intent: 'add_comments',
|
|
2366
|
+
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
2367
|
+
selectedTargets: unresolved.map((selector) => ({ selector, matched: [] })),
|
|
2368
|
+
executedOperations: [],
|
|
2369
|
+
verification: [],
|
|
2370
|
+
errors: [
|
|
2371
|
+
{
|
|
2372
|
+
code: 'TARGET_NOT_FOUND',
|
|
2373
|
+
message: 'none of the requested comment selectors resolved to a block',
|
|
2374
|
+
recovery: { kind: 'reinspect' },
|
|
2375
|
+
},
|
|
2376
|
+
],
|
|
2377
|
+
};
|
|
2392
2378
|
}
|
|
2393
2379
|
const executed = [];
|
|
2394
2380
|
for (const { commentText, nodeId } of resolved.map((r) => ({ commentText: args.commentText, nodeId: r.nodeId }))) {
|
|
@@ -2397,15 +2383,20 @@ async function runAddComments(doc, args) {
|
|
|
2397
2383
|
}
|
|
2398
2384
|
const post = await buildDocumentSnapshot(doc);
|
|
2399
2385
|
const verification = evaluateChecks(pre, post, [{ kind: 'comment-count-delta', delta: resolved.length }]);
|
|
2386
|
+
const verified = verification.every((v) => v.passed);
|
|
2387
|
+
const status = !verified ? 'failed' : unresolved.length > 0 ? 'partial' : 'ok';
|
|
2400
2388
|
return {
|
|
2401
|
-
status
|
|
2389
|
+
status,
|
|
2402
2390
|
intent: 'add_comments',
|
|
2403
2391
|
...(unresolved.length
|
|
2404
2392
|
? { note: `${unresolved.length} selector(s) did not resolve to a block and were skipped` }
|
|
2405
2393
|
: {}),
|
|
2406
2394
|
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
2407
2395
|
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
2408
|
-
selectedTargets:
|
|
2396
|
+
selectedTargets: [
|
|
2397
|
+
...resolved.map((r) => ({ selector: r.selector, matched: [r.nodeId] })),
|
|
2398
|
+
...unresolved.map((selector) => ({ selector, matched: [] })),
|
|
2399
|
+
],
|
|
2409
2400
|
executedOperations: executed,
|
|
2410
2401
|
verification,
|
|
2411
2402
|
};
|
|
@@ -2581,67 +2572,39 @@ async function runReplyToComment(doc, args) {
|
|
|
2581
2572
|
}
|
|
2582
2573
|
}
|
|
2583
2574
|
async function runRewriteBlock(doc, args) {
|
|
2584
|
-
const
|
|
2575
|
+
const context = new ExecutionContext(doc, { evidence: args.evidence, selectors: [args.selector] });
|
|
2585
2576
|
try {
|
|
2586
|
-
const
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
// it can be re-applied over the unchanged prefix/suffix afterward.
|
|
2595
|
-
const preRuns = await captureBlockRuns(doc, target.nodeId, target.text);
|
|
2596
|
-
const steps = [
|
|
2577
|
+
const pre = await context.start();
|
|
2578
|
+
const target = await pre.target(args.selector);
|
|
2579
|
+
if (!target)
|
|
2580
|
+
throw new Error('selector did not resolve to a body block');
|
|
2581
|
+
context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
2582
|
+
const text = normalizeTitleLikeRewriteText(target.text, args.text);
|
|
2583
|
+
const runs = await captureBlockRuns(doc, target.nodeId, target.text);
|
|
2584
|
+
await executeMutations(context.document, [
|
|
2597
2585
|
{
|
|
2598
2586
|
id: 'rewrite-block-1',
|
|
2599
2587
|
op: 'text.rewrite',
|
|
2600
2588
|
where: { by: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
|
|
2601
|
-
args: {
|
|
2602
|
-
replacement: { text: normalizedText },
|
|
2603
|
-
style: preserveRewriteStyle(),
|
|
2604
|
-
},
|
|
2589
|
+
args: { replacement: { text }, style: preserveRewriteStyle() },
|
|
2605
2590
|
},
|
|
2606
|
-
];
|
|
2607
|
-
|
|
2608
|
-
const
|
|
2609
|
-
const
|
|
2610
|
-
const rewrittenBlock = findSnapshotTextByNodeId(post, target.nodeId);
|
|
2591
|
+
], args.changeMode, pre.revision);
|
|
2592
|
+
await preserveRunPatternAfterRewrite(context.document, target.nodeId, target.text, runs, text);
|
|
2593
|
+
const post = await context.finish();
|
|
2594
|
+
const rewritten = await post.block(target.nodeId);
|
|
2611
2595
|
const verification = [
|
|
2612
2596
|
revisionVerification(pre.revision, post.revision, true),
|
|
2613
2597
|
{
|
|
2614
|
-
check: { kind: 'block-text-contains', nodeId: target.nodeId, text
|
|
2615
|
-
passed: !!
|
|
2598
|
+
check: { kind: 'block-text-contains', nodeId: target.nodeId, text },
|
|
2599
|
+
passed: !!rewritten && verifyRewrittenBlockText(rewritten.text, text, args.changeMode),
|
|
2616
2600
|
},
|
|
2617
2601
|
];
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
status: changed && rewritten ? 'ok' : 'failed',
|
|
2622
|
-
intent: 'rewrite_block',
|
|
2623
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
2624
|
-
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
2625
|
-
selectedTargets: [{ selector: args.selector, matched: [target.nodeId] }],
|
|
2626
|
-
executedOperations: [
|
|
2627
|
-
{ operationId: 'doc.mutations.apply', result },
|
|
2628
|
-
...(preservation ? [{ operationId: 'doc.format.apply', result: preservation }] : []),
|
|
2629
|
-
],
|
|
2630
|
-
verification,
|
|
2631
|
-
errors: changed && rewritten
|
|
2632
|
-
? undefined
|
|
2633
|
-
: [
|
|
2634
|
-
{
|
|
2635
|
-
code: 'ACTION_FAILED',
|
|
2636
|
-
message: changed
|
|
2637
|
-
? 'rewrite_block did not produce the requested rewritten text for the selected block'
|
|
2638
|
-
: 'rewrite_block produced no change for the selected block; keep the same target and provide a changed rewrite',
|
|
2639
|
-
},
|
|
2640
|
-
],
|
|
2641
|
-
};
|
|
2602
|
+
return context.receipt('rewrite_block', verification, verification.every((check) => check.passed)
|
|
2603
|
+
? {}
|
|
2604
|
+
: { errors: [{ code: 'ACTION_FAILED', message: 'rewrite_block did not produce the requested changed text' }] });
|
|
2642
2605
|
}
|
|
2643
|
-
catch (
|
|
2644
|
-
return
|
|
2606
|
+
catch (error) {
|
|
2607
|
+
return context.failure('rewrite_block', error);
|
|
2645
2608
|
}
|
|
2646
2609
|
}
|
|
2647
2610
|
const RUN_BOOL_MARKS = ['bold', 'italic', 'underline', 'strike'];
|
|
@@ -3015,7 +2978,7 @@ async function listAllTrackedChanges(listFn) {
|
|
|
3015
2978
|
}
|
|
3016
2979
|
}
|
|
3017
2980
|
async function runNormalizeBodyFontSize(doc, args) {
|
|
3018
|
-
const pre = await
|
|
2981
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3019
2982
|
try {
|
|
3020
2983
|
if (!Number.isFinite(args.fontSize) || args.fontSize <= 0) {
|
|
3021
2984
|
return failedReceipt('normalize_body_font_size', new Error('fontSize must be a positive number'), pre);
|
|
@@ -3032,7 +2995,7 @@ async function runNormalizeBodyFontSize(doc, args) {
|
|
|
3032
2995
|
where: { by: 'block', nodeType: block.nodeType, nodeId: block.nodeId },
|
|
3033
2996
|
args: { inline: { fontSize: args.fontSize }, scope: 'block' },
|
|
3034
2997
|
}));
|
|
3035
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
2998
|
+
const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
3036
2999
|
const post = await buildDocumentSnapshot(doc);
|
|
3037
3000
|
const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
|
|
3038
3001
|
return {
|
|
@@ -3063,7 +3026,7 @@ async function runNormalizeBodyFontSize(doc, args) {
|
|
|
3063
3026
|
* tracked-safe (changeMode).
|
|
3064
3027
|
*/
|
|
3065
3028
|
async function runSetFontFamily(doc, args) {
|
|
3066
|
-
const pre = await
|
|
3029
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
3067
3030
|
try {
|
|
3068
3031
|
const fontFamily = args.fontFamily.trim();
|
|
3069
3032
|
if (fontFamily.length === 0) {
|
|
@@ -3190,7 +3153,7 @@ async function runSetFontFamily(doc, args) {
|
|
|
3190
3153
|
* the recreated block inside an adjacent table cell.
|
|
3191
3154
|
*/
|
|
3192
3155
|
async function runApplyStyle(doc, args) {
|
|
3193
|
-
const pre = await
|
|
3156
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3194
3157
|
try {
|
|
3195
3158
|
const target = selectorToBlockTarget(args.selector, pre);
|
|
3196
3159
|
if (!target) {
|
|
@@ -3292,43 +3255,30 @@ async function runApplyStyle(doc, args) {
|
|
|
3292
3255
|
* escape to superdoc_execute_code and apply the alignment untracked (no pPrChange).
|
|
3293
3256
|
*/
|
|
3294
3257
|
async function runFormatParagraph(doc, args) {
|
|
3295
|
-
const
|
|
3258
|
+
const context = new ExecutionContext(doc, { evidence: args.evidence, selectors: [args.selector] });
|
|
3296
3259
|
try {
|
|
3297
|
-
const target = selectorToBlockTarget(args.selector, pre);
|
|
3298
|
-
if (!target) {
|
|
3299
|
-
return failedReceipt('format_paragraph', new Error('selector did not resolve to a unique body block'), pre);
|
|
3300
|
-
}
|
|
3301
3260
|
const alignment = typeof args.alignment === 'string' ? args.alignment.trim().toLowerCase() : '';
|
|
3302
|
-
const
|
|
3303
|
-
if (!['left', 'center', 'right', 'justify'].includes(
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
const
|
|
3261
|
+
const normalized = alignment === 'both' ? 'justify' : alignment;
|
|
3262
|
+
if (!['left', 'center', 'right', 'justify'].includes(normalized))
|
|
3263
|
+
throw new Error('format_paragraph requires alignment: left | center | right | justify');
|
|
3264
|
+
const pre = await context.start();
|
|
3265
|
+
const target = await pre.target(args.selector);
|
|
3266
|
+
if (!target)
|
|
3267
|
+
throw new Error('selector did not resolve to a unique body block');
|
|
3268
|
+
context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
3269
|
+
await executeMutations(context.document, [
|
|
3307
3270
|
{
|
|
3308
3271
|
id: 'format-paragraph-1',
|
|
3309
3272
|
op: 'format.apply',
|
|
3310
3273
|
where: { by: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
|
|
3311
|
-
args: { alignment:
|
|
3274
|
+
args: { alignment: normalized, scope: 'block' },
|
|
3312
3275
|
},
|
|
3313
|
-
];
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
|
|
3317
|
-
return {
|
|
3318
|
-
status: verification.every((v) => v.passed) ? 'ok' : 'failed',
|
|
3319
|
-
intent: `format_paragraph: ${normalizedAlignment}`,
|
|
3320
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
3321
|
-
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
3322
|
-
selectedTargets: [{ selector: args.selector, matched: [target.nodeId] }],
|
|
3323
|
-
applied: { alignment: normalizedAlignment },
|
|
3324
|
-
executedOperations: [
|
|
3325
|
-
{ operationId: 'doc.mutations.apply', result: compactOpResult(result), rationale: `align ${target.nodeId}` },
|
|
3326
|
-
],
|
|
3327
|
-
verification,
|
|
3328
|
-
};
|
|
3276
|
+
], args.changeMode, pre.revision);
|
|
3277
|
+
const post = await context.finish();
|
|
3278
|
+
return context.receipt(`format_paragraph: ${normalized}`, await evaluateFactChecks(pre, post, [{ kind: 'revision-changed' }]), { applied: { alignment: normalized } });
|
|
3329
3279
|
}
|
|
3330
|
-
catch (
|
|
3331
|
-
return
|
|
3280
|
+
catch (error) {
|
|
3281
|
+
return context.failure('format_paragraph', error);
|
|
3332
3282
|
}
|
|
3333
3283
|
}
|
|
3334
3284
|
/**
|
|
@@ -3346,7 +3296,7 @@ async function runFormatParagraph(doc, args) {
|
|
|
3346
3296
|
* still anchor on the (now tracked-deleted) source span and land right after it.
|
|
3347
3297
|
*/
|
|
3348
3298
|
async function runMoveText(doc, args) {
|
|
3349
|
-
const pre = await
|
|
3299
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
3350
3300
|
try {
|
|
3351
3301
|
if (!args.text || args.text.length === 0) {
|
|
3352
3302
|
return failedReceipt('move_text', new Error('text (the exact source span to move) is required'), pre);
|
|
@@ -3463,7 +3413,7 @@ async function runMoveText(doc, args) {
|
|
|
3463
3413
|
* instead of inserting blank paragraphs.
|
|
3464
3414
|
*/
|
|
3465
3415
|
async function runSetParagraphSpacing(doc, args) {
|
|
3466
|
-
const pre = await
|
|
3416
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3467
3417
|
try {
|
|
3468
3418
|
const target = selectorToBlockTarget(args.selector, pre);
|
|
3469
3419
|
if (!target) {
|
|
@@ -3520,7 +3470,7 @@ async function runSetParagraphSpacing(doc, args) {
|
|
|
3520
3470
|
* THE way to "start X on a new page" instead of padding with empty paragraphs.
|
|
3521
3471
|
*/
|
|
3522
3472
|
async function runInsertPageBreak(doc, args) {
|
|
3523
|
-
const pre = await
|
|
3473
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3524
3474
|
try {
|
|
3525
3475
|
const target = selectorToBlockTarget(args.selector, pre);
|
|
3526
3476
|
if (!target) {
|
|
@@ -3557,7 +3507,7 @@ async function runInsertPageBreak(doc, args) {
|
|
|
3557
3507
|
* Finds the text in the body and applies a link over its range.
|
|
3558
3508
|
*/
|
|
3559
3509
|
async function runAddHyperlink(doc, args) {
|
|
3560
|
-
const pre = await
|
|
3510
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3561
3511
|
try {
|
|
3562
3512
|
const needle = args.text;
|
|
3563
3513
|
const url = args.url;
|
|
@@ -3615,7 +3565,7 @@ async function runAddHyperlink(doc, args) {
|
|
|
3615
3565
|
* dialect, and silently apply the formatting untracked.
|
|
3616
3566
|
*/
|
|
3617
3567
|
async function runFormatText(doc, args) {
|
|
3618
|
-
const pre = await
|
|
3568
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
3619
3569
|
try {
|
|
3620
3570
|
const inline = {};
|
|
3621
3571
|
if (args.bold === true)
|
|
@@ -3776,7 +3726,7 @@ function findRanges(haystack, needle, caseSensitive) {
|
|
|
3776
3726
|
return ranges;
|
|
3777
3727
|
}
|
|
3778
3728
|
async function runApplyLetterSpacing(doc, args) {
|
|
3779
|
-
const pre = await
|
|
3729
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3780
3730
|
try {
|
|
3781
3731
|
if (!Number.isFinite(args.letterSpacing)) {
|
|
3782
3732
|
return failedReceipt('apply_letter_spacing', new Error('letterSpacing must be a finite number'), pre);
|
|
@@ -3796,7 +3746,7 @@ async function runApplyLetterSpacing(doc, args) {
|
|
|
3796
3746
|
args: { inline: { letterSpacing: args.letterSpacing }, scope: 'block' },
|
|
3797
3747
|
},
|
|
3798
3748
|
];
|
|
3799
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
3749
|
+
const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
3800
3750
|
const post = await buildDocumentSnapshot(doc);
|
|
3801
3751
|
const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
|
|
3802
3752
|
return {
|
|
@@ -3954,7 +3904,10 @@ async function runMoveRange(doc, args) {
|
|
|
3954
3904
|
}
|
|
3955
3905
|
}
|
|
3956
3906
|
async function runInsertToc(doc, args) {
|
|
3957
|
-
const
|
|
3907
|
+
const placement = args.placement ?? { at: 'document_start' };
|
|
3908
|
+
const pre = placement.at === 'before' || placement.at === 'after'
|
|
3909
|
+
? await buildMutationSnapshot(doc)
|
|
3910
|
+
: await buildDocumentSnapshot(doc);
|
|
3958
3911
|
try {
|
|
3959
3912
|
const tocFn = maybeMethod(doc, ['create', 'tableOfContents']);
|
|
3960
3913
|
if (!tocFn) {
|
|
@@ -3962,11 +3915,11 @@ async function runInsertToc(doc, args) {
|
|
|
3962
3915
|
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
3963
3916
|
});
|
|
3964
3917
|
}
|
|
3965
|
-
const
|
|
3918
|
+
const resolvedPlacement = resolvePlacement(placement, pre);
|
|
3966
3919
|
const executed = [];
|
|
3967
|
-
let tocPlacement =
|
|
3920
|
+
let tocPlacement = resolvedPlacement;
|
|
3968
3921
|
if (args.title) {
|
|
3969
|
-
const headingResult = await executeCreateHeading(doc, args.title, 1,
|
|
3922
|
+
const headingResult = await executeCreateHeading(doc, args.title, 1, resolvedPlacement, args.changeMode);
|
|
3970
3923
|
executed.push({ operationId: 'doc.create.heading', result: headingResult });
|
|
3971
3924
|
const headingNodeId = asString(asRecord(asRecord(headingResult)?.heading)?.nodeId);
|
|
3972
3925
|
if (headingNodeId) {
|
|
@@ -4093,7 +4046,7 @@ async function runStyleTable(doc, args) {
|
|
|
4093
4046
|
* should reach for instead of delete-and-recreate or insert/undo churn.
|
|
4094
4047
|
*/
|
|
4095
4048
|
async function runMoveTable(doc, args) {
|
|
4096
|
-
const pre = await
|
|
4049
|
+
const pre = await buildMutationSnapshot(doc);
|
|
4097
4050
|
try {
|
|
4098
4051
|
const tableOrdinal = args.tableOrdinal ?? 1;
|
|
4099
4052
|
const table = await resolveTableContextQuick(doc, tableOrdinal);
|
|
@@ -4619,8 +4572,8 @@ function parseScopedReplaceArgs(args) {
|
|
|
4619
4572
|
changeMode: args.changeMode === 'tracked' ? 'tracked' : 'direct',
|
|
4620
4573
|
};
|
|
4621
4574
|
}
|
|
4622
|
-
async function
|
|
4623
|
-
const pre = await
|
|
4575
|
+
async function runScopedReplaceUnchecked(doc, args) {
|
|
4576
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
4624
4577
|
const matched = resolveSnapshotSelector(pre, args.selector);
|
|
4625
4578
|
if (matched.length !== 1) {
|
|
4626
4579
|
const failure = describeSelectorFailure(args.selector, matched, pre, args.edits.map((e) => e.find));
|
|
@@ -4717,7 +4670,7 @@ async function runScopedReplace(doc, args) {
|
|
|
4717
4670
|
// applies directly, so restoring marks adds no second reviewable change.
|
|
4718
4671
|
const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
|
|
4719
4672
|
try {
|
|
4720
|
-
result = await executeMutations(doc, spanSteps, args.changeMode);
|
|
4673
|
+
result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
|
|
4721
4674
|
if (result != null)
|
|
4722
4675
|
await preserveScopedRunPattern(doc, nodeId, block.text, trackedPreRuns, planned);
|
|
4723
4676
|
}
|
|
@@ -4739,10 +4692,10 @@ async function runScopedReplace(doc, args) {
|
|
|
4739
4692
|
args: { replacement: { text: expected }, style: preserveRewriteStyle() },
|
|
4740
4693
|
},
|
|
4741
4694
|
];
|
|
4742
|
-
result = await executeMutations(doc, steps, args.changeMode);
|
|
4695
|
+
result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
4743
4696
|
await preserveScopedRunPattern(doc, nodeId, block.text, preRuns, planned);
|
|
4744
4697
|
}
|
|
4745
|
-
const post = await
|
|
4698
|
+
const post = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
4746
4699
|
const postBlock = findTextTarget(post, nodeId);
|
|
4747
4700
|
// In tracked mode the deleted text is still present in the block, so only
|
|
4748
4701
|
// require the inserted text to be visible.
|
|
@@ -4781,6 +4734,14 @@ async function runScopedReplace(doc, args) {
|
|
|
4781
4734
|
],
|
|
4782
4735
|
};
|
|
4783
4736
|
}
|
|
4737
|
+
async function runScopedReplace(doc, args) {
|
|
4738
|
+
try {
|
|
4739
|
+
return await runScopedReplaceUnchecked(doc, args);
|
|
4740
|
+
}
|
|
4741
|
+
catch (error) {
|
|
4742
|
+
return failedReceipt('replace_text', error);
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4784
4745
|
/**
|
|
4785
4746
|
* Build a `doc.format.apply` `inline` payload from a block row's sampled look.
|
|
4786
4747
|
* Shared by apply_style (likeText copy) and add_list_items (anchor auto-match)
|
|
@@ -4836,12 +4797,6 @@ function paragraphMarkRunPropsOf(payload) {
|
|
|
4836
4797
|
* Spaced dashes are listed separately from bare ones because the label run
|
|
4837
4798
|
* usually stops BEFORE the space ("JavaScript / TypeScript" + " — SuperDoc…").
|
|
4838
4799
|
*/
|
|
4839
|
-
/**
|
|
4840
|
-
* How many blocks the per-block replace lane will capture runs for. Each target
|
|
4841
|
-
* costs a `query.match` read, so the lane is bounded; edits reaching past it
|
|
4842
|
-
* fall through to the select lane rather than being silently truncated.
|
|
4843
|
-
*/
|
|
4844
|
-
const PRESERVE_TARGET_CAP = 20;
|
|
4845
4800
|
const LEAD_IN_DELIMITERS = [':', ' — ', ' – ', ' - ', '—', '–'];
|
|
4846
4801
|
/** A lead-in is a LABEL, not a sentence — longer spans are prose, not a pattern. */
|
|
4847
4802
|
const LEAD_IN_MAX_CHARS = 90;
|
|
@@ -5324,8 +5279,18 @@ async function listBlockRows(doc) {
|
|
|
5324
5279
|
const fn = maybeMethod(doc, ['blocks', 'list']);
|
|
5325
5280
|
if (!fn)
|
|
5326
5281
|
return [];
|
|
5327
|
-
const
|
|
5328
|
-
|
|
5282
|
+
const rows = [];
|
|
5283
|
+
const pageSize = 250;
|
|
5284
|
+
let offset = 0;
|
|
5285
|
+
while (true) {
|
|
5286
|
+
const raw = (await fn({ offset, limit: pageSize }));
|
|
5287
|
+
const page = Array.isArray(raw?.blocks) ? raw.blocks : [];
|
|
5288
|
+
rows.push(...page);
|
|
5289
|
+
offset += page.length;
|
|
5290
|
+
const total = typeof raw?.total === 'number' && Number.isFinite(raw.total) ? raw.total : rows.length;
|
|
5291
|
+
if (page.length === 0 || offset >= total)
|
|
5292
|
+
return rows;
|
|
5293
|
+
}
|
|
5329
5294
|
}
|
|
5330
5295
|
function blockNumbering(row) {
|
|
5331
5296
|
const numbering = row?.numbering;
|
|
@@ -5491,7 +5456,7 @@ async function convertNumberedRange(doc, fromMarker, toMarker, kind, tracked) {
|
|
|
5491
5456
|
};
|
|
5492
5457
|
}
|
|
5493
5458
|
async function convertParagraphRange(doc, fromText, toText, kind, tracked) {
|
|
5494
|
-
const pre = await
|
|
5459
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
5495
5460
|
const blocks = pre.blocks ?? [];
|
|
5496
5461
|
const findBlock = (needle) => blocks.findIndex((b) => b.nodeType === 'paragraph' && (b.text ?? '').toLowerCase().includes(needle.toLowerCase()));
|
|
5497
5462
|
const fromIdx = findBlock(fromText);
|
|
@@ -5535,7 +5500,7 @@ async function convertParagraphRange(doc, fromText, toText, kind, tracked) {
|
|
|
5535
5500
|
};
|
|
5536
5501
|
}
|
|
5537
5502
|
await createListFromParagraphRange(createFn, kind, range[0].nodeId, range[range.length - 1].nodeId, tracked ? 'tracked' : undefined);
|
|
5538
|
-
const post = await
|
|
5503
|
+
const post = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
|
|
5539
5504
|
const postList = (post.lists ?? []).find((l) => l.items.some((it) => it.nodeId === range[0].nodeId));
|
|
5540
5505
|
const allInOneList = postList != null && range.every((b) => postList.items.some((it) => it.nodeId === b.nodeId));
|
|
5541
5506
|
return {
|
|
@@ -5804,7 +5769,7 @@ async function appendListAtPlacement(doc, args) {
|
|
|
5804
5769
|
const selector = placement && isRecord(placement.selector) ? placement.selector : null;
|
|
5805
5770
|
if (items.length === 0 || !placement || (at !== 'after' && at !== 'before') || !selector)
|
|
5806
5771
|
return null;
|
|
5807
|
-
const pre = await
|
|
5772
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
5808
5773
|
const blocks = pre.blocks ?? [];
|
|
5809
5774
|
// Resolve through the SHARED selector resolver so the full selector
|
|
5810
5775
|
// vocabulary works (nodeId / textSearch / ordinal / relative / ref) and
|
|
@@ -5865,7 +5830,7 @@ async function appendListAtPlacement(doc, args) {
|
|
|
5865
5830
|
const listIds = typeof args.headingText === 'string' && args.headingText.length > 0 ? createdIds.slice(1) : createdIds;
|
|
5866
5831
|
await createListFromParagraphRange(listsCreateFn, kind, listIds[0], listIds[listIds.length - 1], parseChangeMode(args.changeMode));
|
|
5867
5832
|
// Verify both promises: the items form one list AND it sits at the anchor.
|
|
5868
|
-
const post = await
|
|
5833
|
+
const post = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
|
|
5869
5834
|
const postBlocks = post.blocks ?? [];
|
|
5870
5835
|
const postList = (post.lists ?? []).find((l) => l.items.some((it) => it.nodeId === listIds[0]));
|
|
5871
5836
|
const listOk = postList != null && listIds.every((id) => postList.items.some((it) => it.nodeId === id));
|
|
@@ -5969,8 +5934,9 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5969
5934
|
const formatApplyFn = maybeMethod(doc, ['format', 'apply']);
|
|
5970
5935
|
if (formatApplyFn) {
|
|
5971
5936
|
const textLength = (created.textPreview ?? '').length;
|
|
5972
|
-
const
|
|
5973
|
-
|
|
5937
|
+
const full = createdText ||
|
|
5938
|
+
(await doc.blocks.list({ nodeIds: [created.nodeId], limit: 1, includeText: true })).blocks.find((block) => block.nodeId === created.nodeId)?.text ||
|
|
5939
|
+
'';
|
|
5974
5940
|
await formatApplyFn({
|
|
5975
5941
|
blockId: created.nodeId,
|
|
5976
5942
|
start: 0,
|
|
@@ -6167,11 +6133,11 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
|
|
|
6167
6133
|
}
|
|
6168
6134
|
return Object.keys(applied).length > 0 ? { matchedSibling: reference.nodeId, ...applied } : null;
|
|
6169
6135
|
}
|
|
6170
|
-
async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args) {
|
|
6136
|
+
async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, availableRows) {
|
|
6171
6137
|
if (receipt.status !== 'ok')
|
|
6172
6138
|
return receipt;
|
|
6173
6139
|
try {
|
|
6174
|
-
const postBlocks = await listBlockRows(doc);
|
|
6140
|
+
const postBlocks = availableRows ?? (await listBlockRows(doc));
|
|
6175
6141
|
const preIds = new Set(preBlocks.map((b) => b.nodeId));
|
|
6176
6142
|
const createdRows = postBlocks
|
|
6177
6143
|
.map((b, i) => ({ row: b, index: i }))
|
|
@@ -6428,7 +6394,7 @@ async function runAttachNumbering(doc, args) {
|
|
|
6428
6394
|
verification: [{ check: { kind: 'marker-rendered' }, passed: !!newMarker }],
|
|
6429
6395
|
};
|
|
6430
6396
|
}
|
|
6431
|
-
|
|
6397
|
+
async function dispatchSuperdocPerformAction(doc, args) {
|
|
6432
6398
|
if (!isRecord(args)) {
|
|
6433
6399
|
throw new SuperDocCliError('superdoc_perform_action arguments must be an object', {
|
|
6434
6400
|
code: 'INVALID_ARGUMENT',
|
|
@@ -6462,9 +6428,9 @@ export async function superdocPerformAction(doc, args) {
|
|
|
6462
6428
|
});
|
|
6463
6429
|
}
|
|
6464
6430
|
const headingLevel = asNumber(args.headingLevel);
|
|
6465
|
-
|
|
6466
|
-
const receipt = await runInsertParagraphs(doc, {
|
|
6431
|
+
return runInsertParagraphs(doc, {
|
|
6467
6432
|
action,
|
|
6433
|
+
evidence: args.evidence,
|
|
6468
6434
|
texts,
|
|
6469
6435
|
textSpecs,
|
|
6470
6436
|
placement: parsePlacement(args.placement),
|
|
@@ -6472,8 +6438,7 @@ export async function superdocPerformAction(doc, args) {
|
|
|
6472
6438
|
headingLevel: headingLevel != null && Number.isInteger(headingLevel) && headingLevel >= 1 && headingLevel <= 6
|
|
6473
6439
|
? headingLevel
|
|
6474
6440
|
: undefined,
|
|
6475
|
-
});
|
|
6476
|
-
return matchInsertedBlockFormatting(doc, preRows, receipt, args);
|
|
6441
|
+
}, args);
|
|
6477
6442
|
}
|
|
6478
6443
|
case 'insert_heading': {
|
|
6479
6444
|
const text = asString(args.text);
|
|
@@ -6597,6 +6562,7 @@ export async function superdocPerformAction(doc, args) {
|
|
|
6597
6562
|
}
|
|
6598
6563
|
return runCreateTable(doc, {
|
|
6599
6564
|
action,
|
|
6565
|
+
evidence: args.evidence,
|
|
6600
6566
|
rows,
|
|
6601
6567
|
columns,
|
|
6602
6568
|
cellTexts: parseCellTexts(args.cellTexts),
|
|
@@ -6620,16 +6586,41 @@ export async function superdocPerformAction(doc, args) {
|
|
|
6620
6586
|
}
|
|
6621
6587
|
case 'add_comments': {
|
|
6622
6588
|
const commentText = asString(args.commentText);
|
|
6623
|
-
|
|
6624
|
-
const selectors = Array.isArray(args.selectors)
|
|
6625
|
-
? args.selectors.map((s) => parseSelector(s)).filter((s) => Boolean(s))
|
|
6626
|
-
: undefined;
|
|
6627
|
-
if (!commentText || (!selector && !(selectors && selectors.length))) {
|
|
6589
|
+
if (!commentText) {
|
|
6628
6590
|
throw new SuperDocCliError('add_comments requires "commentText" and a "selector" or non-empty "selectors" array', {
|
|
6629
6591
|
code: 'INVALID_ARGUMENT',
|
|
6630
6592
|
});
|
|
6631
6593
|
}
|
|
6632
|
-
|
|
6594
|
+
const hasSelector = args.selector != null;
|
|
6595
|
+
const hasSelectors = args.selectors != null;
|
|
6596
|
+
if (hasSelector && hasSelectors) {
|
|
6597
|
+
throw new SuperDocCliError('add_comments requires either "selector" or "selectors", not both', {
|
|
6598
|
+
code: 'INVALID_ARGUMENT',
|
|
6599
|
+
});
|
|
6600
|
+
}
|
|
6601
|
+
if (hasSelectors) {
|
|
6602
|
+
if (!Array.isArray(args.selectors) || args.selectors.length === 0) {
|
|
6603
|
+
throw new SuperDocCliError('add_comments requires "selectors" to be a non-empty array', {
|
|
6604
|
+
code: 'INVALID_ARGUMENT',
|
|
6605
|
+
});
|
|
6606
|
+
}
|
|
6607
|
+
// Parse the complete batch before mutation. Filtering an invalid entry
|
|
6608
|
+
// would make the receipt report success for only part of the request.
|
|
6609
|
+
const selectors = [];
|
|
6610
|
+
for (const [index, raw] of args.selectors.entries()) {
|
|
6611
|
+
const parsed = parseSelector(raw);
|
|
6612
|
+
if (!parsed) {
|
|
6613
|
+
throw new SuperDocCliError(`add_comments selectors[${index}] is not a valid selector; no comments were added`, { code: 'INVALID_ARGUMENT' });
|
|
6614
|
+
}
|
|
6615
|
+
selectors.push(parsed);
|
|
6616
|
+
}
|
|
6617
|
+
return runAddComments(doc, { action, commentText, selectors });
|
|
6618
|
+
}
|
|
6619
|
+
const selector = parseSelector(args.selector);
|
|
6620
|
+
if (!selector) {
|
|
6621
|
+
throw new SuperDocCliError('add_comments requires "selector" to be valid or "selectors" to be a non-empty array', { code: 'INVALID_ARGUMENT' });
|
|
6622
|
+
}
|
|
6623
|
+
return runAddComments(doc, { action, commentText, selector });
|
|
6633
6624
|
}
|
|
6634
6625
|
case 'resolve_comments':
|
|
6635
6626
|
return runResolveComments(doc, {
|
|
@@ -6658,6 +6649,7 @@ export async function superdocPerformAction(doc, args) {
|
|
|
6658
6649
|
}
|
|
6659
6650
|
return runRewriteBlock(doc, {
|
|
6660
6651
|
action,
|
|
6652
|
+
evidence: args.evidence,
|
|
6661
6653
|
text,
|
|
6662
6654
|
selector,
|
|
6663
6655
|
changeMode: parseChangeMode(args.changeMode),
|
|
@@ -6911,6 +6903,7 @@ export async function superdocPerformAction(doc, args) {
|
|
|
6911
6903
|
}
|
|
6912
6904
|
return runFormatParagraph(doc, {
|
|
6913
6905
|
action,
|
|
6906
|
+
evidence: args.evidence,
|
|
6914
6907
|
selector,
|
|
6915
6908
|
alignment: asString(args.alignment) ?? '',
|
|
6916
6909
|
changeMode: parseChangeMode(args.changeMode),
|
|
@@ -7042,6 +7035,24 @@ export async function superdocPerformAction(doc, args) {
|
|
|
7042
7035
|
}
|
|
7043
7036
|
}
|
|
7044
7037
|
}
|
|
7038
|
+
export async function superdocPerformAction(doc, args) {
|
|
7039
|
+
try {
|
|
7040
|
+
return await dispatchSuperdocPerformAction(doc, args);
|
|
7041
|
+
}
|
|
7042
|
+
catch (error) {
|
|
7043
|
+
if (!(error instanceof MutationSnapshotError))
|
|
7044
|
+
throw error;
|
|
7045
|
+
const action = isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
|
|
7046
|
+
return {
|
|
7047
|
+
status: 'failed',
|
|
7048
|
+
intent: action,
|
|
7049
|
+
selectedTargets: [],
|
|
7050
|
+
executedOperations: [],
|
|
7051
|
+
verification: [],
|
|
7052
|
+
errors: [{ code: error.code, message: error.message, recovery: { kind: 'reinspect' } }],
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
7055
|
+
}
|
|
7045
7056
|
function parseChangeMode(value) {
|
|
7046
7057
|
if (value === 'direct' || value === 'tracked')
|
|
7047
7058
|
return value;
|