@superdoc/sdk 2.10.0-next.2 → 2.10.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/actions.cjs +57 -104
- package/dist/agent/actions.js +57 -104
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.d.ts +38 -0
- package/dist/generated/contract.cjs +190 -0
- package/dist/generated/contract.js +190 -0
- package/dist/runtime/sdk-version.generated.cjs +1 -1
- package/dist/runtime/sdk-version.generated.d.ts +1 -1
- package/dist/runtime/sdk-version.generated.js +1 -1
- package/package.json +6 -6
- package/tools/catalog.json +54 -0
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +54 -0
- package/tools/tools.generic.json +54 -0
- package/tools/tools.openai.json +54 -0
- package/tools/tools.vercel.json +54 -0
package/dist/agent/actions.cjs
CHANGED
|
@@ -775,13 +775,28 @@ async function executeCreateHeading(doc, text, level, placement, changeMode) {
|
|
|
775
775
|
// Dual dialect — see executeCreateParagraph.
|
|
776
776
|
return fn(params, changeMode ? { changeMode } : undefined);
|
|
777
777
|
}
|
|
778
|
-
async function executeMutations(doc, steps, changeMode) {
|
|
778
|
+
async function executeMutations(doc, steps, changeMode, expectedRevision) {
|
|
779
779
|
const fn = maybeMethod(doc, ['mutations', 'apply']);
|
|
780
780
|
if (!fn)
|
|
781
781
|
throw new errors.SuperDocCliError('doc.mutations.apply is not available on the document handle.', {
|
|
782
782
|
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
783
783
|
});
|
|
784
|
-
return fn({
|
|
784
|
+
return fn({
|
|
785
|
+
atomic: true,
|
|
786
|
+
changeMode: changeMode ?? 'direct',
|
|
787
|
+
steps,
|
|
788
|
+
...(expectedRevision ? { expectedRevision } : {}),
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
async function previewMutations(doc, steps, changeMode, expectedRevision) {
|
|
792
|
+
const fn = maybeMethod(doc, ['mutations', 'preview']);
|
|
793
|
+
if (!fn) {
|
|
794
|
+
throw new errors.SuperDocCliError('doc.mutations.preview is not available on the document handle.', {
|
|
795
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
const result = await fn({ atomic: true, changeMode: changeMode ?? 'direct', expectedRevision, steps });
|
|
799
|
+
return isRecord(result) ? result : {};
|
|
785
800
|
}
|
|
786
801
|
async function executeCreateTable(doc, args, placement) {
|
|
787
802
|
const fn = maybeMethod(doc, ['create', 'table']);
|
|
@@ -1136,7 +1151,7 @@ async function runInsertHeading(doc, args) {
|
|
|
1136
1151
|
}
|
|
1137
1152
|
async function runReplaceText(doc, args) {
|
|
1138
1153
|
const selectorDomains = args.selector ? snapshotDomainsForSelector(args.selector) : null;
|
|
1139
|
-
const requiresBlockSnapshot = args.selector != null
|
|
1154
|
+
const requiresBlockSnapshot = args.selector != null;
|
|
1140
1155
|
const preIdentity = requiresBlockSnapshot ? null : await readDocumentIdentity(doc);
|
|
1141
1156
|
const pre = requiresBlockSnapshot
|
|
1142
1157
|
? await docSnapshot.buildDocumentSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
|
|
@@ -1149,6 +1164,7 @@ async function runReplaceText(doc, args) {
|
|
|
1149
1164
|
const selectedTargets = [];
|
|
1150
1165
|
let skippedEdits = [];
|
|
1151
1166
|
let steps;
|
|
1167
|
+
let applyExpectedRevision;
|
|
1152
1168
|
// Blocks whose text this call rewrites, with their pre-mutation run layout:
|
|
1153
1169
|
// the engine replace lane renders the replaced range as uniform runs, so a
|
|
1154
1170
|
// whole-paragraph find/replace flattens intra-paragraph formatting (bold
|
|
@@ -1200,98 +1216,9 @@ async function runReplaceText(doc, args) {
|
|
|
1200
1216
|
});
|
|
1201
1217
|
}
|
|
1202
1218
|
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 }) => ({
|
|
1219
|
+
let candidates = args.edits.map((edit, editIndex) => ({
|
|
1220
|
+
edit,
|
|
1221
|
+
step: {
|
|
1295
1222
|
id: `replace-${editIndex + 1}`,
|
|
1296
1223
|
op: 'text.rewrite',
|
|
1297
1224
|
where: {
|
|
@@ -1308,10 +1235,42 @@ async function runReplaceText(doc, args) {
|
|
|
1308
1235
|
replacement: { text: edit.replace },
|
|
1309
1236
|
style: preserveRewriteStyle(),
|
|
1310
1237
|
},
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1238
|
+
},
|
|
1239
|
+
}));
|
|
1240
|
+
if (candidates.length > 1) {
|
|
1241
|
+
applyExpectedRevision = pre.revision;
|
|
1242
|
+
while (candidates.length > 0) {
|
|
1243
|
+
const preview = await previewMutations(doc, candidates.map((candidate) => candidate.step), args.changeMode, pre.revision);
|
|
1244
|
+
if (preview.valid === true)
|
|
1245
|
+
break;
|
|
1246
|
+
const failures = Array.isArray(preview.failures) ? preview.failures.filter(isRecord) : [];
|
|
1247
|
+
const absent = failures.find((failure) => failure.code === 'MATCH_NOT_FOUND' && typeof failure.stepId === 'string');
|
|
1248
|
+
if (!absent) {
|
|
1249
|
+
throw new Error(typeof failures[0]?.message === 'string' ? failures[0].message : 'replacement plan could not be prepared');
|
|
1250
|
+
}
|
|
1251
|
+
const absentIndex = candidates.findIndex((candidate) => candidate.step.id === absent.stepId);
|
|
1252
|
+
if (absentIndex < 0)
|
|
1253
|
+
throw new Error('replacement preview returned an unknown step id');
|
|
1254
|
+
skippedEdits.push({ find: candidates[absentIndex].edit.find });
|
|
1255
|
+
candidates = candidates.filter((_candidate, index) => index !== absentIndex);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (candidates.length === 0) {
|
|
1259
|
+
return {
|
|
1260
|
+
status: 'failed',
|
|
1261
|
+
intent: 'replace_text',
|
|
1262
|
+
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1263
|
+
selectedTargets: [],
|
|
1264
|
+
executedOperations: [],
|
|
1265
|
+
verification: [],
|
|
1266
|
+
errors: [
|
|
1267
|
+
{ code: 'ACTION_FAILED', message: 'none of the requested text replacements matched the current document' },
|
|
1268
|
+
],
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
steps = candidates.map((candidate) => candidate.step);
|
|
1313
1272
|
}
|
|
1314
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
1273
|
+
const result = await executeMutations(doc, steps, args.changeMode, applyExpectedRevision);
|
|
1315
1274
|
const preserved = [];
|
|
1316
1275
|
for (const entry of preserveTargets) {
|
|
1317
1276
|
const restored = await preserveRunPatternAfterRewrite(doc, entry.nodeId, entry.oldText, entry.runs, entry.newText);
|
|
@@ -4839,12 +4798,6 @@ function paragraphMarkRunPropsOf(payload) {
|
|
|
4839
4798
|
* Spaced dashes are listed separately from bare ones because the label run
|
|
4840
4799
|
* usually stops BEFORE the space ("JavaScript / TypeScript" + " — SuperDoc…").
|
|
4841
4800
|
*/
|
|
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
4801
|
const LEAD_IN_DELIMITERS = [':', ' — ', ' – ', ' - ', '—', '–'];
|
|
4849
4802
|
/** A lead-in is a LABEL, not a sentence — longer spans are prose, not a pattern. */
|
|
4850
4803
|
const LEAD_IN_MAX_CHARS = 90;
|
package/dist/agent/actions.js
CHANGED
|
@@ -772,13 +772,28 @@ async function executeCreateHeading(doc, text, level, placement, changeMode) {
|
|
|
772
772
|
// Dual dialect — see executeCreateParagraph.
|
|
773
773
|
return fn(params, changeMode ? { changeMode } : undefined);
|
|
774
774
|
}
|
|
775
|
-
async function executeMutations(doc, steps, changeMode) {
|
|
775
|
+
async function executeMutations(doc, steps, changeMode, expectedRevision) {
|
|
776
776
|
const fn = maybeMethod(doc, ['mutations', 'apply']);
|
|
777
777
|
if (!fn)
|
|
778
778
|
throw new SuperDocCliError('doc.mutations.apply is not available on the document handle.', {
|
|
779
779
|
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
780
780
|
});
|
|
781
|
-
return fn({
|
|
781
|
+
return fn({
|
|
782
|
+
atomic: true,
|
|
783
|
+
changeMode: changeMode ?? 'direct',
|
|
784
|
+
steps,
|
|
785
|
+
...(expectedRevision ? { expectedRevision } : {}),
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
async function previewMutations(doc, steps, changeMode, expectedRevision) {
|
|
789
|
+
const fn = maybeMethod(doc, ['mutations', 'preview']);
|
|
790
|
+
if (!fn) {
|
|
791
|
+
throw new SuperDocCliError('doc.mutations.preview is not available on the document handle.', {
|
|
792
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
const result = await fn({ atomic: true, changeMode: changeMode ?? 'direct', expectedRevision, steps });
|
|
796
|
+
return isRecord(result) ? result : {};
|
|
782
797
|
}
|
|
783
798
|
async function executeCreateTable(doc, args, placement) {
|
|
784
799
|
const fn = maybeMethod(doc, ['create', 'table']);
|
|
@@ -1133,7 +1148,7 @@ async function runInsertHeading(doc, args) {
|
|
|
1133
1148
|
}
|
|
1134
1149
|
async function runReplaceText(doc, args) {
|
|
1135
1150
|
const selectorDomains = args.selector ? snapshotDomainsForSelector(args.selector) : null;
|
|
1136
|
-
const requiresBlockSnapshot = args.selector != null
|
|
1151
|
+
const requiresBlockSnapshot = args.selector != null;
|
|
1137
1152
|
const preIdentity = requiresBlockSnapshot ? null : await readDocumentIdentity(doc);
|
|
1138
1153
|
const pre = requiresBlockSnapshot
|
|
1139
1154
|
? await buildDocumentSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
|
|
@@ -1146,6 +1161,7 @@ async function runReplaceText(doc, args) {
|
|
|
1146
1161
|
const selectedTargets = [];
|
|
1147
1162
|
let skippedEdits = [];
|
|
1148
1163
|
let steps;
|
|
1164
|
+
let applyExpectedRevision;
|
|
1149
1165
|
// Blocks whose text this call rewrites, with their pre-mutation run layout:
|
|
1150
1166
|
// the engine replace lane renders the replaced range as uniform runs, so a
|
|
1151
1167
|
// whole-paragraph find/replace flattens intra-paragraph formatting (bold
|
|
@@ -1197,98 +1213,9 @@ async function runReplaceText(doc, args) {
|
|
|
1197
1213
|
});
|
|
1198
1214
|
}
|
|
1199
1215
|
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 }) => ({
|
|
1216
|
+
let candidates = args.edits.map((edit, editIndex) => ({
|
|
1217
|
+
edit,
|
|
1218
|
+
step: {
|
|
1292
1219
|
id: `replace-${editIndex + 1}`,
|
|
1293
1220
|
op: 'text.rewrite',
|
|
1294
1221
|
where: {
|
|
@@ -1305,10 +1232,42 @@ async function runReplaceText(doc, args) {
|
|
|
1305
1232
|
replacement: { text: edit.replace },
|
|
1306
1233
|
style: preserveRewriteStyle(),
|
|
1307
1234
|
},
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1235
|
+
},
|
|
1236
|
+
}));
|
|
1237
|
+
if (candidates.length > 1) {
|
|
1238
|
+
applyExpectedRevision = pre.revision;
|
|
1239
|
+
while (candidates.length > 0) {
|
|
1240
|
+
const preview = await previewMutations(doc, candidates.map((candidate) => candidate.step), args.changeMode, pre.revision);
|
|
1241
|
+
if (preview.valid === true)
|
|
1242
|
+
break;
|
|
1243
|
+
const failures = Array.isArray(preview.failures) ? preview.failures.filter(isRecord) : [];
|
|
1244
|
+
const absent = failures.find((failure) => failure.code === 'MATCH_NOT_FOUND' && typeof failure.stepId === 'string');
|
|
1245
|
+
if (!absent) {
|
|
1246
|
+
throw new Error(typeof failures[0]?.message === 'string' ? failures[0].message : 'replacement plan could not be prepared');
|
|
1247
|
+
}
|
|
1248
|
+
const absentIndex = candidates.findIndex((candidate) => candidate.step.id === absent.stepId);
|
|
1249
|
+
if (absentIndex < 0)
|
|
1250
|
+
throw new Error('replacement preview returned an unknown step id');
|
|
1251
|
+
skippedEdits.push({ find: candidates[absentIndex].edit.find });
|
|
1252
|
+
candidates = candidates.filter((_candidate, index) => index !== absentIndex);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (candidates.length === 0) {
|
|
1256
|
+
return {
|
|
1257
|
+
status: 'failed',
|
|
1258
|
+
intent: 'replace_text',
|
|
1259
|
+
preSnapshot: { revision: pre.revision, counts: pre.counts },
|
|
1260
|
+
selectedTargets: [],
|
|
1261
|
+
executedOperations: [],
|
|
1262
|
+
verification: [],
|
|
1263
|
+
errors: [
|
|
1264
|
+
{ code: 'ACTION_FAILED', message: 'none of the requested text replacements matched the current document' },
|
|
1265
|
+
],
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
steps = candidates.map((candidate) => candidate.step);
|
|
1310
1269
|
}
|
|
1311
|
-
const result = await executeMutations(doc, steps, args.changeMode);
|
|
1270
|
+
const result = await executeMutations(doc, steps, args.changeMode, applyExpectedRevision);
|
|
1312
1271
|
const preserved = [];
|
|
1313
1272
|
for (const entry of preserveTargets) {
|
|
1314
1273
|
const restored = await preserveRunPatternAfterRewrite(doc, entry.nodeId, entry.oldText, entry.runs, entry.newText);
|
|
@@ -4836,12 +4795,6 @@ function paragraphMarkRunPropsOf(payload) {
|
|
|
4836
4795
|
* Spaced dashes are listed separately from bare ones because the label run
|
|
4837
4796
|
* usually stops BEFORE the space ("JavaScript / TypeScript" + " — SuperDoc…").
|
|
4838
4797
|
*/
|
|
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
4798
|
const LEAD_IN_DELIMITERS = [':', ' — ', ' – ', ' - ', '—', '–'];
|
|
4846
4799
|
/** A lead-in is a LABEL, not a sentence — longer spans are prose, not a pattern. */
|
|
4847
4800
|
const LEAD_IN_MAX_CHARS = 90;
|