@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.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var executionContext = require('./execution-context.cjs');
|
|
3
4
|
var errors = require('../runtime/errors.cjs');
|
|
4
5
|
var listTransform = require('../action-primitives/tools/list-transform.cjs');
|
|
5
6
|
var structureInsert = require('../action-primitives/tools/structure-insert.cjs');
|
|
@@ -170,18 +171,18 @@ const ACTION_GROUPS = [
|
|
|
170
171
|
* action at compile time.
|
|
171
172
|
*/
|
|
172
173
|
const ACTION_ARGS = {
|
|
173
|
-
insert_paragraphs: ['text', 'texts', 'headingLevel', 'placement', 'changeMode'],
|
|
174
|
+
insert_paragraphs: ['text', 'texts', 'headingLevel', 'placement', 'changeMode', 'evidence'],
|
|
174
175
|
insert_heading: ['text', 'level', 'placement', 'changeMode'],
|
|
175
176
|
replace_text: ['edits', 'selector', 'caseSensitive', 'changeMode'],
|
|
176
177
|
delete_text: ['finds', 'selector', 'caseSensitive', 'changeMode'],
|
|
177
178
|
delete_blocks: ['selectors', 'selector', 'changeMode'],
|
|
178
179
|
append_list: ['items', 'kind', 'headingText', 'headingLevel', 'placement', 'changeMode'],
|
|
179
|
-
create_table: ['rows', 'columns', 'cellTexts', 'placement', 'changeMode'],
|
|
180
|
+
create_table: ['rows', 'columns', 'cellTexts', 'placement', 'changeMode', 'evidence'],
|
|
180
181
|
comment_paragraphs: ['commentText', 'scope', 'excludeBlockQuotes'],
|
|
181
182
|
add_comments: ['commentText', 'selector', 'selectors'],
|
|
182
183
|
resolve_comments: ['anchorText', 'reopen'],
|
|
183
184
|
reply_to_comment: ['commentText', 'anchorText', 'commentId'],
|
|
184
|
-
rewrite_block: ['text', 'selector', 'changeMode'],
|
|
185
|
+
rewrite_block: ['text', 'selector', 'changeMode', 'evidence'],
|
|
185
186
|
accept_tracked_changes: ['author', 'changeType'],
|
|
186
187
|
reject_tracked_changes: ['author', 'changeType'],
|
|
187
188
|
normalize_body_font_size: ['fontSize', 'changeMode'],
|
|
@@ -216,7 +217,7 @@ const ACTION_ARGS = {
|
|
|
216
217
|
'changeMode',
|
|
217
218
|
],
|
|
218
219
|
apply_style: ['selector', 'styleId', 'headingLevel', 'likeText'],
|
|
219
|
-
format_paragraph: ['selector', 'alignment', 'changeMode'],
|
|
220
|
+
format_paragraph: ['selector', 'alignment', 'changeMode', 'evidence'],
|
|
220
221
|
move_text: ['text', 'afterText', 'changeMode'],
|
|
221
222
|
style_table: ['tableOrdinal', 'accentColor'],
|
|
222
223
|
move_table: ['tableOrdinal', 'placement'],
|
|
@@ -319,10 +320,6 @@ function findSnapshotTextByNodeId(snapshot, nodeId) {
|
|
|
319
320
|
}
|
|
320
321
|
return null;
|
|
321
322
|
}
|
|
322
|
-
function lastBlock(snapshot) {
|
|
323
|
-
const block = snapshot.blocks[snapshot.blocks.length - 1];
|
|
324
|
-
return block ? { nodeId: block.nodeId, nodeType: block.nodeType } : null;
|
|
325
|
-
}
|
|
326
323
|
function createdBlockTarget(result) {
|
|
327
324
|
const rec = asRecord(result);
|
|
328
325
|
const paragraph = asRecord(rec?.paragraph);
|
|
@@ -514,6 +511,8 @@ function revisionVerification(preRevision, postRevision, expectChanged) {
|
|
|
514
511
|
}
|
|
515
512
|
function failedReceipt(intent, err, preSnapshot) {
|
|
516
513
|
const message = reasonOf(err);
|
|
514
|
+
const snapshotError = err instanceof docSnapshot.MutationSnapshotError ? err : null;
|
|
515
|
+
const errorCode = snapshotError?.code ?? asString(asRecord(err)?.code) ?? 'ACTION_FAILED';
|
|
517
516
|
return {
|
|
518
517
|
status: 'failed',
|
|
519
518
|
intent,
|
|
@@ -523,7 +522,13 @@ function failedReceipt(intent, err, preSnapshot) {
|
|
|
523
522
|
selectedTargets: [],
|
|
524
523
|
executedOperations: [],
|
|
525
524
|
verification: [],
|
|
526
|
-
errors: [
|
|
525
|
+
errors: [
|
|
526
|
+
{
|
|
527
|
+
code: errorCode,
|
|
528
|
+
message,
|
|
529
|
+
...(snapshotError || errorCode === 'REVISION_CONFLICT' ? { recovery: { kind: 'reinspect' } } : {}),
|
|
530
|
+
},
|
|
531
|
+
],
|
|
527
532
|
};
|
|
528
533
|
}
|
|
529
534
|
async function receiptFromWorkflowResult(doc, intent, pre, workflowResult, selectedTargets = [], checks = [{ kind: 'revision-changed' }]) {
|
|
@@ -567,14 +572,16 @@ async function receiptFromWorkflowResult(doc, intent, pre, workflowResult, selec
|
|
|
567
572
|
}
|
|
568
573
|
function buildFullBlockTextTarget(snapshot, blockId) {
|
|
569
574
|
const block = snapshot.blocks.find((entry) => entry.nodeId === blockId);
|
|
570
|
-
|
|
575
|
+
const cell = block ? null : snapshot.tables.flatMap((table) => table.cells).find((entry) => entry.nodeId === blockId);
|
|
576
|
+
const text = block?.text ?? cell?.text;
|
|
577
|
+
if (text == null)
|
|
571
578
|
return null;
|
|
572
579
|
return {
|
|
573
580
|
kind: 'text',
|
|
574
581
|
blockId,
|
|
575
582
|
range: {
|
|
576
583
|
start: 0,
|
|
577
|
-
end:
|
|
584
|
+
end: text.length,
|
|
578
585
|
},
|
|
579
586
|
};
|
|
580
587
|
}
|
|
@@ -627,8 +634,8 @@ function evaluateChecks(pre, post, checks) {
|
|
|
627
634
|
else if (check.kind === 'comment-count-delta') {
|
|
628
635
|
results.push({
|
|
629
636
|
check,
|
|
630
|
-
passed: post.comments
|
|
631
|
-
detail: `pre=${pre.comments
|
|
637
|
+
passed: post.counts.comments - pre.counts.comments === check.delta,
|
|
638
|
+
detail: `pre=${pre.counts.comments} post=${post.counts.comments}`,
|
|
632
639
|
});
|
|
633
640
|
}
|
|
634
641
|
else if (check.kind === 'tracked-change-count-delta') {
|
|
@@ -775,13 +782,28 @@ async function executeCreateHeading(doc, text, level, placement, changeMode) {
|
|
|
775
782
|
// Dual dialect — see executeCreateParagraph.
|
|
776
783
|
return fn(params, changeMode ? { changeMode } : undefined);
|
|
777
784
|
}
|
|
778
|
-
async function executeMutations(doc, steps, changeMode) {
|
|
785
|
+
async function executeMutations(doc, steps, changeMode, expectedRevision) {
|
|
779
786
|
const fn = maybeMethod(doc, ['mutations', 'apply']);
|
|
780
787
|
if (!fn)
|
|
781
788
|
throw new errors.SuperDocCliError('doc.mutations.apply is not available on the document handle.', {
|
|
782
789
|
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
783
790
|
});
|
|
784
|
-
return fn({
|
|
791
|
+
return fn({
|
|
792
|
+
atomic: true,
|
|
793
|
+
changeMode: changeMode ?? 'direct',
|
|
794
|
+
steps,
|
|
795
|
+
...(expectedRevision ? { expectedRevision } : {}),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
async function previewMutations(doc, steps, changeMode, expectedRevision) {
|
|
799
|
+
const fn = maybeMethod(doc, ['mutations', 'preview']);
|
|
800
|
+
if (!fn) {
|
|
801
|
+
throw new errors.SuperDocCliError('doc.mutations.preview is not available on the document handle.', {
|
|
802
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
const result = await fn({ atomic: true, changeMode: changeMode ?? 'direct', expectedRevision, steps });
|
|
806
|
+
return isRecord(result) ? result : {};
|
|
785
807
|
}
|
|
786
808
|
async function executeCreateTable(doc, args, placement) {
|
|
787
809
|
const fn = maybeMethod(doc, ['create', 'table']);
|
|
@@ -835,7 +857,7 @@ forceDirectFill = false) {
|
|
|
835
857
|
});
|
|
836
858
|
}
|
|
837
859
|
const extracted = asRecord(await extractFn({}));
|
|
838
|
-
const blocks = (Array.isArray(extracted?.blocks) ? extracted
|
|
860
|
+
const blocks = (Array.isArray(extracted?.blocks) ? extracted.blocks : [])
|
|
839
861
|
.map((block) => asRecord(block))
|
|
840
862
|
.filter((block) => block != null)
|
|
841
863
|
.flatMap((block) => {
|
|
@@ -1024,97 +1046,74 @@ async function styleTableCells(doc, cells, changeMode) {
|
|
|
1024
1046
|
await applyItemStyling(doc, nodeIds, specs, changeMode);
|
|
1025
1047
|
return styled.length;
|
|
1026
1048
|
}
|
|
1027
|
-
async function runInsertParagraphs(doc, args) {
|
|
1028
|
-
const
|
|
1049
|
+
async function runInsertParagraphs(doc, args, formatArgs) {
|
|
1050
|
+
const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, selectors: placementSelectors(args.placement) });
|
|
1029
1051
|
try {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
// For subsequent items: append after the previously inserted block by
|
|
1041
|
-
// using `documentEnd` (the SDK keeps blocks contiguous) so order is
|
|
1042
|
-
// preserved.
|
|
1052
|
+
const pre = await context.start();
|
|
1053
|
+
const placement = await resolveFactPlacement(args.placement, pre);
|
|
1054
|
+
const isNewDocument = (await pre.count()) <= 1;
|
|
1055
|
+
const anchor = placement.kind === 'before' || placement.kind === 'after'
|
|
1056
|
+
? await pre.block(placement.target.nodeId)
|
|
1057
|
+
: await pre.target({
|
|
1058
|
+
kind: 'placement',
|
|
1059
|
+
at: placement.kind === 'documentStart' ? 'document_start' : 'document_end',
|
|
1060
|
+
});
|
|
1061
|
+
const preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
|
|
1043
1062
|
let currentPlacement = placement;
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
return
|
|
1063
|
+
const nodeIds = [];
|
|
1064
|
+
const specs = [];
|
|
1065
|
+
const advance = async (result) => {
|
|
1066
|
+
const target = createdBlockTarget(result);
|
|
1067
|
+
if (target) {
|
|
1068
|
+
currentPlacement = {
|
|
1069
|
+
kind: 'after',
|
|
1070
|
+
target: { kind: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
|
|
1071
|
+
};
|
|
1072
|
+
return target.nodeId;
|
|
1054
1073
|
}
|
|
1055
|
-
const
|
|
1056
|
-
const last =
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1074
|
+
const current = await context.finishRevision();
|
|
1075
|
+
const last = await current.target({ kind: 'placement', at: 'document_end' });
|
|
1076
|
+
if (!last)
|
|
1077
|
+
throw new Error('Created paragraph identity is unavailable.');
|
|
1078
|
+
currentPlacement = { kind: 'after', target: { kind: 'block', nodeType: last.nodeType, nodeId: last.nodeId } };
|
|
1079
|
+
return last.nodeId;
|
|
1060
1080
|
};
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
currentPlacement = await advanceAfter(result);
|
|
1084
|
-
// New-document drafts get a blank paragraph after each item for spacing;
|
|
1085
|
-
// inserts into an existing document do not (would scatter blank lines).
|
|
1086
|
-
if (isNewDocument) {
|
|
1087
|
-
const spacer = await executeCreateParagraph(doc, '', currentPlacement, args.changeMode);
|
|
1088
|
-
executedOperations.push({ operationId: 'doc.create.paragraph', result: spacer, rationale: 'spacing' });
|
|
1089
|
-
currentPlacement = await advanceAfter(spacer);
|
|
1081
|
+
const texts = args.texts ?? (args.text ? [args.text] : []);
|
|
1082
|
+
for (let i = 0; i < texts.length; i++) {
|
|
1083
|
+
const result = i === 0 && args.headingLevel
|
|
1084
|
+
? await executeCreateHeading(context.document, texts[i], args.headingLevel, currentPlacement, args.changeMode)
|
|
1085
|
+
: await executeCreateParagraph(context.document, texts[i], currentPlacement, args.changeMode);
|
|
1086
|
+
const id = await advance(result);
|
|
1087
|
+
nodeIds.push(id);
|
|
1088
|
+
specs.push({ ...args.textSpecs?.[i], text: texts[i] });
|
|
1089
|
+
if (isNewDocument)
|
|
1090
|
+
await advance(await executeCreateParagraph(context.document, '', currentPlacement, args.changeMode));
|
|
1091
|
+
}
|
|
1092
|
+
if (specs.some((spec) => spec.runs?.length || spec.marks))
|
|
1093
|
+
await applyItemStyling(context.document, nodeIds, specs, args.changeMode);
|
|
1094
|
+
let post = await context.finishRevision();
|
|
1095
|
+
let formatted;
|
|
1096
|
+
if (formatArgs) {
|
|
1097
|
+
let localRows;
|
|
1098
|
+
if (!pre.complete) {
|
|
1099
|
+
const created = await post.page({ nodeIds, limit: Math.max(nodeIds.length, 1) });
|
|
1100
|
+
localRows = [...(anchor ? [anchor] : []), ...created].sort((a, b) => a.ordinal - b.ordinal);
|
|
1090
1101
|
}
|
|
1102
|
+
const count = context.executedOperations.length;
|
|
1103
|
+
formatted = await matchInsertedBlockFormatting(context.document, preRows, context.receipt('insert_paragraphs', []), formatArgs, localRows);
|
|
1104
|
+
if (context.executedOperations.length !== count)
|
|
1105
|
+
post = await context.finishRevision();
|
|
1091
1106
|
}
|
|
1092
|
-
|
|
1093
|
-
const styling = await applyItemStyling(doc, styledNodeIds, styledSpecs, args.changeMode);
|
|
1094
|
-
executedOperations.push({
|
|
1095
|
-
operationId: 'doc.format.apply',
|
|
1096
|
-
result: styling,
|
|
1097
|
-
rationale: `Styled ${styledNodeIds.length} inserted paragraph(s)`,
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
const postIdentity = await readDocumentIdentity(doc);
|
|
1101
|
-
const verification = [revisionVerification(pre.revision, postIdentity.revision, true)];
|
|
1102
|
-
return {
|
|
1103
|
-
status: verification.every((v) => v.passed) ? 'ok' : 'failed',
|
|
1104
|
-
intent: 'insert_paragraphs',
|
|
1105
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1106
|
-
postSnapshot: postIdentity,
|
|
1107
|
-
selectedTargets: [],
|
|
1108
|
-
executedOperations,
|
|
1109
|
-
verification,
|
|
1110
|
-
};
|
|
1107
|
+
return context.receipt('insert_paragraphs', await executionContext.evaluateFactChecks(pre, post, [{ kind: 'revision-changed' }]), formatted?.contextualFormatting ? { contextualFormatting: formatted.contextualFormatting } : {});
|
|
1111
1108
|
}
|
|
1112
|
-
catch (
|
|
1113
|
-
return
|
|
1109
|
+
catch (error) {
|
|
1110
|
+
return context.failure('insert_paragraphs', error);
|
|
1114
1111
|
}
|
|
1115
1112
|
}
|
|
1116
1113
|
async function runInsertHeading(doc, args) {
|
|
1117
|
-
const pre =
|
|
1114
|
+
const pre = args.placement?.at === 'before' || args.placement?.at === 'after'
|
|
1115
|
+
? await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] })
|
|
1116
|
+
: await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
|
|
1118
1117
|
try {
|
|
1119
1118
|
const placement = resolvePlacement(args.placement, pre);
|
|
1120
1119
|
const result = await executeCreateHeading(doc, args.text, args.level, placement, args.changeMode);
|
|
@@ -1136,10 +1135,10 @@ async function runInsertHeading(doc, args) {
|
|
|
1136
1135
|
}
|
|
1137
1136
|
async function runReplaceText(doc, args) {
|
|
1138
1137
|
const selectorDomains = args.selector ? snapshotDomainsForSelector(args.selector) : null;
|
|
1139
|
-
const requiresBlockSnapshot = args.selector != null
|
|
1138
|
+
const requiresBlockSnapshot = args.selector != null;
|
|
1140
1139
|
const preIdentity = requiresBlockSnapshot ? null : await readDocumentIdentity(doc);
|
|
1141
1140
|
const pre = requiresBlockSnapshot
|
|
1142
|
-
? await docSnapshot.
|
|
1141
|
+
? await docSnapshot.buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
|
|
1143
1142
|
: snapshotFromIdentity(preIdentity);
|
|
1144
1143
|
try {
|
|
1145
1144
|
if (args.edits.length === 0) {
|
|
@@ -1149,6 +1148,7 @@ async function runReplaceText(doc, args) {
|
|
|
1149
1148
|
const selectedTargets = [];
|
|
1150
1149
|
let skippedEdits = [];
|
|
1151
1150
|
let steps;
|
|
1151
|
+
let applyExpectedRevision;
|
|
1152
1152
|
// Blocks whose text this call rewrites, with their pre-mutation run layout:
|
|
1153
1153
|
// the engine replace lane renders the replaced range as uniform runs, so a
|
|
1154
1154
|
// whole-paragraph find/replace flattens intra-paragraph formatting (bold
|
|
@@ -1190,6 +1190,7 @@ async function runReplaceText(doc, args) {
|
|
|
1190
1190
|
},
|
|
1191
1191
|
},
|
|
1192
1192
|
];
|
|
1193
|
+
applyExpectedRevision = pre.revision;
|
|
1193
1194
|
selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
1194
1195
|
preserveTargets.push({
|
|
1195
1196
|
nodeId: target.nodeId,
|
|
@@ -1200,98 +1201,9 @@ async function runReplaceText(doc, args) {
|
|
|
1200
1201
|
});
|
|
1201
1202
|
}
|
|
1202
1203
|
else {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
:
|
|
1206
|
-
skippedEdits = args.edits.filter((edit) => !matchingEdits.includes(edit));
|
|
1207
|
-
if (matchingEdits.length === 0) {
|
|
1208
|
-
return {
|
|
1209
|
-
status: 'failed',
|
|
1210
|
-
intent: 'replace_text',
|
|
1211
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1212
|
-
selectedTargets: [],
|
|
1213
|
-
executedOperations: [],
|
|
1214
|
-
verification: [],
|
|
1215
|
-
errors: [
|
|
1216
|
-
{
|
|
1217
|
-
code: 'ACTION_FAILED',
|
|
1218
|
-
message: 'none of the requested text replacements matched the current document',
|
|
1219
|
-
},
|
|
1220
|
-
],
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
// Rewrite affected BODY blocks with block-targeted rewrites. The select
|
|
1224
|
-
// (matched-range) rewrite lane flattens the WHOLE paragraph's runs in
|
|
1225
|
-
// collaborative sessions and leaves format.apply inert on the block
|
|
1226
|
-
// afterwards, so per-block rewrites + the preserve pass below are the
|
|
1227
|
-
// only path that keeps intra-paragraph formatting alive. Bounded; edits
|
|
1228
|
-
// that touch no scanned body block (e.g. header/footer text) fall back
|
|
1229
|
-
// to a select-rewrite step.
|
|
1230
|
-
// Tracked mode keeps the matched-range lane. A block rewrite asks the
|
|
1231
|
-
// engine to delete and re-insert the WHOLE paragraph, so a reviewer sees
|
|
1232
|
-
// the entire block struck and re-added instead of the words that changed,
|
|
1233
|
-
// and each block contributes its own replacement/deletion pair. The
|
|
1234
|
-
// per-block lane exists to keep intra-paragraph formatting alive, which
|
|
1235
|
-
// is a direct-mode concern; under tracked mode the revision itself is the
|
|
1236
|
-
// reviewable unit and must stay scoped to the edit.
|
|
1237
|
-
const scanBlocks = args.changeMode === 'tracked'
|
|
1238
|
-
? []
|
|
1239
|
-
: pre.blocks.length > 0
|
|
1240
|
-
? pre.blocks
|
|
1241
|
-
: (await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] })).blocks;
|
|
1242
|
-
// An edit counts as covered only when EVERY block holding it got a
|
|
1243
|
-
// rewrite target. The per-block lane is bounded (run capture costs a read
|
|
1244
|
-
// each), so a find present in more blocks than the cap must fall through
|
|
1245
|
-
// to the select lane below, which carries `require: 'all'`. Marking such
|
|
1246
|
-
// an edit covered after its first match would replace only the first
|
|
1247
|
-
// PRESERVE_TARGET_CAP blocks and still report success.
|
|
1248
|
-
const blocksHoldingEdit = matchingEdits.map((edit) => scanBlocks.filter((block) => block.nodeId && block.text && textIncludes(block.text, edit.find, caseSensitive))
|
|
1249
|
-
.length);
|
|
1250
|
-
const blocksCoveredForEdit = Array.from({ length: matchingEdits.length }, () => 0);
|
|
1251
|
-
for (const block of scanBlocks) {
|
|
1252
|
-
if (preserveTargets.length >= PRESERVE_TARGET_CAP)
|
|
1253
|
-
break;
|
|
1254
|
-
if (!block.nodeId || !block.text)
|
|
1255
|
-
continue;
|
|
1256
|
-
let newText = block.text;
|
|
1257
|
-
const touched = [];
|
|
1258
|
-
matchingEdits.forEach((edit, editIndex) => {
|
|
1259
|
-
if (textIncludes(newText, edit.find, caseSensitive)) {
|
|
1260
|
-
newText = replaceAllText(newText, edit.find, edit.replace, caseSensitive);
|
|
1261
|
-
touched.push(editIndex);
|
|
1262
|
-
}
|
|
1263
|
-
});
|
|
1264
|
-
if (newText === block.text)
|
|
1265
|
-
continue;
|
|
1266
|
-
preserveTargets.push({
|
|
1267
|
-
nodeId: block.nodeId,
|
|
1268
|
-
nodeType: block.nodeType ?? 'paragraph',
|
|
1269
|
-
oldText: block.text,
|
|
1270
|
-
newText,
|
|
1271
|
-
runs: await captureBlockRuns(doc, block.nodeId, block.text),
|
|
1272
|
-
});
|
|
1273
|
-
for (const editIndex of touched)
|
|
1274
|
-
blocksCoveredForEdit[editIndex] += 1;
|
|
1275
|
-
}
|
|
1276
|
-
const coveredEdits = new Set(matchingEdits
|
|
1277
|
-
.map((_edit, editIndex) => editIndex)
|
|
1278
|
-
.filter((editIndex) => blocksHoldingEdit[editIndex] > 0 && blocksCoveredForEdit[editIndex] >= blocksHoldingEdit[editIndex]));
|
|
1279
|
-
steps = [
|
|
1280
|
-
...preserveTargets.map((entry, index) => ({
|
|
1281
|
-
id: `replace-block-${index + 1}`,
|
|
1282
|
-
op: 'text.rewrite',
|
|
1283
|
-
where: { by: 'block', nodeType: entry.nodeType, nodeId: entry.nodeId },
|
|
1284
|
-
args: {
|
|
1285
|
-
replacement: { text: entry.newText },
|
|
1286
|
-
style: preserveRewriteStyle(),
|
|
1287
|
-
},
|
|
1288
|
-
})),
|
|
1289
|
-
// Edits that touched no scanned body block (header/footer text, or a
|
|
1290
|
-
// doc larger than the scan window) keep the original select-rewrite.
|
|
1291
|
-
...matchingEdits
|
|
1292
|
-
.map((edit, editIndex) => ({ edit, editIndex }))
|
|
1293
|
-
.filter(({ editIndex }) => !coveredEdits.has(editIndex))
|
|
1294
|
-
.map(({ edit, editIndex }) => ({
|
|
1204
|
+
let candidates = args.edits.map((edit, editIndex) => ({
|
|
1205
|
+
edit,
|
|
1206
|
+
step: {
|
|
1295
1207
|
id: `replace-${editIndex + 1}`,
|
|
1296
1208
|
op: 'text.rewrite',
|
|
1297
1209
|
where: {
|
|
@@ -1308,10 +1220,42 @@ async function runReplaceText(doc, args) {
|
|
|
1308
1220
|
replacement: { text: edit.replace },
|
|
1309
1221
|
style: preserveRewriteStyle(),
|
|
1310
1222
|
},
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1223
|
+
},
|
|
1224
|
+
}));
|
|
1225
|
+
if (candidates.length > 1) {
|
|
1226
|
+
applyExpectedRevision = pre.revision;
|
|
1227
|
+
while (candidates.length > 0) {
|
|
1228
|
+
const preview = await previewMutations(doc, candidates.map((candidate) => candidate.step), args.changeMode, pre.revision);
|
|
1229
|
+
if (preview.valid === true)
|
|
1230
|
+
break;
|
|
1231
|
+
const failures = Array.isArray(preview.failures) ? preview.failures.filter(isRecord) : [];
|
|
1232
|
+
const absent = failures.find((failure) => failure.code === 'MATCH_NOT_FOUND' && typeof failure.stepId === 'string');
|
|
1233
|
+
if (!absent) {
|
|
1234
|
+
throw new Error(typeof failures[0]?.message === 'string' ? failures[0].message : 'replacement plan could not be prepared');
|
|
1235
|
+
}
|
|
1236
|
+
const absentIndex = candidates.findIndex((candidate) => candidate.step.id === absent.stepId);
|
|
1237
|
+
if (absentIndex < 0)
|
|
1238
|
+
throw new Error('replacement preview returned an unknown step id');
|
|
1239
|
+
skippedEdits.push({ find: candidates[absentIndex].edit.find });
|
|
1240
|
+
candidates = candidates.filter((_candidate, index) => index !== absentIndex);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (candidates.length === 0) {
|
|
1244
|
+
return {
|
|
1245
|
+
status: 'failed',
|
|
1246
|
+
intent: 'replace_text',
|
|
1247
|
+
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1248
|
+
selectedTargets: [],
|
|
1249
|
+
executedOperations: [],
|
|
1250
|
+
verification: [],
|
|
1251
|
+
errors: [
|
|
1252
|
+
{ code: 'ACTION_FAILED', message: 'none of the requested text replacements matched the current document' },
|
|
1253
|
+
],
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
steps = candidates.map((candidate) => candidate.step);
|
|
1313
1257
|
}
|
|
1314
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
1258
|
+
const result = await executeMutations(doc, steps, args.changeMode, applyExpectedRevision);
|
|
1315
1259
|
const preserved = [];
|
|
1316
1260
|
for (const entry of preserveTargets) {
|
|
1317
1261
|
const restored = await preserveRunPatternAfterRewrite(doc, entry.nodeId, entry.oldText, entry.runs, entry.newText);
|
|
@@ -1319,7 +1263,7 @@ async function runReplaceText(doc, args) {
|
|
|
1319
1263
|
preserved.push({ nodeId: entry.nodeId, ...restored });
|
|
1320
1264
|
}
|
|
1321
1265
|
if (args.selector && selectedTargets[0]) {
|
|
1322
|
-
const post = await docSnapshot.
|
|
1266
|
+
const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] });
|
|
1323
1267
|
const blockId = selectedTargets[0].matched[0];
|
|
1324
1268
|
const postTarget = findSnapshotTextByNodeId(post, blockId);
|
|
1325
1269
|
const preTarget = findSnapshotTextByNodeId(pre, blockId);
|
|
@@ -1388,7 +1332,7 @@ async function runDeleteText(doc, args) {
|
|
|
1388
1332
|
// path). Rewriting the block minus the finds keeps the deletion local — without
|
|
1389
1333
|
// this a short/whitespace find matches document-wide and blows the target cap.
|
|
1390
1334
|
if (args.selector) {
|
|
1391
|
-
const scopedPre = await docSnapshot.
|
|
1335
|
+
const scopedPre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: snapshotDomainsForSelector(args.selector) });
|
|
1392
1336
|
try {
|
|
1393
1337
|
if (args.finds.length === 0) {
|
|
1394
1338
|
return failedReceipt('delete_text', new Error('finds must be non-empty'), scopedPre);
|
|
@@ -1431,7 +1375,7 @@ async function runDeleteText(doc, args) {
|
|
|
1431
1375
|
args: { replacement: { text: rewritten }, style: preserveRewriteStyle() },
|
|
1432
1376
|
},
|
|
1433
1377
|
];
|
|
1434
|
-
const scopedResult = await executeMutations(doc, scopedSteps, args.changeMode);
|
|
1378
|
+
const scopedResult = await executeMutations(doc, scopedSteps, args.changeMode, scopedPre.revision);
|
|
1435
1379
|
const scopedPost = await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: ['blocks'] });
|
|
1436
1380
|
const verification = evaluateChecks(scopedPre, scopedPost, [{ kind: 'revision-changed' }]);
|
|
1437
1381
|
return {
|
|
@@ -1493,7 +1437,7 @@ async function runDeleteText(doc, args) {
|
|
|
1493
1437
|
},
|
|
1494
1438
|
args: {},
|
|
1495
1439
|
}));
|
|
1496
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
1440
|
+
const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
1497
1441
|
const revision = asRecord(asRecord(result)?.revision);
|
|
1498
1442
|
const postIdentity = args.changeMode === 'tracked'
|
|
1499
1443
|
? await readDocumentIdentity(doc)
|
|
@@ -1536,7 +1480,7 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1536
1480
|
for (const domain of snapshotDomainsForSelector(selector))
|
|
1537
1481
|
domains.add(domain);
|
|
1538
1482
|
}
|
|
1539
|
-
const pre = await docSnapshot.
|
|
1483
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: [...domains] });
|
|
1540
1484
|
try {
|
|
1541
1485
|
if (args.selectors.length === 0) {
|
|
1542
1486
|
return failedReceipt('delete_blocks', new Error('selectors must be non-empty'), pre);
|
|
@@ -1629,7 +1573,7 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1629
1573
|
});
|
|
1630
1574
|
}
|
|
1631
1575
|
}
|
|
1632
|
-
const post = await docSnapshot.
|
|
1576
|
+
const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: [...domains] });
|
|
1633
1577
|
// Tracked deletions leave the block in place until the revision is decided,
|
|
1634
1578
|
// so the block count cannot move — count the structural revisions instead.
|
|
1635
1579
|
// Direct deletions must show one fewer block of each deleted node type.
|
|
@@ -1747,7 +1691,7 @@ async function runInsertListItems(doc, args) {
|
|
|
1747
1691
|
}
|
|
1748
1692
|
}
|
|
1749
1693
|
async function runAddListItems(doc, args, opts) {
|
|
1750
|
-
const pre = await docSnapshot.
|
|
1694
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
1751
1695
|
try {
|
|
1752
1696
|
const needle = args.anchorText?.trim();
|
|
1753
1697
|
const anchorNodeId = args.anchorNodeId?.trim();
|
|
@@ -2280,56 +2224,79 @@ async function runSplitList(doc, args) {
|
|
|
2280
2224
|
return failedReceipt('split_list', err, pre);
|
|
2281
2225
|
}
|
|
2282
2226
|
}
|
|
2227
|
+
async function resolveFactPlacement(placement, facts) {
|
|
2228
|
+
if (!placement || placement.at === 'document_end')
|
|
2229
|
+
return { kind: 'documentEnd' };
|
|
2230
|
+
if (placement.at === 'document_start')
|
|
2231
|
+
return { kind: 'documentStart' };
|
|
2232
|
+
const target = await facts.target(placement.selector);
|
|
2233
|
+
if (!target)
|
|
2234
|
+
throw new errors.SuperDocCliError('placement selector did not resolve to a unique body block', {
|
|
2235
|
+
code: 'INVALID_ARGUMENT',
|
|
2236
|
+
});
|
|
2237
|
+
return { kind: placement.at, target: { kind: 'block', nodeType: target.nodeType, nodeId: target.nodeId } };
|
|
2238
|
+
}
|
|
2239
|
+
function placementSelectors(placement) {
|
|
2240
|
+
return placement?.at === 'before' || placement?.at === 'after' ? [placement.selector] : [];
|
|
2241
|
+
}
|
|
2283
2242
|
async function runCreateTable(doc, args) {
|
|
2284
|
-
const
|
|
2243
|
+
const checks = [
|
|
2244
|
+
{ kind: 'revision-changed' },
|
|
2245
|
+
{ kind: 'block-count-delta', nodeType: 'table', delta: 1 },
|
|
2246
|
+
];
|
|
2247
|
+
const context = new executionContext.ExecutionContext(doc, {
|
|
2248
|
+
evidence: args.evidence,
|
|
2249
|
+
selectors: placementSelectors(args.placement),
|
|
2250
|
+
checks,
|
|
2251
|
+
});
|
|
2285
2252
|
try {
|
|
2286
|
-
if (!Number.isInteger(args.rows) || args.rows < 1 || !Number.isInteger(args.columns) || args.columns < 1)
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
const placement =
|
|
2290
|
-
const
|
|
2291
|
-
const
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
const appliedCells = await applyTableCellTexts(doc, createdTableNodeId, insertedTableOrdinal, flattenCellTexts(args.cellTexts), args.changeMode);
|
|
2298
|
-
if (appliedCells.length > 0) {
|
|
2299
|
-
executedOperations.push({
|
|
2300
|
-
operationId: 'doc.mutations.apply',
|
|
2301
|
-
rationale: `Populated ${appliedCells.length} table cells.`,
|
|
2302
|
-
});
|
|
2253
|
+
if (!Number.isInteger(args.rows) || args.rows < 1 || !Number.isInteger(args.columns) || args.columns < 1)
|
|
2254
|
+
throw new Error('rows and columns must be positive integers');
|
|
2255
|
+
const pre = await context.start();
|
|
2256
|
+
const placement = await resolveFactPlacement(args.placement, pre);
|
|
2257
|
+
const result = await executeCreateTable(context.document, args, placement);
|
|
2258
|
+
const tableNodeId = asString(asRecord(asRecord(result)?.table)?.nodeId);
|
|
2259
|
+
if (args.cellTexts && tableNodeId) {
|
|
2260
|
+
const cells = flattenCellTexts(args.cellTexts).filter((cell) => cell.text.trim());
|
|
2261
|
+
if (pre.complete) {
|
|
2262
|
+
const applied = await applyTableCellTexts(context.document, tableNodeId, estimateInsertedTableOrdinal(pre.complete, placement), cells, args.changeMode);
|
|
2263
|
+
await styleTableCells(context.document, applied, args.changeMode);
|
|
2303
2264
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2265
|
+
else {
|
|
2266
|
+
for (const cell of cells)
|
|
2267
|
+
await context.document.tables.setCellText({
|
|
2268
|
+
target: { kind: 'block', nodeType: 'table', nodeId: tableNodeId },
|
|
2269
|
+
rowIndex: cell.rowIndex,
|
|
2270
|
+
columnIndex: cell.columnIndex,
|
|
2271
|
+
text: cell.text,
|
|
2272
|
+
changeMode: args.changeMode,
|
|
2273
|
+
});
|
|
2274
|
+
const styled = cells.filter((cell) => cell.runs?.length || cell.marks);
|
|
2275
|
+
if (styled.length) {
|
|
2276
|
+
const identities = await doc.tables.getCells({ nodeId: tableNodeId });
|
|
2277
|
+
const resolved = styled.map((cell) => {
|
|
2278
|
+
const identity = identities.cells.find((item) => item.rowIndex === cell.rowIndex &&
|
|
2279
|
+
item.columnIndex <= cell.columnIndex &&
|
|
2280
|
+
item.columnIndex + item.colspan > cell.columnIndex);
|
|
2281
|
+
if (!identity?.firstParagraphNodeId)
|
|
2282
|
+
throw new Error('Required cell paragraph identity is unavailable.');
|
|
2283
|
+
return { ...cell, nodeId: identity.firstParagraphNodeId };
|
|
2284
|
+
});
|
|
2285
|
+
const styling = await applyItemStyling(context.document, resolved.map((cell) => cell.nodeId), resolved, args.changeMode);
|
|
2286
|
+
if (styling.skipped)
|
|
2287
|
+
throw new Error(`Requested cell styling failed: ${styling.skipped}`);
|
|
2288
|
+
}
|
|
2310
2289
|
}
|
|
2311
2290
|
}
|
|
2312
|
-
const post = await
|
|
2313
|
-
|
|
2314
|
-
{ kind: 'revision-changed' },
|
|
2315
|
-
{ kind: 'block-count-delta', nodeType: 'table', delta: 1 },
|
|
2316
|
-
]);
|
|
2317
|
-
return {
|
|
2318
|
-
status: verification.every((v) => v.passed) ? 'ok' : 'failed',
|
|
2319
|
-
intent: 'create_table',
|
|
2320
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
2321
|
-
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
2322
|
-
selectedTargets: [],
|
|
2323
|
-
executedOperations,
|
|
2324
|
-
verification,
|
|
2325
|
-
};
|
|
2291
|
+
const post = await context.finish();
|
|
2292
|
+
return context.receipt('create_table', await executionContext.evaluateFactChecks(pre, post, checks));
|
|
2326
2293
|
}
|
|
2327
|
-
catch (
|
|
2328
|
-
return
|
|
2294
|
+
catch (error) {
|
|
2295
|
+
return context.failure('create_table', error);
|
|
2329
2296
|
}
|
|
2330
2297
|
}
|
|
2331
2298
|
async function runCommentParagraphs(doc, args) {
|
|
2332
|
-
const pre = await docSnapshot.
|
|
2299
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
2333
2300
|
try {
|
|
2334
2301
|
const blocks = pre.blocks.filter((b) => {
|
|
2335
2302
|
if (args.scope === 'all') {
|
|
@@ -2371,13 +2338,18 @@ async function runCommentParagraphs(doc, args) {
|
|
|
2371
2338
|
}
|
|
2372
2339
|
}
|
|
2373
2340
|
async function runAddComments(doc, args) {
|
|
2374
|
-
const
|
|
2341
|
+
const selectorList = args.selectors?.length ? args.selectors : args.selector ? [args.selector] : [];
|
|
2342
|
+
const domains = new Set(['blocks']);
|
|
2343
|
+
for (const selector of selectorList) {
|
|
2344
|
+
for (const domain of snapshotDomainsForSelector(selector))
|
|
2345
|
+
domains.add(domain);
|
|
2346
|
+
}
|
|
2347
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: [...domains] });
|
|
2375
2348
|
try {
|
|
2376
2349
|
// Batch form: `selectors` comments many blocks in ONE action call so the
|
|
2377
2350
|
// model never fans out N concurrent add_comments tool calls (which race the
|
|
2378
2351
|
// shared document and the comment-count verification). A single `selector`
|
|
2379
2352
|
// is the one-target shorthand. Comments are applied sequentially.
|
|
2380
|
-
const selectorList = args.selectors?.length ? args.selectors : args.selector ? [args.selector] : [];
|
|
2381
2353
|
if (!selectorList.length) {
|
|
2382
2354
|
return failedReceipt('add_comments', new Error('add_comments requires a "selector" or a non-empty "selectors" array'), pre);
|
|
2383
2355
|
}
|
|
@@ -2391,7 +2363,21 @@ async function runAddComments(doc, args) {
|
|
|
2391
2363
|
unresolved.push(sel);
|
|
2392
2364
|
}
|
|
2393
2365
|
if (!resolved.length) {
|
|
2394
|
-
return
|
|
2366
|
+
return {
|
|
2367
|
+
status: 'failed',
|
|
2368
|
+
intent: 'add_comments',
|
|
2369
|
+
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
2370
|
+
selectedTargets: unresolved.map((selector) => ({ selector, matched: [] })),
|
|
2371
|
+
executedOperations: [],
|
|
2372
|
+
verification: [],
|
|
2373
|
+
errors: [
|
|
2374
|
+
{
|
|
2375
|
+
code: 'TARGET_NOT_FOUND',
|
|
2376
|
+
message: 'none of the requested comment selectors resolved to a block',
|
|
2377
|
+
recovery: { kind: 'reinspect' },
|
|
2378
|
+
},
|
|
2379
|
+
],
|
|
2380
|
+
};
|
|
2395
2381
|
}
|
|
2396
2382
|
const executed = [];
|
|
2397
2383
|
for (const { commentText, nodeId } of resolved.map((r) => ({ commentText: args.commentText, nodeId: r.nodeId }))) {
|
|
@@ -2400,15 +2386,20 @@ async function runAddComments(doc, args) {
|
|
|
2400
2386
|
}
|
|
2401
2387
|
const post = await docSnapshot.buildDocumentSnapshot(doc);
|
|
2402
2388
|
const verification = evaluateChecks(pre, post, [{ kind: 'comment-count-delta', delta: resolved.length }]);
|
|
2389
|
+
const verified = verification.every((v) => v.passed);
|
|
2390
|
+
const status = !verified ? 'failed' : unresolved.length > 0 ? 'partial' : 'ok';
|
|
2403
2391
|
return {
|
|
2404
|
-
status
|
|
2392
|
+
status,
|
|
2405
2393
|
intent: 'add_comments',
|
|
2406
2394
|
...(unresolved.length
|
|
2407
2395
|
? { note: `${unresolved.length} selector(s) did not resolve to a block and were skipped` }
|
|
2408
2396
|
: {}),
|
|
2409
2397
|
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
2410
2398
|
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
2411
|
-
selectedTargets:
|
|
2399
|
+
selectedTargets: [
|
|
2400
|
+
...resolved.map((r) => ({ selector: r.selector, matched: [r.nodeId] })),
|
|
2401
|
+
...unresolved.map((selector) => ({ selector, matched: [] })),
|
|
2402
|
+
],
|
|
2412
2403
|
executedOperations: executed,
|
|
2413
2404
|
verification,
|
|
2414
2405
|
};
|
|
@@ -2584,67 +2575,39 @@ async function runReplyToComment(doc, args) {
|
|
|
2584
2575
|
}
|
|
2585
2576
|
}
|
|
2586
2577
|
async function runRewriteBlock(doc, args) {
|
|
2587
|
-
const
|
|
2578
|
+
const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, selectors: [args.selector] });
|
|
2588
2579
|
try {
|
|
2589
|
-
const
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
// it can be re-applied over the unchanged prefix/suffix afterward.
|
|
2598
|
-
const preRuns = await captureBlockRuns(doc, target.nodeId, target.text);
|
|
2599
|
-
const steps = [
|
|
2580
|
+
const pre = await context.start();
|
|
2581
|
+
const target = await pre.target(args.selector);
|
|
2582
|
+
if (!target)
|
|
2583
|
+
throw new Error('selector did not resolve to a body block');
|
|
2584
|
+
context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
2585
|
+
const text = normalizeTitleLikeRewriteText(target.text, args.text);
|
|
2586
|
+
const runs = await captureBlockRuns(doc, target.nodeId, target.text);
|
|
2587
|
+
await executeMutations(context.document, [
|
|
2600
2588
|
{
|
|
2601
2589
|
id: 'rewrite-block-1',
|
|
2602
2590
|
op: 'text.rewrite',
|
|
2603
2591
|
where: { by: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
|
|
2604
|
-
args: {
|
|
2605
|
-
replacement: { text: normalizedText },
|
|
2606
|
-
style: preserveRewriteStyle(),
|
|
2607
|
-
},
|
|
2592
|
+
args: { replacement: { text }, style: preserveRewriteStyle() },
|
|
2608
2593
|
},
|
|
2609
|
-
];
|
|
2610
|
-
|
|
2611
|
-
const
|
|
2612
|
-
const
|
|
2613
|
-
const rewrittenBlock = findSnapshotTextByNodeId(post, target.nodeId);
|
|
2594
|
+
], args.changeMode, pre.revision);
|
|
2595
|
+
await preserveRunPatternAfterRewrite(context.document, target.nodeId, target.text, runs, text);
|
|
2596
|
+
const post = await context.finish();
|
|
2597
|
+
const rewritten = await post.block(target.nodeId);
|
|
2614
2598
|
const verification = [
|
|
2615
2599
|
revisionVerification(pre.revision, post.revision, true),
|
|
2616
2600
|
{
|
|
2617
|
-
check: { kind: 'block-text-contains', nodeId: target.nodeId, text
|
|
2618
|
-
passed: !!
|
|
2601
|
+
check: { kind: 'block-text-contains', nodeId: target.nodeId, text },
|
|
2602
|
+
passed: !!rewritten && verifyRewrittenBlockText(rewritten.text, text, args.changeMode),
|
|
2619
2603
|
},
|
|
2620
2604
|
];
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
status: changed && rewritten ? 'ok' : 'failed',
|
|
2625
|
-
intent: 'rewrite_block',
|
|
2626
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
2627
|
-
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
2628
|
-
selectedTargets: [{ selector: args.selector, matched: [target.nodeId] }],
|
|
2629
|
-
executedOperations: [
|
|
2630
|
-
{ operationId: 'doc.mutations.apply', result },
|
|
2631
|
-
...(preservation ? [{ operationId: 'doc.format.apply', result: preservation }] : []),
|
|
2632
|
-
],
|
|
2633
|
-
verification,
|
|
2634
|
-
errors: changed && rewritten
|
|
2635
|
-
? undefined
|
|
2636
|
-
: [
|
|
2637
|
-
{
|
|
2638
|
-
code: 'ACTION_FAILED',
|
|
2639
|
-
message: changed
|
|
2640
|
-
? 'rewrite_block did not produce the requested rewritten text for the selected block'
|
|
2641
|
-
: 'rewrite_block produced no change for the selected block; keep the same target and provide a changed rewrite',
|
|
2642
|
-
},
|
|
2643
|
-
],
|
|
2644
|
-
};
|
|
2605
|
+
return context.receipt('rewrite_block', verification, verification.every((check) => check.passed)
|
|
2606
|
+
? {}
|
|
2607
|
+
: { errors: [{ code: 'ACTION_FAILED', message: 'rewrite_block did not produce the requested changed text' }] });
|
|
2645
2608
|
}
|
|
2646
|
-
catch (
|
|
2647
|
-
return
|
|
2609
|
+
catch (error) {
|
|
2610
|
+
return context.failure('rewrite_block', error);
|
|
2648
2611
|
}
|
|
2649
2612
|
}
|
|
2650
2613
|
const RUN_BOOL_MARKS = ['bold', 'italic', 'underline', 'strike'];
|
|
@@ -3018,7 +2981,7 @@ async function listAllTrackedChanges(listFn) {
|
|
|
3018
2981
|
}
|
|
3019
2982
|
}
|
|
3020
2983
|
async function runNormalizeBodyFontSize(doc, args) {
|
|
3021
|
-
const pre = await docSnapshot.
|
|
2984
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3022
2985
|
try {
|
|
3023
2986
|
if (!Number.isFinite(args.fontSize) || args.fontSize <= 0) {
|
|
3024
2987
|
return failedReceipt('normalize_body_font_size', new Error('fontSize must be a positive number'), pre);
|
|
@@ -3035,7 +2998,7 @@ async function runNormalizeBodyFontSize(doc, args) {
|
|
|
3035
2998
|
where: { by: 'block', nodeType: block.nodeType, nodeId: block.nodeId },
|
|
3036
2999
|
args: { inline: { fontSize: args.fontSize }, scope: 'block' },
|
|
3037
3000
|
}));
|
|
3038
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
3001
|
+
const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
3039
3002
|
const post = await docSnapshot.buildDocumentSnapshot(doc);
|
|
3040
3003
|
const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
|
|
3041
3004
|
return {
|
|
@@ -3066,7 +3029,7 @@ async function runNormalizeBodyFontSize(doc, args) {
|
|
|
3066
3029
|
* tracked-safe (changeMode).
|
|
3067
3030
|
*/
|
|
3068
3031
|
async function runSetFontFamily(doc, args) {
|
|
3069
|
-
const pre = await docSnapshot.
|
|
3032
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
3070
3033
|
try {
|
|
3071
3034
|
const fontFamily = args.fontFamily.trim();
|
|
3072
3035
|
if (fontFamily.length === 0) {
|
|
@@ -3193,7 +3156,7 @@ async function runSetFontFamily(doc, args) {
|
|
|
3193
3156
|
* the recreated block inside an adjacent table cell.
|
|
3194
3157
|
*/
|
|
3195
3158
|
async function runApplyStyle(doc, args) {
|
|
3196
|
-
const pre = await docSnapshot.
|
|
3159
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3197
3160
|
try {
|
|
3198
3161
|
const target = selectorToBlockTarget(args.selector, pre);
|
|
3199
3162
|
if (!target) {
|
|
@@ -3295,43 +3258,30 @@ async function runApplyStyle(doc, args) {
|
|
|
3295
3258
|
* escape to superdoc_execute_code and apply the alignment untracked (no pPrChange).
|
|
3296
3259
|
*/
|
|
3297
3260
|
async function runFormatParagraph(doc, args) {
|
|
3298
|
-
const
|
|
3261
|
+
const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, selectors: [args.selector] });
|
|
3299
3262
|
try {
|
|
3300
|
-
const target = selectorToBlockTarget(args.selector, pre);
|
|
3301
|
-
if (!target) {
|
|
3302
|
-
return failedReceipt('format_paragraph', new Error('selector did not resolve to a unique body block'), pre);
|
|
3303
|
-
}
|
|
3304
3263
|
const alignment = typeof args.alignment === 'string' ? args.alignment.trim().toLowerCase() : '';
|
|
3305
|
-
const
|
|
3306
|
-
if (!['left', 'center', 'right', 'justify'].includes(
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
const
|
|
3264
|
+
const normalized = alignment === 'both' ? 'justify' : alignment;
|
|
3265
|
+
if (!['left', 'center', 'right', 'justify'].includes(normalized))
|
|
3266
|
+
throw new Error('format_paragraph requires alignment: left | center | right | justify');
|
|
3267
|
+
const pre = await context.start();
|
|
3268
|
+
const target = await pre.target(args.selector);
|
|
3269
|
+
if (!target)
|
|
3270
|
+
throw new Error('selector did not resolve to a unique body block');
|
|
3271
|
+
context.selectedTargets.push({ selector: args.selector, matched: [target.nodeId] });
|
|
3272
|
+
await executeMutations(context.document, [
|
|
3310
3273
|
{
|
|
3311
3274
|
id: 'format-paragraph-1',
|
|
3312
3275
|
op: 'format.apply',
|
|
3313
3276
|
where: { by: 'block', nodeType: target.nodeType, nodeId: target.nodeId },
|
|
3314
|
-
args: { alignment:
|
|
3277
|
+
args: { alignment: normalized, scope: 'block' },
|
|
3315
3278
|
},
|
|
3316
|
-
];
|
|
3317
|
-
const
|
|
3318
|
-
|
|
3319
|
-
const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
|
|
3320
|
-
return {
|
|
3321
|
-
status: verification.every((v) => v.passed) ? 'ok' : 'failed',
|
|
3322
|
-
intent: `format_paragraph: ${normalizedAlignment}`,
|
|
3323
|
-
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
3324
|
-
postSnapshot: { revision: post.revision, counts: post.counts },
|
|
3325
|
-
selectedTargets: [{ selector: args.selector, matched: [target.nodeId] }],
|
|
3326
|
-
applied: { alignment: normalizedAlignment },
|
|
3327
|
-
executedOperations: [
|
|
3328
|
-
{ operationId: 'doc.mutations.apply', result: compactOpResult(result), rationale: `align ${target.nodeId}` },
|
|
3329
|
-
],
|
|
3330
|
-
verification,
|
|
3331
|
-
};
|
|
3279
|
+
], args.changeMode, pre.revision);
|
|
3280
|
+
const post = await context.finish();
|
|
3281
|
+
return context.receipt(`format_paragraph: ${normalized}`, await executionContext.evaluateFactChecks(pre, post, [{ kind: 'revision-changed' }]), { applied: { alignment: normalized } });
|
|
3332
3282
|
}
|
|
3333
|
-
catch (
|
|
3334
|
-
return
|
|
3283
|
+
catch (error) {
|
|
3284
|
+
return context.failure('format_paragraph', error);
|
|
3335
3285
|
}
|
|
3336
3286
|
}
|
|
3337
3287
|
/**
|
|
@@ -3349,7 +3299,7 @@ async function runFormatParagraph(doc, args) {
|
|
|
3349
3299
|
* still anchor on the (now tracked-deleted) source span and land right after it.
|
|
3350
3300
|
*/
|
|
3351
3301
|
async function runMoveText(doc, args) {
|
|
3352
|
-
const pre = await docSnapshot.
|
|
3302
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
3353
3303
|
try {
|
|
3354
3304
|
if (!args.text || args.text.length === 0) {
|
|
3355
3305
|
return failedReceipt('move_text', new Error('text (the exact source span to move) is required'), pre);
|
|
@@ -3466,7 +3416,7 @@ async function runMoveText(doc, args) {
|
|
|
3466
3416
|
* instead of inserting blank paragraphs.
|
|
3467
3417
|
*/
|
|
3468
3418
|
async function runSetParagraphSpacing(doc, args) {
|
|
3469
|
-
const pre = await docSnapshot.
|
|
3419
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3470
3420
|
try {
|
|
3471
3421
|
const target = selectorToBlockTarget(args.selector, pre);
|
|
3472
3422
|
if (!target) {
|
|
@@ -3523,7 +3473,7 @@ async function runSetParagraphSpacing(doc, args) {
|
|
|
3523
3473
|
* THE way to "start X on a new page" instead of padding with empty paragraphs.
|
|
3524
3474
|
*/
|
|
3525
3475
|
async function runInsertPageBreak(doc, args) {
|
|
3526
|
-
const pre = await docSnapshot.
|
|
3476
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3527
3477
|
try {
|
|
3528
3478
|
const target = selectorToBlockTarget(args.selector, pre);
|
|
3529
3479
|
if (!target) {
|
|
@@ -3560,7 +3510,7 @@ async function runInsertPageBreak(doc, args) {
|
|
|
3560
3510
|
* Finds the text in the body and applies a link over its range.
|
|
3561
3511
|
*/
|
|
3562
3512
|
async function runAddHyperlink(doc, args) {
|
|
3563
|
-
const pre = await docSnapshot.
|
|
3513
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3564
3514
|
try {
|
|
3565
3515
|
const needle = args.text;
|
|
3566
3516
|
const url = args.url;
|
|
@@ -3618,7 +3568,7 @@ async function runAddHyperlink(doc, args) {
|
|
|
3618
3568
|
* dialect, and silently apply the formatting untracked.
|
|
3619
3569
|
*/
|
|
3620
3570
|
async function runFormatText(doc, args) {
|
|
3621
|
-
const pre = await docSnapshot.
|
|
3571
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
3622
3572
|
try {
|
|
3623
3573
|
const inline = {};
|
|
3624
3574
|
if (args.bold === true)
|
|
@@ -3779,7 +3729,7 @@ function findRanges(haystack, needle, caseSensitive) {
|
|
|
3779
3729
|
return ranges;
|
|
3780
3730
|
}
|
|
3781
3731
|
async function runApplyLetterSpacing(doc, args) {
|
|
3782
|
-
const pre = await docSnapshot.
|
|
3732
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
3783
3733
|
try {
|
|
3784
3734
|
if (!Number.isFinite(args.letterSpacing)) {
|
|
3785
3735
|
return failedReceipt('apply_letter_spacing', new Error('letterSpacing must be a finite number'), pre);
|
|
@@ -3799,7 +3749,7 @@ async function runApplyLetterSpacing(doc, args) {
|
|
|
3799
3749
|
args: { inline: { letterSpacing: args.letterSpacing }, scope: 'block' },
|
|
3800
3750
|
},
|
|
3801
3751
|
];
|
|
3802
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
3752
|
+
const result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
3803
3753
|
const post = await docSnapshot.buildDocumentSnapshot(doc);
|
|
3804
3754
|
const verification = evaluateChecks(pre, post, [{ kind: 'revision-changed' }]);
|
|
3805
3755
|
return {
|
|
@@ -3957,7 +3907,10 @@ async function runMoveRange(doc, args) {
|
|
|
3957
3907
|
}
|
|
3958
3908
|
}
|
|
3959
3909
|
async function runInsertToc(doc, args) {
|
|
3960
|
-
const
|
|
3910
|
+
const placement = args.placement ?? { at: 'document_start' };
|
|
3911
|
+
const pre = placement.at === 'before' || placement.at === 'after'
|
|
3912
|
+
? await docSnapshot.buildMutationSnapshot(doc)
|
|
3913
|
+
: await docSnapshot.buildDocumentSnapshot(doc);
|
|
3961
3914
|
try {
|
|
3962
3915
|
const tocFn = maybeMethod(doc, ['create', 'tableOfContents']);
|
|
3963
3916
|
if (!tocFn) {
|
|
@@ -3965,11 +3918,11 @@ async function runInsertToc(doc, args) {
|
|
|
3965
3918
|
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
3966
3919
|
});
|
|
3967
3920
|
}
|
|
3968
|
-
const
|
|
3921
|
+
const resolvedPlacement = resolvePlacement(placement, pre);
|
|
3969
3922
|
const executed = [];
|
|
3970
|
-
let tocPlacement =
|
|
3923
|
+
let tocPlacement = resolvedPlacement;
|
|
3971
3924
|
if (args.title) {
|
|
3972
|
-
const headingResult = await executeCreateHeading(doc, args.title, 1,
|
|
3925
|
+
const headingResult = await executeCreateHeading(doc, args.title, 1, resolvedPlacement, args.changeMode);
|
|
3973
3926
|
executed.push({ operationId: 'doc.create.heading', result: headingResult });
|
|
3974
3927
|
const headingNodeId = asString(asRecord(asRecord(headingResult)?.heading)?.nodeId);
|
|
3975
3928
|
if (headingNodeId) {
|
|
@@ -4096,7 +4049,7 @@ async function runStyleTable(doc, args) {
|
|
|
4096
4049
|
* should reach for instead of delete-and-recreate or insert/undo churn.
|
|
4097
4050
|
*/
|
|
4098
4051
|
async function runMoveTable(doc, args) {
|
|
4099
|
-
const pre = await docSnapshot.
|
|
4052
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc);
|
|
4100
4053
|
try {
|
|
4101
4054
|
const tableOrdinal = args.tableOrdinal ?? 1;
|
|
4102
4055
|
const table = await resolveTableContextQuick(doc, tableOrdinal);
|
|
@@ -4622,8 +4575,8 @@ function parseScopedReplaceArgs(args) {
|
|
|
4622
4575
|
changeMode: args.changeMode === 'tracked' ? 'tracked' : 'direct',
|
|
4623
4576
|
};
|
|
4624
4577
|
}
|
|
4625
|
-
async function
|
|
4626
|
-
const pre = await docSnapshot.
|
|
4578
|
+
async function runScopedReplaceUnchecked(doc, args) {
|
|
4579
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
4627
4580
|
const matched = docSnapshot.resolveSnapshotSelector(pre, args.selector);
|
|
4628
4581
|
if (matched.length !== 1) {
|
|
4629
4582
|
const failure = describeSelectorFailure(args.selector, matched, pre, args.edits.map((e) => e.find));
|
|
@@ -4720,7 +4673,7 @@ async function runScopedReplace(doc, args) {
|
|
|
4720
4673
|
// applies directly, so restoring marks adds no second reviewable change.
|
|
4721
4674
|
const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
|
|
4722
4675
|
try {
|
|
4723
|
-
result = await executeMutations(doc, spanSteps, args.changeMode);
|
|
4676
|
+
result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
|
|
4724
4677
|
if (result != null)
|
|
4725
4678
|
await preserveScopedRunPattern(doc, nodeId, block.text, trackedPreRuns, planned);
|
|
4726
4679
|
}
|
|
@@ -4742,10 +4695,10 @@ async function runScopedReplace(doc, args) {
|
|
|
4742
4695
|
args: { replacement: { text: expected }, style: preserveRewriteStyle() },
|
|
4743
4696
|
},
|
|
4744
4697
|
];
|
|
4745
|
-
result = await executeMutations(doc, steps, args.changeMode);
|
|
4698
|
+
result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
4746
4699
|
await preserveScopedRunPattern(doc, nodeId, block.text, preRuns, planned);
|
|
4747
4700
|
}
|
|
4748
|
-
const post = await docSnapshot.
|
|
4701
|
+
const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
4749
4702
|
const postBlock = findTextTarget(post, nodeId);
|
|
4750
4703
|
// In tracked mode the deleted text is still present in the block, so only
|
|
4751
4704
|
// require the inserted text to be visible.
|
|
@@ -4784,6 +4737,14 @@ async function runScopedReplace(doc, args) {
|
|
|
4784
4737
|
],
|
|
4785
4738
|
};
|
|
4786
4739
|
}
|
|
4740
|
+
async function runScopedReplace(doc, args) {
|
|
4741
|
+
try {
|
|
4742
|
+
return await runScopedReplaceUnchecked(doc, args);
|
|
4743
|
+
}
|
|
4744
|
+
catch (error) {
|
|
4745
|
+
return failedReceipt('replace_text', error);
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4787
4748
|
/**
|
|
4788
4749
|
* Build a `doc.format.apply` `inline` payload from a block row's sampled look.
|
|
4789
4750
|
* Shared by apply_style (likeText copy) and add_list_items (anchor auto-match)
|
|
@@ -4839,12 +4800,6 @@ function paragraphMarkRunPropsOf(payload) {
|
|
|
4839
4800
|
* Spaced dashes are listed separately from bare ones because the label run
|
|
4840
4801
|
* usually stops BEFORE the space ("JavaScript / TypeScript" + " — SuperDoc…").
|
|
4841
4802
|
*/
|
|
4842
|
-
/**
|
|
4843
|
-
* How many blocks the per-block replace lane will capture runs for. Each target
|
|
4844
|
-
* costs a `query.match` read, so the lane is bounded; edits reaching past it
|
|
4845
|
-
* fall through to the select lane rather than being silently truncated.
|
|
4846
|
-
*/
|
|
4847
|
-
const PRESERVE_TARGET_CAP = 20;
|
|
4848
4803
|
const LEAD_IN_DELIMITERS = [':', ' — ', ' – ', ' - ', '—', '–'];
|
|
4849
4804
|
/** A lead-in is a LABEL, not a sentence — longer spans are prose, not a pattern. */
|
|
4850
4805
|
const LEAD_IN_MAX_CHARS = 90;
|
|
@@ -5327,8 +5282,18 @@ async function listBlockRows(doc) {
|
|
|
5327
5282
|
const fn = maybeMethod(doc, ['blocks', 'list']);
|
|
5328
5283
|
if (!fn)
|
|
5329
5284
|
return [];
|
|
5330
|
-
const
|
|
5331
|
-
|
|
5285
|
+
const rows = [];
|
|
5286
|
+
const pageSize = 250;
|
|
5287
|
+
let offset = 0;
|
|
5288
|
+
while (true) {
|
|
5289
|
+
const raw = (await fn({ offset, limit: pageSize }));
|
|
5290
|
+
const page = Array.isArray(raw?.blocks) ? raw.blocks : [];
|
|
5291
|
+
rows.push(...page);
|
|
5292
|
+
offset += page.length;
|
|
5293
|
+
const total = typeof raw?.total === 'number' && Number.isFinite(raw.total) ? raw.total : rows.length;
|
|
5294
|
+
if (page.length === 0 || offset >= total)
|
|
5295
|
+
return rows;
|
|
5296
|
+
}
|
|
5332
5297
|
}
|
|
5333
5298
|
function blockNumbering(row) {
|
|
5334
5299
|
const numbering = row?.numbering;
|
|
@@ -5494,7 +5459,7 @@ async function convertNumberedRange(doc, fromMarker, toMarker, kind, tracked) {
|
|
|
5494
5459
|
};
|
|
5495
5460
|
}
|
|
5496
5461
|
async function convertParagraphRange(doc, fromText, toText, kind, tracked) {
|
|
5497
|
-
const pre = await docSnapshot.
|
|
5462
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
5498
5463
|
const blocks = pre.blocks ?? [];
|
|
5499
5464
|
const findBlock = (needle) => blocks.findIndex((b) => b.nodeType === 'paragraph' && (b.text ?? '').toLowerCase().includes(needle.toLowerCase()));
|
|
5500
5465
|
const fromIdx = findBlock(fromText);
|
|
@@ -5538,7 +5503,7 @@ async function convertParagraphRange(doc, fromText, toText, kind, tracked) {
|
|
|
5538
5503
|
};
|
|
5539
5504
|
}
|
|
5540
5505
|
await createListFromParagraphRange(createFn, kind, range[0].nodeId, range[range.length - 1].nodeId, tracked ? 'tracked' : undefined);
|
|
5541
|
-
const post = await docSnapshot.
|
|
5506
|
+
const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
|
|
5542
5507
|
const postList = (post.lists ?? []).find((l) => l.items.some((it) => it.nodeId === range[0].nodeId));
|
|
5543
5508
|
const allInOneList = postList != null && range.every((b) => postList.items.some((it) => it.nodeId === b.nodeId));
|
|
5544
5509
|
return {
|
|
@@ -5807,7 +5772,7 @@ async function appendListAtPlacement(doc, args) {
|
|
|
5807
5772
|
const selector = placement && isRecord(placement.selector) ? placement.selector : null;
|
|
5808
5773
|
if (items.length === 0 || !placement || (at !== 'after' && at !== 'before') || !selector)
|
|
5809
5774
|
return null;
|
|
5810
|
-
const pre = await docSnapshot.
|
|
5775
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
5811
5776
|
const blocks = pre.blocks ?? [];
|
|
5812
5777
|
// Resolve through the SHARED selector resolver so the full selector
|
|
5813
5778
|
// vocabulary works (nodeId / textSearch / ordinal / relative / ref) and
|
|
@@ -5868,7 +5833,7 @@ async function appendListAtPlacement(doc, args) {
|
|
|
5868
5833
|
const listIds = typeof args.headingText === 'string' && args.headingText.length > 0 ? createdIds.slice(1) : createdIds;
|
|
5869
5834
|
await createListFromParagraphRange(listsCreateFn, kind, listIds[0], listIds[listIds.length - 1], parseChangeMode(args.changeMode));
|
|
5870
5835
|
// Verify both promises: the items form one list AND it sits at the anchor.
|
|
5871
|
-
const post = await docSnapshot.
|
|
5836
|
+
const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
|
|
5872
5837
|
const postBlocks = post.blocks ?? [];
|
|
5873
5838
|
const postList = (post.lists ?? []).find((l) => l.items.some((it) => it.nodeId === listIds[0]));
|
|
5874
5839
|
const listOk = postList != null && listIds.every((id) => postList.items.some((it) => it.nodeId === id));
|
|
@@ -5972,8 +5937,9 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5972
5937
|
const formatApplyFn = maybeMethod(doc, ['format', 'apply']);
|
|
5973
5938
|
if (formatApplyFn) {
|
|
5974
5939
|
const textLength = (created.textPreview ?? '').length;
|
|
5975
|
-
const
|
|
5976
|
-
|
|
5940
|
+
const full = createdText ||
|
|
5941
|
+
(await doc.blocks.list({ nodeIds: [created.nodeId], limit: 1, includeText: true })).blocks.find((block) => block.nodeId === created.nodeId)?.text ||
|
|
5942
|
+
'';
|
|
5977
5943
|
await formatApplyFn({
|
|
5978
5944
|
blockId: created.nodeId,
|
|
5979
5945
|
start: 0,
|
|
@@ -6170,11 +6136,11 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
|
|
|
6170
6136
|
}
|
|
6171
6137
|
return Object.keys(applied).length > 0 ? { matchedSibling: reference.nodeId, ...applied } : null;
|
|
6172
6138
|
}
|
|
6173
|
-
async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args) {
|
|
6139
|
+
async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, availableRows) {
|
|
6174
6140
|
if (receipt.status !== 'ok')
|
|
6175
6141
|
return receipt;
|
|
6176
6142
|
try {
|
|
6177
|
-
const postBlocks = await listBlockRows(doc);
|
|
6143
|
+
const postBlocks = availableRows ?? (await listBlockRows(doc));
|
|
6178
6144
|
const preIds = new Set(preBlocks.map((b) => b.nodeId));
|
|
6179
6145
|
const createdRows = postBlocks
|
|
6180
6146
|
.map((b, i) => ({ row: b, index: i }))
|
|
@@ -6431,7 +6397,7 @@ async function runAttachNumbering(doc, args) {
|
|
|
6431
6397
|
verification: [{ check: { kind: 'marker-rendered' }, passed: !!newMarker }],
|
|
6432
6398
|
};
|
|
6433
6399
|
}
|
|
6434
|
-
async function
|
|
6400
|
+
async function dispatchSuperdocPerformAction(doc, args) {
|
|
6435
6401
|
if (!isRecord(args)) {
|
|
6436
6402
|
throw new errors.SuperDocCliError('superdoc_perform_action arguments must be an object', {
|
|
6437
6403
|
code: 'INVALID_ARGUMENT',
|
|
@@ -6465,9 +6431,9 @@ async function superdocPerformAction(doc, args) {
|
|
|
6465
6431
|
});
|
|
6466
6432
|
}
|
|
6467
6433
|
const headingLevel = asNumber(args.headingLevel);
|
|
6468
|
-
|
|
6469
|
-
const receipt = await runInsertParagraphs(doc, {
|
|
6434
|
+
return runInsertParagraphs(doc, {
|
|
6470
6435
|
action,
|
|
6436
|
+
evidence: args.evidence,
|
|
6471
6437
|
texts,
|
|
6472
6438
|
textSpecs,
|
|
6473
6439
|
placement: parsePlacement(args.placement),
|
|
@@ -6475,8 +6441,7 @@ async function superdocPerformAction(doc, args) {
|
|
|
6475
6441
|
headingLevel: headingLevel != null && Number.isInteger(headingLevel) && headingLevel >= 1 && headingLevel <= 6
|
|
6476
6442
|
? headingLevel
|
|
6477
6443
|
: undefined,
|
|
6478
|
-
});
|
|
6479
|
-
return matchInsertedBlockFormatting(doc, preRows, receipt, args);
|
|
6444
|
+
}, args);
|
|
6480
6445
|
}
|
|
6481
6446
|
case 'insert_heading': {
|
|
6482
6447
|
const text = asString(args.text);
|
|
@@ -6600,6 +6565,7 @@ async function superdocPerformAction(doc, args) {
|
|
|
6600
6565
|
}
|
|
6601
6566
|
return runCreateTable(doc, {
|
|
6602
6567
|
action,
|
|
6568
|
+
evidence: args.evidence,
|
|
6603
6569
|
rows,
|
|
6604
6570
|
columns,
|
|
6605
6571
|
cellTexts: parseCellTexts(args.cellTexts),
|
|
@@ -6623,16 +6589,41 @@ async function superdocPerformAction(doc, args) {
|
|
|
6623
6589
|
}
|
|
6624
6590
|
case 'add_comments': {
|
|
6625
6591
|
const commentText = asString(args.commentText);
|
|
6626
|
-
|
|
6627
|
-
const selectors = Array.isArray(args.selectors)
|
|
6628
|
-
? args.selectors.map((s) => parseSelector(s)).filter((s) => Boolean(s))
|
|
6629
|
-
: undefined;
|
|
6630
|
-
if (!commentText || (!selector && !(selectors && selectors.length))) {
|
|
6592
|
+
if (!commentText) {
|
|
6631
6593
|
throw new errors.SuperDocCliError('add_comments requires "commentText" and a "selector" or non-empty "selectors" array', {
|
|
6632
6594
|
code: 'INVALID_ARGUMENT',
|
|
6633
6595
|
});
|
|
6634
6596
|
}
|
|
6635
|
-
|
|
6597
|
+
const hasSelector = args.selector != null;
|
|
6598
|
+
const hasSelectors = args.selectors != null;
|
|
6599
|
+
if (hasSelector && hasSelectors) {
|
|
6600
|
+
throw new errors.SuperDocCliError('add_comments requires either "selector" or "selectors", not both', {
|
|
6601
|
+
code: 'INVALID_ARGUMENT',
|
|
6602
|
+
});
|
|
6603
|
+
}
|
|
6604
|
+
if (hasSelectors) {
|
|
6605
|
+
if (!Array.isArray(args.selectors) || args.selectors.length === 0) {
|
|
6606
|
+
throw new errors.SuperDocCliError('add_comments requires "selectors" to be a non-empty array', {
|
|
6607
|
+
code: 'INVALID_ARGUMENT',
|
|
6608
|
+
});
|
|
6609
|
+
}
|
|
6610
|
+
// Parse the complete batch before mutation. Filtering an invalid entry
|
|
6611
|
+
// would make the receipt report success for only part of the request.
|
|
6612
|
+
const selectors = [];
|
|
6613
|
+
for (const [index, raw] of args.selectors.entries()) {
|
|
6614
|
+
const parsed = parseSelector(raw);
|
|
6615
|
+
if (!parsed) {
|
|
6616
|
+
throw new errors.SuperDocCliError(`add_comments selectors[${index}] is not a valid selector; no comments were added`, { code: 'INVALID_ARGUMENT' });
|
|
6617
|
+
}
|
|
6618
|
+
selectors.push(parsed);
|
|
6619
|
+
}
|
|
6620
|
+
return runAddComments(doc, { action, commentText, selectors });
|
|
6621
|
+
}
|
|
6622
|
+
const selector = parseSelector(args.selector);
|
|
6623
|
+
if (!selector) {
|
|
6624
|
+
throw new errors.SuperDocCliError('add_comments requires "selector" to be valid or "selectors" to be a non-empty array', { code: 'INVALID_ARGUMENT' });
|
|
6625
|
+
}
|
|
6626
|
+
return runAddComments(doc, { action, commentText, selector });
|
|
6636
6627
|
}
|
|
6637
6628
|
case 'resolve_comments':
|
|
6638
6629
|
return runResolveComments(doc, {
|
|
@@ -6661,6 +6652,7 @@ async function superdocPerformAction(doc, args) {
|
|
|
6661
6652
|
}
|
|
6662
6653
|
return runRewriteBlock(doc, {
|
|
6663
6654
|
action,
|
|
6655
|
+
evidence: args.evidence,
|
|
6664
6656
|
text,
|
|
6665
6657
|
selector,
|
|
6666
6658
|
changeMode: parseChangeMode(args.changeMode),
|
|
@@ -6912,6 +6904,7 @@ async function superdocPerformAction(doc, args) {
|
|
|
6912
6904
|
}
|
|
6913
6905
|
return runFormatParagraph(doc, {
|
|
6914
6906
|
action,
|
|
6907
|
+
evidence: args.evidence,
|
|
6915
6908
|
selector,
|
|
6916
6909
|
alignment: asString(args.alignment) ?? '',
|
|
6917
6910
|
changeMode: parseChangeMode(args.changeMode),
|
|
@@ -7043,6 +7036,24 @@ async function superdocPerformAction(doc, args) {
|
|
|
7043
7036
|
}
|
|
7044
7037
|
}
|
|
7045
7038
|
}
|
|
7039
|
+
async function superdocPerformAction(doc, args) {
|
|
7040
|
+
try {
|
|
7041
|
+
return await dispatchSuperdocPerformAction(doc, args);
|
|
7042
|
+
}
|
|
7043
|
+
catch (error) {
|
|
7044
|
+
if (!(error instanceof docSnapshot.MutationSnapshotError))
|
|
7045
|
+
throw error;
|
|
7046
|
+
const action = isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
|
|
7047
|
+
return {
|
|
7048
|
+
status: 'failed',
|
|
7049
|
+
intent: action,
|
|
7050
|
+
selectedTargets: [],
|
|
7051
|
+
executedOperations: [],
|
|
7052
|
+
verification: [],
|
|
7053
|
+
errors: [{ code: error.code, message: error.message, recovery: { kind: 'reinspect' } }],
|
|
7054
|
+
};
|
|
7055
|
+
}
|
|
7056
|
+
}
|
|
7046
7057
|
function parseChangeMode(value) {
|
|
7047
7058
|
if (value === 'direct' || value === 'tracked')
|
|
7048
7059
|
return value;
|