@ansonlai/docx-redline-js 0.4.0 → 0.5.0

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.
Files changed (100) hide show
  1. package/AGENTS.md +589 -287
  2. package/ARCHITECTURE.md +215 -9
  3. package/CHANGELOG.md +319 -0
  4. package/README.md +604 -360
  5. package/adapters/config.js +45 -43
  6. package/bin/docx-redline.js +3 -0
  7. package/core/list-targeting.js +101 -110
  8. package/core/paragraph-targeting.js +501 -61
  9. package/core/paragraph-text.js +209 -0
  10. package/core/revision-cloning.js +38 -0
  11. package/core/types.js +64 -10
  12. package/core/word-xml.js +43 -15
  13. package/dist/docx-redline-js.esm.js +2849 -466
  14. package/dist/docx-redline-js.esm.js.map +4 -4
  15. package/dist/docx-redline-js.esm.min.js +87 -76
  16. package/dist/docx-redline-js.esm.min.js.map +4 -4
  17. package/docs/TESTING.md +342 -23
  18. package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
  19. package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
  20. package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
  21. package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
  22. package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
  23. package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
  24. package/docs/schemas/document-operations.schema.json +109 -0
  25. package/docs/test-comparison-dashboard.html +4250 -7
  26. package/engine/formatting-removal.js +11 -2
  27. package/engine/oxml-engine.js +491 -336
  28. package/engine/reconstruction-mode.js +15 -14
  29. package/engine/reconstruction-writer.js +247 -142
  30. package/engine/route-selection.js +35 -0
  31. package/engine/rpr-helpers.js +334 -35
  32. package/engine/run-builders.js +239 -196
  33. package/engine/surgical-diff-application.js +222 -37
  34. package/engine/surgical-mode.js +134 -6
  35. package/engine/surgical-spans.js +52 -1
  36. package/engine/table-cell-context.js +3 -6
  37. package/engine/table-mode.js +1 -1
  38. package/index.d.ts +234 -6
  39. package/index.js +24 -1
  40. package/node/cli.js +317 -0
  41. package/node/docx-document.js +302 -0
  42. package/node/index.d.ts +31 -0
  43. package/node/index.js +2 -0
  44. package/node/zip-archive.js +52 -0
  45. package/orchestration/list-markdown.js +10 -16
  46. package/orchestration/list-parsing.js +7 -12
  47. package/orchestration/list-structural-fallback.js +21 -10
  48. package/package.json +24 -3
  49. package/pipeline/content-analysis.js +12 -17
  50. package/pipeline/ingestion-export.js +3 -31
  51. package/pipeline/ingestion-paragraph.js +10 -5
  52. package/pipeline/list-generation.js +150 -55
  53. package/pipeline/list-markers.js +70 -3
  54. package/pipeline/serialization.js +4 -2
  55. package/pipeline/structured-content.js +160 -0
  56. package/scripts/apply_changes.mjs +27 -0
  57. package/scripts/benchmark-operation-session.mjs +137 -0
  58. package/scripts/benchmark-targeting-browser.html +74 -0
  59. package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
  60. package/scripts/benchmark-test-runner.mjs +59 -0
  61. package/scripts/build-test-dashboard.mjs +23 -0
  62. package/scripts/export-lane1-fixtures.mjs +380 -0
  63. package/scripts/export-reredline-stress-fixtures.mjs +317 -0
  64. package/scripts/export-validation-fixtures.mjs +1 -1
  65. package/scripts/extract_text.mjs +7 -0
  66. package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
  67. package/scripts/generate-test-dashboard.mjs +362 -11
  68. package/scripts/lib/word-coverage-catalogue.mjs +6 -2
  69. package/scripts/profile-route-selection.mjs +19 -0
  70. package/scripts/render-agenda-multilevel.mjs +0 -5
  71. package/scripts/render-multilevel-cases.mjs +0 -1
  72. package/scripts/run-tests.mjs +107 -35
  73. package/scripts/word-com-corpus-suite.ps1 +3 -0
  74. package/scripts/word-com-differential.ps1 +64 -4
  75. package/scripts/word-com-suite.ps1 +3 -0
  76. package/services/batch-operation-orchestrator.js +494 -0
  77. package/services/capture-engine.js +226 -0
  78. package/services/comment-builders.js +23 -6
  79. package/services/comment-engine.js +108 -47
  80. package/services/comment-locator.js +187 -82
  81. package/services/comment-replies.js +95 -0
  82. package/services/document-inspection.js +258 -0
  83. package/services/document-operation-applier.js +372 -0
  84. package/services/document-operation-contract.js +323 -0
  85. package/services/document-operation-mutations.js +1733 -0
  86. package/services/document-operation-session.js +258 -0
  87. package/services/numbering-service.js +14 -5
  88. package/services/operation-heuristics.js +173 -0
  89. package/services/operation-preflight.js +366 -0
  90. package/services/receipt-collector.js +288 -0
  91. package/services/revision-comment-management.js +37 -5
  92. package/services/revision-token.js +290 -0
  93. package/services/standalone-docx-plumbing.js +123 -8
  94. package/services/standalone-operation-runner.d.ts +296 -0
  95. package/services/standalone-operation-runner.js +10 -1455
  96. package/services/table-reconciliation.js +15 -6
  97. package/docs/VALIDATION.md +0 -183
  98. package/docs/WORD-MANUAL-REVIEW.md +0 -138
  99. package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
  100. /package/docs/plans/{2026-08-30-reliability-testing-improvements.md → completed/2026-08-30-reliability-testing-improvements.md} +0 -0
