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