@ansonlai/docx-redline-js 0.5.4 → 0.6.1
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/AGENTS.md +82 -697
- package/ARCHITECTURE.md +13 -1
- package/CHANGELOG.md +8 -0
- package/README.md +177 -45
- package/core/paragraph-targeting.js +14 -2
- package/dist/docx-redline-js.esm.js +184 -51
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +77 -77
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/AGENT_FAST_START.md +59 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +878 -0
- package/docs/SKILL_AUTHORING.md +126 -0
- package/docs/TESTING.md +35 -1
- package/docs/schemas/document-operations.schema.json +5 -1
- package/docs/validation-reports/2026-09-12-agent-cli-discovery-baseline.md +56 -0
- package/docs/validation-reports/2026-09-12-agent-protocol-rollout.md +86 -0
- package/docs/validation-reports/2026-09-13-agent-cli-efficiency-rollout.md +86 -0
- package/engine/oxml-engine.js +80 -13
- package/engine/run-builders.js +5 -15
- package/index.d.ts +28 -3
- package/node/cli-help.js +209 -0
- package/node/cli.js +323 -65
- package/node/docx-document.js +120 -69
- package/node/index.d.ts +6 -2
- package/package.json +15 -3
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +25 -25
- package/services/batch-operation-orchestrator.js +215 -120
- package/services/document-inspection.js +89 -11
- package/services/document-operation-applier.js +52 -34
- package/services/document-operation-contract.js +10 -6
- package/services/document-operation-mutations.js +51 -5
- package/services/document-operation-session.js +4 -0
- package/services/error-recovery.js +174 -0
- package/services/operation-batch-compiler.js +394 -0
- package/services/operation-preflight.js +91 -72
- package/services/standalone-operation-runner.d.ts +17 -1
- package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +0 -1669
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +0 -1399
- package/docs/plans/completed/2026-03-01-release-0.1.4-design.md +0 -33
- package/docs/plans/completed/2026-03-01-release-0.1.4.md +0 -110
- package/docs/plans/completed/2026-05-31-architectural changes.md +0 -593
- package/docs/plans/completed/2026-08-02-reliability-improvements.md +0 -1155
- package/docs/plans/completed/2026-08-30-reliability-testing-improvements.md +0 -488
- package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +0 -669
- package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +0 -427
- package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +0 -519
- package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +0 -69
- package/docs/plans/completed/structural-revision-capability-matrix.md +0 -115
- package/docs/test-comparison-dashboard.html +0 -4338
- package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +0 -22
- package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +0 -24
- package/docs/validation-reports/2026-08-30-phase-3-coverage.md +0 -73
- package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +0 -82
- package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +0 -114
- package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +0 -79
|
@@ -19,10 +19,12 @@ import {
|
|
|
19
19
|
computeDocumentPartsRevisionToken,
|
|
20
20
|
areRevisionTokensEqual
|
|
21
21
|
} from './revision-token.js';
|
|
22
|
-
import {
|
|
23
|
-
createEmptyReceipt,
|
|
24
|
-
reconcileReceiptsAgainstOutput
|
|
25
|
-
} from './receipt-collector.js';
|
|
22
|
+
import {
|
|
23
|
+
createEmptyReceipt,
|
|
24
|
+
reconcileReceiptsAgainstOutput
|
|
25
|
+
} from './receipt-collector.js';
|
|
26
|
+
import { compileOperationBatch } from './operation-batch-compiler.js';
|
|
27
|
+
import { createRetryPlan } from './error-recovery.js';
|
|
26
28
|
|
|
27
29
|
const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
28
30
|
|
|
@@ -70,9 +72,10 @@ export function buildOperationDependencyPlan(operations = []) {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
// 2. Build dependency edges
|
|
73
|
-
const inDegrees = new Array(list.length).fill(0);
|
|
74
|
-
const dependents = Array.from({ length: list.length }, () => new Set());
|
|
75
|
-
const dependencies = Array.from({ length: list.length }, () => new Set());
|
|
75
|
+
const inDegrees = new Array(list.length).fill(0);
|
|
76
|
+
const dependents = Array.from({ length: list.length }, () => new Set());
|
|
77
|
+
const dependencies = Array.from({ length: list.length }, () => new Set());
|
|
78
|
+
const mutatingCaptureConsumers = new Map();
|
|
76
79
|
|
|
77
80
|
for (let i = 0; i < list.length; i++) {
|
|
78
81
|
const op = list[i];
|
|
@@ -107,13 +110,40 @@ export function buildOperationDependencyPlan(operations = []) {
|
|
|
107
110
|
}
|
|
108
111
|
};
|
|
109
112
|
}
|
|
110
|
-
if (!dependencies[i].has(producerIndex)) {
|
|
111
|
-
dependencies[i].add(producerIndex);
|
|
112
|
-
dependents[producerIndex].add(i);
|
|
113
|
-
inDegrees[i]++;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
if (!dependencies[i].has(producerIndex)) {
|
|
114
|
+
dependencies[i].add(producerIndex);
|
|
115
|
+
dependents[producerIndex].add(i);
|
|
116
|
+
inDegrees[i]++;
|
|
117
|
+
}
|
|
118
|
+
const kind = normalizeDocumentOperation(op).operationKind;
|
|
119
|
+
if (kind !== 'comment' && kind !== 'comment_reply') {
|
|
120
|
+
if (!mutatingCaptureConsumers.has(ref)) mutatingCaptureConsumers.set(ref, []);
|
|
121
|
+
mutatingCaptureConsumers.get(ref).push({
|
|
122
|
+
index: i,
|
|
123
|
+
select: op?.target?.select ?? op?.targetDescriptor?.select ?? null
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const [captureRef, consumers] of mutatingCaptureConsumers) {
|
|
130
|
+
if (consumers.length < 2) continue;
|
|
131
|
+
const selectors = consumers.map(consumer => consumer.select);
|
|
132
|
+
const distinctSelectors = new Set(selectors);
|
|
133
|
+
if (selectors.every(selector => typeof selector === 'string' && selector.length > 0)
|
|
134
|
+
&& distinctSelectors.size === selectors.length) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
valid: false,
|
|
139
|
+
error: {
|
|
140
|
+
code: 'CAPTURE_FANOUT_CONFLICT',
|
|
141
|
+
message: `Capture "${captureRef}" has overlapping or unscoped mutating consumers at operation indices ${consumers.map(consumer => consumer.index + 1).join(', ')}.`,
|
|
142
|
+
operationIndexes: consumers.map(consumer => consumer.index + 1),
|
|
143
|
+
captureRef
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
117
147
|
|
|
118
148
|
// 3. Stable topological sort with comment priority among ready nodes
|
|
119
149
|
const ready = [];
|
|
@@ -191,62 +221,56 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
191
221
|
i + 1,
|
|
192
222
|
op?.operationId,
|
|
193
223
|
resolveDocumentOperationAuthor(op, author, defaultAuthor),
|
|
194
|
-
'not_attempted'
|
|
224
|
+
'not_attempted'
|
|
195
225
|
));
|
|
196
|
-
|
|
197
|
-
|
|
226
|
+
const failedBeforeExecution = (error, extra = {}) => {
|
|
227
|
+
const receipts = emptyReceipts();
|
|
198
228
|
return {
|
|
199
229
|
documentXml,
|
|
200
230
|
hasChanges: false,
|
|
201
231
|
commentsXml: null,
|
|
202
232
|
numberingXmlParts: [],
|
|
203
233
|
results: [],
|
|
204
|
-
receipts
|
|
234
|
+
receipts,
|
|
205
235
|
executionOrder: [],
|
|
206
236
|
authorsUsed: [],
|
|
237
|
+
...(options.atomic === true ? { rolledBack: true } : {}),
|
|
207
238
|
status: 'error',
|
|
208
|
-
error:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
error: normalizeOperationError(error),
|
|
240
|
+
retryPlan: createRetryPlan({
|
|
241
|
+
atomic: options.atomic === true,
|
|
242
|
+
rolledBack: options.atomic === true,
|
|
243
|
+
results: [],
|
|
244
|
+
receipts,
|
|
245
|
+
operationCount: sourceOperations.length
|
|
246
|
+
}),
|
|
247
|
+
...extra
|
|
212
248
|
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (options.existingRevisions != null && !isExistingRevisionsPolicy(options.existingRevisions)) {
|
|
252
|
+
return failedBeforeExecution({
|
|
253
|
+
code: 'INVALID_OPERATION',
|
|
254
|
+
message: `Unsupported existingRevisions policy: "${String(options.existingRevisions)}".`,
|
|
255
|
+
field: 'existingRevisions'
|
|
256
|
+
});
|
|
213
257
|
}
|
|
214
258
|
|
|
215
259
|
if (options?.expectedRevision) {
|
|
216
260
|
const tokenValidation = validateRevisionToken(options.expectedRevision);
|
|
217
261
|
if (!tokenValidation.valid) {
|
|
218
|
-
return {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
numberingXmlParts: [],
|
|
223
|
-
results: [],
|
|
224
|
-
receipts: emptyReceipts(),
|
|
225
|
-
executionOrder: [],
|
|
226
|
-
authorsUsed: [],
|
|
227
|
-
status: 'error',
|
|
228
|
-
error: {
|
|
229
|
-
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
230
|
-
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
231
|
-
}
|
|
232
|
-
};
|
|
262
|
+
return failedBeforeExecution({
|
|
263
|
+
code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
|
|
264
|
+
message: tokenValidation.error?.message || 'Invalid revision token.'
|
|
265
|
+
});
|
|
233
266
|
}
|
|
234
267
|
if (options.expectedRevision.scope !== 'document-parts') {
|
|
235
|
-
return {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
receipts: emptyReceipts(),
|
|
242
|
-
executionOrder: [],
|
|
243
|
-
authorsUsed: [],
|
|
244
|
-
status: 'error',
|
|
245
|
-
error: {
|
|
246
|
-
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
247
|
-
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
|
|
248
|
-
}
|
|
249
|
-
};
|
|
268
|
+
return failedBeforeExecution({
|
|
269
|
+
code: 'REVISION_TOKEN_SCOPE_MISMATCH',
|
|
270
|
+
message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`,
|
|
271
|
+
expectedScope: 'document-parts',
|
|
272
|
+
actualScope: options.expectedRevision.scope
|
|
273
|
+
});
|
|
250
274
|
}
|
|
251
275
|
const currentToken = await computeDocumentPartsRevisionToken({
|
|
252
276
|
documentXml,
|
|
@@ -256,21 +280,12 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
256
280
|
stylesXml: runtimeContext?.stylesXml || options.stylesXml
|
|
257
281
|
}, options);
|
|
258
282
|
if (!areRevisionTokensEqual(currentToken.value, options.expectedRevision.value)) {
|
|
259
|
-
return {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
receipts: emptyReceipts(),
|
|
266
|
-
executionOrder: [],
|
|
267
|
-
authorsUsed: [],
|
|
268
|
-
status: 'error',
|
|
269
|
-
error: {
|
|
270
|
-
code: 'REVISION_MISMATCH',
|
|
271
|
-
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`
|
|
272
|
-
}
|
|
273
|
-
};
|
|
283
|
+
return failedBeforeExecution({
|
|
284
|
+
code: 'REVISION_MISMATCH',
|
|
285
|
+
message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`,
|
|
286
|
+
expectedRevision: options.expectedRevision,
|
|
287
|
+
currentRevision: currentToken
|
|
288
|
+
});
|
|
274
289
|
}
|
|
275
290
|
}
|
|
276
291
|
|
|
@@ -281,34 +296,73 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
281
296
|
_deferDocumentSerialization: true
|
|
282
297
|
});
|
|
283
298
|
if (!session.valid) {
|
|
284
|
-
return {
|
|
285
|
-
documentXml,
|
|
286
|
-
hasChanges: false,
|
|
287
|
-
commentsXml: null,
|
|
288
|
-
numberingXmlParts: [],
|
|
289
|
-
results: [],
|
|
290
|
-
receipts: emptyReceipts(),
|
|
291
|
-
executionOrder: [],
|
|
292
|
-
authorsUsed: [],
|
|
293
|
-
status: 'error',
|
|
294
|
-
error: session.parseResult.error,
|
|
295
|
-
warnings: session.parseResult.warnings
|
|
296
|
-
};
|
|
299
|
+
return failedBeforeExecution(session.parseResult.error, { warnings: session.parseResult.warnings });
|
|
297
300
|
}
|
|
298
|
-
const
|
|
301
|
+
const compilation = compileOperationBatch(session.document, sourceOperations, {
|
|
302
|
+
strictTargets: options.strictTargets !== false,
|
|
303
|
+
onInfo: options.onInfo,
|
|
304
|
+
onWarn: options.onWarn
|
|
305
|
+
});
|
|
306
|
+
session.sourceTargetRegistry = compilation.registry;
|
|
307
|
+
if (compilation.conflicts.length > 0) {
|
|
308
|
+
const conflictByIndex = new Map();
|
|
309
|
+
for (const conflict of compilation.conflicts) {
|
|
310
|
+
for (const index of conflict.operationIndexes) {
|
|
311
|
+
if (!conflictByIndex.has(index)) conflictByIndex.set(index, conflict);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const conflictResults = Array.from(conflictByIndex, ([index, conflict]) => {
|
|
315
|
+
const operation = sourceOperations[index - 1];
|
|
316
|
+
const authorUsed = resolveDocumentOperationAuthor(operation, author, defaultAuthor);
|
|
317
|
+
const normalizedConflict = normalizeOperationError(conflict, {
|
|
318
|
+
operationIndex: index,
|
|
319
|
+
...(operation?.operationId ? { operationId: operation.operationId } : {})
|
|
320
|
+
});
|
|
321
|
+
return {
|
|
322
|
+
index,
|
|
323
|
+
type: operation?.type || 'redline',
|
|
324
|
+
operationType: normalizeDocumentOperation(operation).operationKind,
|
|
325
|
+
status: 'error',
|
|
326
|
+
authorUsed,
|
|
327
|
+
error: normalizedConflict,
|
|
328
|
+
receipt: createEmptyReceipt(index, operation?.operationId, authorUsed, 'refused')
|
|
329
|
+
};
|
|
330
|
+
}).sort((left, right) => left.index - right.index);
|
|
331
|
+
const receipts = sourceOperations.map((operation, index) => {
|
|
332
|
+
const conflictResult = conflictResults.find(result => result.index === index + 1);
|
|
333
|
+
return conflictResult?.receipt || createEmptyReceipt(
|
|
334
|
+
index + 1,
|
|
335
|
+
operation?.operationId,
|
|
336
|
+
resolveDocumentOperationAuthor(operation, author, defaultAuthor),
|
|
337
|
+
'not_attempted'
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
documentXml,
|
|
342
|
+
hasChanges: false,
|
|
343
|
+
commentsXml: null,
|
|
344
|
+
numberingXmlParts: [],
|
|
345
|
+
results: conflictResults,
|
|
346
|
+
receipts,
|
|
347
|
+
executionOrder: [],
|
|
348
|
+
authorsUsed: [],
|
|
349
|
+
rolledBack: options.atomic === true,
|
|
350
|
+
status: 'error',
|
|
351
|
+
error: normalizeOperationError(compilation.conflicts[0]),
|
|
352
|
+
conflicts: compilation.conflicts.map(conflict => normalizeOperationError(conflict)),
|
|
353
|
+
retryPlan: createRetryPlan({
|
|
354
|
+
atomic: options.atomic === true,
|
|
355
|
+
rolledBack: options.atomic === true,
|
|
356
|
+
results: conflictResults,
|
|
357
|
+
receipts,
|
|
358
|
+
operationCount: sourceOperations.length
|
|
359
|
+
})
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const executableOperations = compilation.compiledOperations;
|
|
363
|
+
const dependencyPlan = buildOperationDependencyPlan(executableOperations);
|
|
299
364
|
if (!dependencyPlan.valid) {
|
|
300
|
-
return
|
|
301
|
-
documentXml,
|
|
302
|
-
hasChanges: false,
|
|
303
|
-
commentsXml: null,
|
|
304
|
-
numberingXmlParts: [],
|
|
305
|
-
results: [],
|
|
306
|
-
receipts: emptyReceipts(),
|
|
307
|
-
executionOrder: [],
|
|
308
|
-
authorsUsed: [],
|
|
309
|
-
status: 'error',
|
|
310
|
-
error: dependencyPlan.error
|
|
311
|
-
};
|
|
365
|
+
return failedBeforeExecution(dependencyPlan.error);
|
|
312
366
|
}
|
|
313
367
|
const scheduled = dependencyPlan.scheduled;
|
|
314
368
|
|
|
@@ -332,9 +386,37 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
332
386
|
const authorsUsed = session.authorsUsed;
|
|
333
387
|
let operationFailed = false;
|
|
334
388
|
|
|
335
|
-
for (const { operation, index } of scheduled) {
|
|
336
|
-
executionOrder.push(index + 1);
|
|
337
|
-
|
|
389
|
+
for (const { operation, index } of scheduled) {
|
|
390
|
+
executionOrder.push(index + 1);
|
|
391
|
+
const sourceBinding = compilation.bindings[index];
|
|
392
|
+
if (sourceBinding?.error) {
|
|
393
|
+
operationFailed = true;
|
|
394
|
+
const authorUsed = resolveDocumentOperationAuthor(operation, author, defaultAuthor);
|
|
395
|
+
const errorReceipt = createEmptyReceipt(
|
|
396
|
+
index + 1,
|
|
397
|
+
operation?.operationId,
|
|
398
|
+
authorUsed,
|
|
399
|
+
'refused'
|
|
400
|
+
);
|
|
401
|
+
const normalizedBindingError = normalizeOperationError(sourceBinding.error, {
|
|
402
|
+
operationIndex: index + 1,
|
|
403
|
+
...(operation?.operationId ? { operationId: operation.operationId } : {})
|
|
404
|
+
});
|
|
405
|
+
errorReceipt.warnings.push(normalizedBindingError.message);
|
|
406
|
+
results.push({
|
|
407
|
+
index: index + 1,
|
|
408
|
+
type: operation?.type || 'redline',
|
|
409
|
+
status: 'error',
|
|
410
|
+
operationType: normalizeDocumentOperation(operation).operationKind,
|
|
411
|
+
authorUsed,
|
|
412
|
+
warnings: [normalizedBindingError.message],
|
|
413
|
+
error: normalizedBindingError,
|
|
414
|
+
receipt: errorReceipt
|
|
415
|
+
});
|
|
416
|
+
if (!continueOnError) break;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
338
420
|
const result = await applyOperationToDocumentXml(
|
|
339
421
|
session.currentDocumentXml,
|
|
340
422
|
operation,
|
|
@@ -374,8 +456,11 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
374
456
|
...(result.receipt ? { receipt: result.receipt } : {})
|
|
375
457
|
});
|
|
376
458
|
if (isError && !continueOnError) break;
|
|
377
|
-
} catch (error) {
|
|
378
|
-
const normalizedError = normalizeOperationError(error
|
|
459
|
+
} catch (error) {
|
|
460
|
+
const normalizedError = normalizeOperationError(error, {
|
|
461
|
+
operationIndex: index + 1,
|
|
462
|
+
...(operation?.operationId ? { operationId: operation.operationId } : {})
|
|
463
|
+
});
|
|
379
464
|
operationFailed = true;
|
|
380
465
|
const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
|
|
381
466
|
const errorReceipt = createEmptyReceipt(
|
|
@@ -476,8 +561,8 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
476
561
|
|
|
477
562
|
if (!rolledBack) commitBatchRuntimeContext(runtimeContext, context);
|
|
478
563
|
|
|
479
|
-
|
|
480
|
-
documentXml: rolledBack ? session.rollback() : outputDocumentXml,
|
|
564
|
+
const batchResult = {
|
|
565
|
+
documentXml: rolledBack ? session.rollback() : outputDocumentXml,
|
|
481
566
|
hasChanges: rolledBack ? false : hasChanges,
|
|
482
567
|
commentsXml: rolledBack ? null : session.commentsXml,
|
|
483
568
|
commentsExtendedXml: rolledBack ? null : session.commentsExtendedXml,
|
|
@@ -491,23 +576,33 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
|
|
|
491
576
|
...(rolledBack ? {
|
|
492
577
|
rolledBack: true,
|
|
493
578
|
status: 'error',
|
|
494
|
-
error: {
|
|
495
|
-
code: reconciliationError ? reconciliationError.code : (serializationError ? 'DOCUMENT_SERIALIZATION_FAILED' : 'BATCH_OPERATION_FAILED'),
|
|
496
|
-
message: reconciliationError?.message
|
|
497
|
-
|| serializationError?.message
|
|
498
|
-
|| 'Atomic batch rolled back because one or more operations failed.'
|
|
499
|
-
}
|
|
500
|
-
} : (reconciliationError ? {
|
|
501
|
-
status: 'error',
|
|
502
|
-
error: reconciliationError
|
|
503
|
-
} : (operationFailed ? {
|
|
504
|
-
status: hasChanges ? 'partial' : 'error',
|
|
505
|
-
error: {
|
|
506
|
-
code: 'BATCH_OPERATION_FAILED',
|
|
507
|
-
message: 'One or more operations failed.'
|
|
508
|
-
}
|
|
509
|
-
} : {
|
|
510
|
-
status: 'ok'
|
|
511
|
-
})))
|
|
512
|
-
};
|
|
513
|
-
|
|
579
|
+
error: normalizeOperationError({
|
|
580
|
+
code: reconciliationError ? reconciliationError.code : (serializationError ? 'DOCUMENT_SERIALIZATION_FAILED' : 'BATCH_OPERATION_FAILED'),
|
|
581
|
+
message: reconciliationError?.message
|
|
582
|
+
|| serializationError?.message
|
|
583
|
+
|| 'Atomic batch rolled back because one or more operations failed.'
|
|
584
|
+
})
|
|
585
|
+
} : (reconciliationError ? {
|
|
586
|
+
status: 'error',
|
|
587
|
+
error: normalizeOperationError(reconciliationError)
|
|
588
|
+
} : (operationFailed ? {
|
|
589
|
+
status: hasChanges ? 'partial' : 'error',
|
|
590
|
+
error: normalizeOperationError({
|
|
591
|
+
code: 'BATCH_OPERATION_FAILED',
|
|
592
|
+
message: 'One or more operations failed.'
|
|
593
|
+
})
|
|
594
|
+
} : {
|
|
595
|
+
status: 'ok'
|
|
596
|
+
})))
|
|
597
|
+
};
|
|
598
|
+
if (batchResult.status === 'error' || batchResult.status === 'partial') {
|
|
599
|
+
batchResult.retryPlan = createRetryPlan({
|
|
600
|
+
atomic,
|
|
601
|
+
rolledBack,
|
|
602
|
+
results: batchResult.results,
|
|
603
|
+
receipts: batchResult.receipts,
|
|
604
|
+
operationCount: sourceOperations.length
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return batchResult;
|
|
608
|
+
}
|
|
@@ -183,6 +183,80 @@ function collectDocumentCommentAnchors(paragraphNodes, revisionView) {
|
|
|
183
183
|
return anchors;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
function selectInspectionParagraphs(allParagraphs, options) {
|
|
187
|
+
let matches = allParagraphs;
|
|
188
|
+
if (options.revisedOnly) matches = matches.filter(item => item.hasRevisions);
|
|
189
|
+
if (options.inTable != null) matches = matches.filter(item => item.inTable === !!options.inTable);
|
|
190
|
+
if (options.skipEmpty) matches = matches.filter(item => item.text.length > 0);
|
|
191
|
+
if (options.search) {
|
|
192
|
+
const needle = String(options.search).toLowerCase();
|
|
193
|
+
matches = matches.filter(item => item.text.toLowerCase().includes(needle));
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(options.indexes)) {
|
|
196
|
+
const indexes = new Set(options.indexes);
|
|
197
|
+
matches = matches.filter(item => indexes.has(item.index));
|
|
198
|
+
}
|
|
199
|
+
let rangeStart = 1;
|
|
200
|
+
let rangeEnd = allParagraphs.length;
|
|
201
|
+
if (options.range) {
|
|
202
|
+
rangeStart = Number(options.range.start ?? options.range[0]);
|
|
203
|
+
rangeEnd = Number(options.range.end ?? options.range[1]);
|
|
204
|
+
matches = matches.filter(item => item.index >= rangeStart && item.index <= rangeEnd);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const totalMatches = matches.length;
|
|
208
|
+
const after = Number.isInteger(options.after) && options.after > 0 ? options.after : null;
|
|
209
|
+
const remaining = after == null ? matches : matches.filter(item => item.index > after);
|
|
210
|
+
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : null;
|
|
211
|
+
const selectedMatches = limit == null ? remaining : remaining.slice(0, limit);
|
|
212
|
+
const truncated = selectedMatches.length < remaining.length;
|
|
213
|
+
const around = Number.isInteger(options.around) && options.around > 0 ? options.around : 0;
|
|
214
|
+
const exposeSelection = !!options.search || around > 0 || limit != null || after != null;
|
|
215
|
+
|
|
216
|
+
let paragraphs = selectedMatches;
|
|
217
|
+
if (around > 0 && options.search) {
|
|
218
|
+
const directIndexes = new Set(selectedMatches.map(item => item.index));
|
|
219
|
+
const contextFor = new Map();
|
|
220
|
+
for (const match of selectedMatches) {
|
|
221
|
+
const start = Math.max(rangeStart, match.index - around);
|
|
222
|
+
const end = Math.min(rangeEnd, match.index + around);
|
|
223
|
+
for (let index = start; index <= end; index++) {
|
|
224
|
+
if (directIndexes.has(index)) continue;
|
|
225
|
+
const owners = contextFor.get(index) || [];
|
|
226
|
+
owners.push(match.index);
|
|
227
|
+
contextFor.set(index, owners);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const returnedIndexes = new Set([...directIndexes, ...contextFor.keys()]);
|
|
231
|
+
paragraphs = allParagraphs
|
|
232
|
+
.filter(item => returnedIndexes.has(item.index))
|
|
233
|
+
.map(item => directIndexes.has(item.index)
|
|
234
|
+
? { ...item, selectionRole: 'match' }
|
|
235
|
+
: { ...item, selectionRole: 'context', contextFor: contextFor.get(item.index) || [] });
|
|
236
|
+
} else if (exposeSelection) {
|
|
237
|
+
paragraphs = selectedMatches.map(item => ({ ...item, selectionRole: 'match' }));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
paragraphs,
|
|
242
|
+
...(exposeSelection ? {
|
|
243
|
+
selection: {
|
|
244
|
+
...(options.search ? { search: String(options.search), caseSensitive: false } : {}),
|
|
245
|
+
totalMatches,
|
|
246
|
+
returnedMatches: selectedMatches.length,
|
|
247
|
+
returnedParagraphs: paragraphs.length,
|
|
248
|
+
truncated,
|
|
249
|
+
nextAfter: truncated && selectedMatches.length > 0
|
|
250
|
+
? selectedMatches[selectedMatches.length - 1].index
|
|
251
|
+
: null,
|
|
252
|
+
...(limit != null ? { limit } : {}),
|
|
253
|
+
...(after != null ? { after } : {}),
|
|
254
|
+
...(around > 0 ? { around } : {})
|
|
255
|
+
}
|
|
256
|
+
} : {})
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
186
260
|
/** Read-only, stable document-parts inspection for agents and package adapters. */
|
|
187
261
|
export function inspectDocumentParts(parts, options = {}) {
|
|
188
262
|
const documentPart = parseXml(parts?.documentXml, 'word/document.xml', true);
|
|
@@ -199,9 +273,10 @@ export function inspectDocumentParts(parts, options = {}) {
|
|
|
199
273
|
const resolveNumbering = createNumberingResolver(numberingPart.doc);
|
|
200
274
|
let nearestHeading = null;
|
|
201
275
|
const paragraphNodes = getDocumentParagraphNodes(documentPart.doc);
|
|
202
|
-
const
|
|
276
|
+
const revisionView = options.revisionView === 'rejected' ? 'rejected' : 'accepted';
|
|
277
|
+
const commentAnchors = collectDocumentCommentAnchors(paragraphNodes, revisionView);
|
|
203
278
|
let paragraphs = paragraphNodes.map((paragraph, zeroIndex) => {
|
|
204
|
-
const text = extractCanonicalParagraphText(paragraph, { revisionView
|
|
279
|
+
const text = extractCanonicalParagraphText(paragraph, { revisionView });
|
|
205
280
|
const level = headingLevel(paragraph);
|
|
206
281
|
if (level) nearestHeading = { level, text };
|
|
207
282
|
const ids = [...new Set([...descendants(paragraph, 'commentRangeStart'), ...descendants(paragraph, 'commentReference')].map(node => attr(node, 'id')).filter(Boolean))];
|
|
@@ -212,11 +287,17 @@ export function inspectDocumentParts(parts, options = {}) {
|
|
|
212
287
|
const index = zeroIndex + 1;
|
|
213
288
|
const provision = list?.label && list.format !== 'bullet' ? list.label : null;
|
|
214
289
|
const headingText = nearestHeading?.text || null;
|
|
215
|
-
const
|
|
290
|
+
const excerpt = text.slice(0, options.excerptLength || 120);
|
|
291
|
+
const humanReference = level
|
|
292
|
+
? text
|
|
293
|
+
: (provision
|
|
294
|
+
? [provision, headingText].filter(Boolean).join(' — ')
|
|
295
|
+
: (headingText ? [headingText, excerpt].filter(Boolean).join(' — ') : excerpt));
|
|
216
296
|
const segments = extractParagraphRevisionSegments(paragraph);
|
|
217
297
|
return {
|
|
218
|
-
index, ref: `P${index}`, paragraphId: getParagraphId(paragraph),
|
|
219
|
-
|
|
298
|
+
index, ref: `P${index}`, paragraphId: getParagraphId(paragraph),
|
|
299
|
+
fingerprint: createParagraphFingerprint(paragraph, { text, index, revisionView }), revisionView,
|
|
300
|
+
text, exactText: text, excerpt, humanReference, provision, inTable: hasAncestor(paragraph, 'tc'), table: structure.table,
|
|
220
301
|
styleId, headingLevel: level, nearestHeading, list, structuralReferences: structure.references, hasRevisions: authors.length > 0, revisionAuthors: authors, commentIds: ids,
|
|
221
302
|
segments
|
|
222
303
|
};
|
|
@@ -227,12 +308,8 @@ export function inspectDocumentParts(parts, options = {}) {
|
|
|
227
308
|
definition.anchoredText ??= commentAnchors.get(id) || paragraph.text;
|
|
228
309
|
comments.set(id, definition);
|
|
229
310
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (options.skipEmpty) paragraphs = paragraphs.filter(item => item.text.length > 0);
|
|
233
|
-
if (options.search) { const needle = String(options.search).toLowerCase(); paragraphs = paragraphs.filter(item => item.text.toLowerCase().includes(needle)); }
|
|
234
|
-
if (Array.isArray(options.indexes)) { const indexes = new Set(options.indexes); paragraphs = paragraphs.filter(item => indexes.has(item.index)); }
|
|
235
|
-
if (options.range) { const start = Number(options.range.start ?? options.range[0]); const end = Number(options.range.end ?? options.range[1]); paragraphs = paragraphs.filter(item => item.index >= start && item.index <= end); }
|
|
311
|
+
const selected = selectInspectionParagraphs(paragraphs, options);
|
|
312
|
+
paragraphs = selected.paragraphs;
|
|
236
313
|
const allRevisionAuthors = [...new Set(paragraphs.flatMap(item => item.revisionAuthors))].sort();
|
|
237
314
|
const coveredEntries = extractDocumentPartsEntries(parts);
|
|
238
315
|
const coveredParts = coveredEntries.map(e => e.name).sort();
|
|
@@ -249,6 +326,7 @@ export function inspectDocumentParts(parts, options = {}) {
|
|
|
249
326
|
revisionToken,
|
|
250
327
|
coveredParts,
|
|
251
328
|
paragraphs,
|
|
329
|
+
...(selected.selection ? { selection: selected.selection } : {}),
|
|
252
330
|
comments: [...comments.values()],
|
|
253
331
|
revisionAuthors: allRevisionAuthors,
|
|
254
332
|
commentAuthors: [...new Set([...comments.values()].map(item => item.author).filter(Boolean))].sort(),
|