@@ -0,0 +1,494 @@
1
+ import { createSerializer, parseOoxmlSafe } from '../adapters/xml-adapter.js';
2
+ import { getDefaultAuthor } from '../adapters/config.js';
3
+ import {
4
+ normalizeDocumentOperation,
5
+ resolveDocumentOperationAuthor
6
+ } from './document-operation-contract.js';
7
+ import {
8
+ applyOperationToDocumentXml,
9
+ normalizeOperationError
10
+ } from './document-operation-applier.js';
11
+ import {
12
+ cloneBatchRuntimeContext,
13
+ commitBatchRuntimeContext,
14
+ DocumentOperationSession
15
+ } from './document-operation-session.js';
16
+ import {
17
+ validateRevisionToken,
18
+ computeDocumentPartsRevisionToken,
19
+ areRevisionTokensEqual
20
+ } from './revision-token.js';
21
+ import {
22
+ createEmptyReceipt,
23
+ reconcileReceiptsAgainstOutput
24
+ } from './receipt-collector.js';
25
+
26
+ const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
27
+
28
+ function operationTargetPriority(op) {
29
+ return op?.type === 'comment' || op?.type === 'comment_reply' ? 0 : 1;
30
+ }
31
+
32
+ /**
33
+ * Builds a dependency graph and topological execution plan for a batch of operations.
34
+ *
35
+ * @param {Array<object>} operations
36
+ * @returns {{ valid: boolean, scheduled?: Array<{ operation: object, index: number }>, captureProducers?: Map<string, number>, error?: { code: string, message: string } }}
37
+ */
38
+ export function buildOperationDependencyPlan(operations = []) {
39
+ const list = Array.isArray(operations) ? operations : [];
40
+ const captureProducers = new Map();
41
+
42
+ // 1. Collect and validate capture keys
43
+ for (let i = 0; i < list.length; i++) {
44
+ const op = list[i];
45
+ const captureKey = op?.captureKey ?? op?.targetDescriptor?.captureKey;
46
+ if (captureKey != null) {
47
+ if (typeof captureKey !== 'string' || !captureKey.trim() || captureKey.trim().length > 256) {
48
+ return {
49
+ valid: false,
50
+ error: {
51
+ code: 'INVALID_OPERATION',
52
+ message: `Operation ${i + 1} declared an invalid captureKey: must be a non-empty string under 256 characters.`
53
+ }
54
+ };
55
+ }
56
+ const key = captureKey.trim();
57
+ if (captureProducers.has(key)) {
58
+ const priorIndex = captureProducers.get(key);
59
+ return {
60
+ valid: false,
61
+ error: {
62
+ code: 'DUPLICATE_CAPTURE_KEY',
63
+ message: `Duplicate capture key "${key}" declared by operations at indices ${priorIndex + 1} and ${i + 1}.`
64
+ }
65
+ };
66
+ }
67
+ captureProducers.set(key, i);
68
+ }
69
+ }
70
+
71
+ // 2. Build dependency edges
72
+ const inDegrees = new Array(list.length).fill(0);
73
+ const dependents = Array.from({ length: list.length }, () => new Set());
74
+ const dependencies = Array.from({ length: list.length }, () => new Set());
75
+
76
+ for (let i = 0; i < list.length; i++) {
77
+ const op = list[i];
78
+ const captureRef = op?.target?.captureRef ?? op?.targetDescriptor?.captureRef;
79
+ if (captureRef != null) {
80
+ if (typeof captureRef !== 'string' || !captureRef.trim() || captureRef.trim().length > 256) {
81
+ return {
82
+ valid: false,
83
+ error: {
84
+ code: 'INVALID_OPERATION',
85
+ message: `Operation ${i + 1} referenced an invalid captureRef: must be a non-empty string under 256 characters.`
86
+ }
87
+ };
88
+ }
89
+ const ref = captureRef.trim();
90
+ if (!captureProducers.has(ref)) {
91
+ return {
92
+ valid: false,
93
+ error: {
94
+ code: 'CAPTURE_NOT_FOUND',
95
+ message: `Capture "${ref}" referenced by operation ${i + 1} was not found in the batch.`
96
+ }
97
+ };
98
+ }
99
+ const producerIndex = captureProducers.get(ref);
100
+ if (producerIndex === i) {
101
+ return {
102
+ valid: false,
103
+ error: {
104
+ code: 'CAPTURE_DEPENDENCY_CYCLE',
105
+ message: `Capture dependency cycle detected: operation ${i + 1} references its own capture "${ref}".`
106
+ }
107
+ };
108
+ }
109
+ if (!dependencies[i].has(producerIndex)) {
110
+ dependencies[i].add(producerIndex);
111
+ dependents[producerIndex].add(i);
112
+ inDegrees[i]++;
113
+ }
114
+ }
115
+ }
116
+
117
+ // 3. Stable topological sort with comment priority among ready nodes
118
+ const ready = [];
119
+ for (let i = 0; i < list.length; i++) {
120
+ if (inDegrees[i] === 0) {
121
+ ready.push({ operation: list[i], index: i });
122
+ }
123
+ }
124
+
125
+ const scheduled = [];
126
+ while (ready.length > 0) {
127
+ ready.sort((a, b) => operationTargetPriority(a.operation) - operationTargetPriority(b.operation) || a.index - b.index);
128
+ const next = ready.shift();
129
+ scheduled.push(next);
130
+
131
+ for (const depIndex of dependents[next.index]) {
132
+ inDegrees[depIndex]--;
133
+ if (inDegrees[depIndex] === 0) {
134
+ ready.push({ operation: list[depIndex], index: depIndex });
135
+ }
136
+ }
137
+ }
138
+
139
+ if (scheduled.length < list.length) {
140
+ return {
141
+ valid: false,
142
+ error: {
143
+ code: 'CAPTURE_DEPENDENCY_CYCLE',
144
+ message: 'Capture dependency cycle detected among batch operations.'
145
+ }
146
+ };
147
+ }
148
+
149
+ return {
150
+ valid: true,
151
+ scheduled,
152
+ captureProducers
153
+ };
154
+ }
155
+
156
+ export function orderOperationsForStableTargets(operations = []) {
157
+ const plan = buildOperationDependencyPlan(operations);
158
+ if (!plan.valid) {
159
+ throw Object.assign(new Error(plan.error.message), { code: plan.error.code });
160
+ }
161
+ return plan.scheduled.map(item => item.operation);
162
+ }
163
+
164
+ function mergeCommentsXml(existingXml, incomingXml) {
165
+ if (!incomingXml) return existingXml;
166
+ if (!existingXml) return incomingXml;
167
+ const existingDoc = parseOoxmlSafe(existingXml, 'application/xml')?.doc;
168
+ const incomingDoc = parseOoxmlSafe(incomingXml, 'application/xml')?.doc;
169
+ if (!existingDoc || !incomingDoc) return existingXml;
170
+ const serializer = createSerializer();
171
+ const existingRoot = existingDoc.documentElement;
172
+ const existingIds = new Set(
173
+ Array.from(existingDoc.getElementsByTagNameNS(NS_W, 'comment'))
174
+ .map(node => node.getAttribute('w:id') || node.getAttribute('id'))
175
+ .filter(Boolean)
176
+ );
177
+ for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W, 'comment'))) {
178
+ const id = comment.getAttribute('w:id') || comment.getAttribute('id');
179
+ if (id && existingIds.has(id)) continue;
180
+ existingRoot.appendChild(existingDoc.importNode(comment, true));
181
+ if (id) existingIds.add(id);
182
+ }
183
+ return serializer.serializeToString(existingDoc);
184
+ }
185
+
186
+ export async function applyOperationsToDocumentXml(documentXml, operations, author, runtimeContext = null, options = {}) {
187
+ const sourceOperations = Array.isArray(operations) ? operations : [];
188
+ const defaultAuthor = getDefaultAuthor();
189
+ const emptyReceipts = () => sourceOperations.map((op, i) => createEmptyReceipt(
190
+ i + 1,
191
+ op?.operationId,
192
+ resolveDocumentOperationAuthor(op, author, defaultAuthor),
193
+ 'not_attempted'
194
+ ));
195
+
196
+ if (options?.expectedRevision) {
197
+ const tokenValidation = validateRevisionToken(options.expectedRevision);
198
+ if (!tokenValidation.valid) {
199
+ return {
200
+ documentXml,
201
+ hasChanges: false,
202
+ commentsXml: null,
203
+ numberingXmlParts: [],
204
+ results: [],
205
+ receipts: emptyReceipts(),
206
+ executionOrder: [],
207
+ authorsUsed: [],
208
+ status: 'error',
209
+ error: {
210
+ code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
211
+ message: tokenValidation.error?.message || 'Invalid revision token.'
212
+ }
213
+ };
214
+ }
215
+ if (options.expectedRevision.scope !== 'document-parts') {
216
+ return {
217
+ documentXml,
218
+ hasChanges: false,
219
+ commentsXml: null,
220
+ numberingXmlParts: [],
221
+ results: [],
222
+ receipts: emptyReceipts(),
223
+ executionOrder: [],
224
+ authorsUsed: [],
225
+ status: 'error',
226
+ error: {
227
+ code: 'REVISION_TOKEN_SCOPE_MISMATCH',
228
+ message: `Revision token scope mismatch: expected 'document-parts', got '${options.expectedRevision.scope}'.`
229
+ }
230
+ };
231
+ }
232
+ const currentToken = await computeDocumentPartsRevisionToken({
233
+ documentXml,
234
+ commentsXml: runtimeContext?.commentsXml || options.commentsXml,
235
+ commentsExtendedXml: runtimeContext?.commentsExtendedXml || options.commentsExtendedXml,
236
+ numberingXml: runtimeContext?.numberingXml || options.numberingXml,
237
+ stylesXml: runtimeContext?.stylesXml || options.stylesXml
238
+ }, options);
239
+ if (!areRevisionTokensEqual(currentToken.value, options.expectedRevision.value)) {
240
+ return {
241
+ documentXml,
242
+ hasChanges: false,
243
+ commentsXml: null,
244
+ numberingXmlParts: [],
245
+ results: [],
246
+ receipts: emptyReceipts(),
247
+ executionOrder: [],
248
+ authorsUsed: [],
249
+ status: 'error',
250
+ error: {
251
+ code: 'REVISION_MISMATCH',
252
+ message: `Document revision mismatch: expected '${options.expectedRevision.value}', current is '${currentToken.value}'.`
253
+ }
254
+ };
255
+ }
256
+ }
257
+
258
+ const session = options?._documentOperationSession instanceof DocumentOperationSession
259
+ ? options._documentOperationSession
260
+ : new DocumentOperationSession(documentXml, {
261
+ ...options,
262
+ _deferDocumentSerialization: true
263
+ });
264
+ if (!session.valid) {
265
+ return {
266
+ documentXml,
267
+ hasChanges: false,
268
+ commentsXml: null,
269
+ numberingXmlParts: [],
270
+ results: [],
271
+ receipts: emptyReceipts(),
272
+ executionOrder: [],
273
+ authorsUsed: [],
274
+ status: 'error',
275
+ error: session.parseResult.error,
276
+ warnings: session.parseResult.warnings
277
+ };
278
+ }
279
+ const dependencyPlan = buildOperationDependencyPlan(sourceOperations);
280
+ if (!dependencyPlan.valid) {
281
+ return {
282
+ documentXml,
283
+ hasChanges: false,
284
+ commentsXml: null,
285
+ numberingXmlParts: [],
286
+ results: [],
287
+ receipts: emptyReceipts(),
288
+ executionOrder: [],
289
+ authorsUsed: [],
290
+ status: 'error',
291
+ error: dependencyPlan.error
292
+ };
293
+ }
294
+ const scheduled = dependencyPlan.scheduled;
295
+
296
+ const atomic = options.atomic === true;
297
+ const continueOnError = options.continueOnError !== false;
298
+ const context = cloneBatchRuntimeContext(runtimeContext);
299
+ if (!(context.targetRefSnapshot instanceof Map)) {
300
+ context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
301
+ }
302
+ session.runtimeContext = context;
303
+ if (sourceOperations.some(op => op?.type === 'comment_reply')) {
304
+ session.commentsXml = context.commentsXml || options.commentsXml || null;
305
+ session.commentsExtendedXml = context.commentsExtendedXml || options.commentsExtendedXml || null;
306
+ session.commentsXmlMode = 'replace';
307
+ session.commentsExtendedXmlMode = 'replace';
308
+ }
309
+
310
+ let hasChanges = false;
311
+ const results = session.results;
312
+ const executionOrder = session.executionOrder;
313
+ const authorsUsed = session.authorsUsed;
314
+ let operationFailed = false;
315
+
316
+ for (const { operation, index } of scheduled) {
317
+ executionOrder.push(index + 1);
318
+ try {
319
+ const result = await applyOperationToDocumentXml(
320
+ session.currentDocumentXml,
321
+ operation,
322
+ author,
323
+ context,
324
+ {
325
+ ...options,
326
+ _documentOperationSession: session,
327
+ _deferDocumentSerialization: true,
328
+ _operationIndex: index + 1
329
+ }
330
+ );
331
+ hasChanges = hasChanges || result.hasChanges === true;
332
+ session.commentsXml = result.commentsXmlMode === 'replace'
333
+ ? (result.commentsXml || session.commentsXml)
334
+ : mergeCommentsXml(session.commentsXml, result.commentsXml || null);
335
+ session.commentsExtendedXml = result.commentsExtendedXmlMode === 'replace'
336
+ ? (result.commentsExtendedXml || session.commentsExtendedXml)
337
+ : (result.commentsExtendedXml || session.commentsExtendedXml);
338
+ if (result.commentsXmlMode === 'replace') session.commentsXmlMode = 'replace';
339
+ if (result.commentsExtendedXmlMode === 'replace') session.commentsExtendedXmlMode = 'replace';
340
+ if (result.numberingXml) session.numberingXmlParts.push(result.numberingXml);
341
+ const isError = result.status === 'error' || !!result.error;
342
+ operationFailed = operationFailed || isError;
343
+ if (!isError && result.hasChanges && result.authorUsed) authorsUsed.add(result.authorUsed);
344
+ results.push({
345
+ index: index + 1,
346
+ type: operation?.type || 'redline',
347
+ status: isError ? 'error' : (result.hasChanges ? 'applied' : 'no_change'),
348
+ operationType: result.operationType || 'redline',
349
+ authorUsed: result.authorUsed,
350
+ ...(result.resolvedBy ? { resolvedBy: result.resolvedBy } : {}),
351
+ ...(result.resolvedTarget ? { resolvedTarget: result.resolvedTarget } : {}),
352
+ ...(result.resolvedAnchor ? { resolvedAnchor: result.resolvedAnchor } : {}),
353
+ ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
354
+ ...(result.error ? { error: result.error } : {}),
355
+ ...(result.receipt ? { receipt: result.receipt } : {})
356
+ });
357
+ if (isError && !continueOnError) break;
358
+ } catch (error) {
359
+ const normalizedError = normalizeOperationError(error);
360
+ operationFailed = true;
361
+ const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
362
+ const errorReceipt = createEmptyReceipt(
363
+ index + 1,
364
+ operation?.operationId,
365
+ authorUsed,
366
+ 'refused'
367
+ );
368
+ errorReceipt.warnings.push(normalizedError.message);
369
+ results.push({
370
+ index: index + 1,
371
+ type: operation?.type || 'redline',
372
+ status: 'error',
373
+ operationType: normalizeDocumentOperation(operation).operationKind,
374
+ authorUsed,
375
+ warnings: [normalizedError.message],
376
+ error: normalizedError,
377
+ receipt: errorReceipt
378
+ });
379
+ if (!continueOnError) break;
380
+ }
381
+ }
382
+
383
+ results.sort((a, b) => a.index - b.index);
384
+
385
+ const allReceipts = [];
386
+ for (let i = 0; i < sourceOperations.length; i++) {
387
+ const opIndex = i + 1;
388
+ const executed = results.find(r => r.index === opIndex);
389
+ if (executed?.receipt) {
390
+ allReceipts.push(executed.receipt);
391
+ } else {
392
+ const unattemptedReceipt = createEmptyReceipt(
393
+ opIndex,
394
+ sourceOperations[i]?.operationId,
395
+ resolveDocumentOperationAuthor(sourceOperations[i], author, defaultAuthor),
396
+ 'not_attempted'
397
+ );
398
+ allReceipts.push(unattemptedReceipt);
399
+ }
400
+ }
401
+
402
+ let rolledBack = atomic && operationFailed;
403
+ let outputDocumentXml = documentXml;
404
+ let serializationError = null;
405
+ if (!rolledBack && hasChanges) {
406
+ try {
407
+ outputDocumentXml = session.serializeCurrent();
408
+ } catch (error) {
409
+ serializationError = normalizeOperationError(error);
410
+ rolledBack = true;
411
+ }
412
+ }
413
+
414
+ let reconciliationError = null;
415
+ if (!rolledBack && hasChanges) {
416
+ const reconciliation = reconcileReceiptsAgainstOutput({
417
+ documentXml: outputDocumentXml,
418
+ commentsXml: session.commentsXml,
419
+ commentsExtendedXml: session.commentsExtendedXml,
420
+ numberingXmlParts: session.numberingXmlParts
421
+ }, allReceipts);
422
+ if (!reconciliation.valid) {
423
+ reconciliationError = reconciliation.error;
424
+ if (atomic) {
425
+ rolledBack = true;
426
+ }
427
+ }
428
+ }
429
+
430
+ if (rolledBack) {
431
+ for (const r of results) {
432
+ if (r.receipt && r.receipt.attemptedDisposition === 'applied') {
433
+ r.receipt.finalDisposition = 'rolled_back';
434
+ r.receipt.committed = false;
435
+ }
436
+ }
437
+ for (const receipt of allReceipts) {
438
+ if (receipt.attemptedDisposition === 'applied') {
439
+ receipt.finalDisposition = 'rolled_back';
440
+ receipt.committed = false;
441
+ }
442
+ }
443
+ } else {
444
+ for (const r of results) {
445
+ if (r.receipt && r.receipt.attemptedDisposition === 'applied') {
446
+ r.receipt.committed = true;
447
+ r.receipt.finalDisposition = 'applied';
448
+ }
449
+ }
450
+ for (const receipt of allReceipts) {
451
+ if (receipt.attemptedDisposition === 'applied') {
452
+ receipt.committed = true;
453
+ receipt.finalDisposition = 'applied';
454
+ }
455
+ }
456
+ }
457
+
458
+ if (!rolledBack) commitBatchRuntimeContext(runtimeContext, context);
459
+
460
+ return {
461
+ documentXml: rolledBack ? session.rollback() : outputDocumentXml,
462
+ hasChanges: rolledBack ? false : hasChanges,
463
+ commentsXml: rolledBack ? null : session.commentsXml,
464
+ commentsExtendedXml: rolledBack ? null : session.commentsExtendedXml,
465
+ commentsXmlMode: session.commentsXmlMode,
466
+ commentsExtendedXmlMode: session.commentsExtendedXmlMode,
467
+ numberingXmlParts: rolledBack ? [] : session.numberingXmlParts,
468
+ results: [...results].sort((a, b) => (a.index || 0) - (b.index || 0)),
469
+ receipts: allReceipts.sort((a, b) => a.operationIndex - b.operationIndex),
470
+ executionOrder,
471
+ authorsUsed: rolledBack ? [] : Array.from(authorsUsed),
472
+ ...(rolledBack ? {
473
+ rolledBack: true,
474
+ status: 'error',
475
+ error: {
476
+ code: reconciliationError ? reconciliationError.code : (serializationError ? 'DOCUMENT_SERIALIZATION_FAILED' : 'BATCH_OPERATION_FAILED'),
477
+ message: reconciliationError?.message
478
+ || serializationError?.message
479
+ || 'Atomic batch rolled back because one or more operations failed.'
480
+ }
481
+ } : (reconciliationError ? {
482
+ status: 'error',
483
+ error: reconciliationError
484
+ } : (operationFailed ? {
485
+ status: hasChanges ? 'partial' : 'error',
486
+ error: {
487
+ code: 'BATCH_OPERATION_FAILED',
488
+ message: 'One or more operations failed.'
489
+ }
490
+ } : {
491
+ status: 'ok'
492
+ })))
493
+ };
494
+ }