@ansonlai/docx-redline-js 0.7.2 → 0.8.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 (53) hide show
  1. package/AGENTS.md +15 -10
  2. package/ARCHITECTURE.md +30 -10
  3. package/CHANGELOG.md +539 -431
  4. package/README.md +331 -309
  5. package/adapters/xml-adapter.js +84 -56
  6. package/core/sha256.js +120 -0
  7. package/dist/docx-redline-js.esm.js +8982 -693
  8. package/dist/docx-redline-js.esm.js.map +4 -4
  9. package/dist/docx-redline-js.esm.min.js +102 -89
  10. package/dist/docx-redline-js.esm.min.js.map +4 -4
  11. package/dist/docx-redline.bundle.cjs +28305 -0
  12. package/dist/docx-redline.bundle.cjs.map +7 -0
  13. package/dist/docx-redline.bundle.js +28296 -0
  14. package/dist/docx-redline.bundle.js.map +7 -0
  15. package/docs/AGENT_FAST_START.md +2 -1
  16. package/docs/AGENT_KNOWLEDGE_BASE.md +480 -393
  17. package/docs/SKILL_AUTHORING.md +5 -2
  18. package/docs/TESTING.md +2 -1
  19. package/docs/schemas/document-operations.schema.json +29 -0
  20. package/document/docx-document.d.ts +39 -0
  21. package/document/docx-document.js +1010 -0
  22. package/document/zip-archive.js +258 -0
  23. package/engine/reconstruction-writer.js +18 -0
  24. package/index.d.ts +598 -531
  25. package/index.js +349 -340
  26. package/node/cli-help.js +5 -5
  27. package/node/cli.js +760 -754
  28. package/node/docx-document.js +11 -355
  29. package/node/index.d.ts +1 -1
  30. package/node/zip-archive.js +28 -38
  31. package/package.json +14 -11
  32. package/scripts/build.mjs +40 -5
  33. package/scripts/generate-word-comment-edge-case-fixture.ps1 +58 -0
  34. package/scripts/generate-word-comment-thread-fixture.ps1 +36 -0
  35. package/scripts/generate-word-header-footer-fixture.ps1 +32 -0
  36. package/scripts/generate-word-multi-section-fixture.ps1 +27 -0
  37. package/scripts/generate-word-package-integrity-cases.mjs +141 -0
  38. package/scripts/generate-word-thread-state-fixtures.ps1 +50 -0
  39. package/scripts/word-com-package-integrity.ps1 +98 -0
  40. package/services/batch-operation-orchestrator.js +262 -259
  41. package/services/comment-replies.js +96 -95
  42. package/services/comment-thread-parts.js +280 -0
  43. package/services/document-inspection.js +366 -365
  44. package/services/document-operation-applier.js +230 -202
  45. package/services/document-operation-contract.js +17 -2
  46. package/services/error-recovery.js +6 -0
  47. package/services/headers-footers.js +206 -0
  48. package/services/operation-batch-compiler.js +1 -0
  49. package/services/operation-preflight.js +231 -218
  50. package/services/package-parts.js +80 -0
  51. package/services/revision-token.js +11 -7
  52. package/services/standalone-docx-plumbing.js +631 -569
  53. package/services/standalone-operation-runner.d.ts +19 -3
@@ -1,8 +1,8 @@
1
1
  import { createSerializer, parseOoxmlSafe } from '../adapters/xml-adapter.js';
2
2
  import { getDefaultAuthor } from '../adapters/config.js';
3
- import {
4
- isExistingRevisionsPolicy,
5
- normalizeDocumentOperation,
3
+ import {
4
+ isExistingRevisionsPolicy,
5
+ normalizeDocumentOperation,
6
6
  resolveDocumentOperationAuthor
7
7
  } from './document-operation-contract.js';
8
8
  import {
@@ -19,17 +19,17 @@ import {
19
19
  computeDocumentPartsRevisionToken,
20
20
  areRevisionTokensEqual
21
21
  } from './revision-token.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';
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';
28
28
 
29
29
  const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
30
30
 
31
31
  function operationTargetPriority(op) {
32
- return op?.type === 'comment' || op?.type === 'comment_reply' ? 0 : 1;
32
+ return op?.type === 'comment' || op?.type === 'comment_reply' || op?.type === 'comment_resolve' ? 0 : 1;
33
33
  }
34
34
 
35
35
  /**
@@ -72,10 +72,10 @@ export function buildOperationDependencyPlan(operations = []) {
72
72
  }
73
73
 
74
74
  // 2. Build dependency edges
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();
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();
79
79
 
80
80
  for (let i = 0; i < list.length; i++) {
81
81
  const op = list[i];
@@ -110,40 +110,40 @@ export function buildOperationDependencyPlan(operations = []) {
110
110
  }
111
111
  };
112
112
  }
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
- }
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' && kind !== 'comment_resolve') {
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
+ }
147
147
 
148
148
  // 3. Stable topological sort with comment priority among ready nodes
149
149
  const ready = [];
@@ -214,63 +214,63 @@ function mergeCommentsXml(existingXml, incomingXml) {
214
214
  return serializer.serializeToString(existingDoc);
215
215
  }
216
216
 
217
- export async function applyOperationsToDocumentXml(documentXml, operations, author, runtimeContext = null, options = {}) {
217
+ export async function applyOperationsToDocumentXml(documentXml, operations, author, runtimeContext = null, options = {}) {
218
218
  const sourceOperations = Array.isArray(operations) ? operations : [];
219
219
  const defaultAuthor = getDefaultAuthor();
220
- const emptyReceipts = () => sourceOperations.map((op, i) => createEmptyReceipt(
220
+ const emptyReceipts = () => sourceOperations.map((op, i) => createEmptyReceipt(
221
221
  i + 1,
222
222
  op?.operationId,
223
223
  resolveDocumentOperationAuthor(op, author, defaultAuthor),
224
- 'not_attempted'
225
- ));
226
- const failedBeforeExecution = (error, extra = {}) => {
227
- const receipts = emptyReceipts();
228
- return {
229
- documentXml,
230
- hasChanges: false,
231
- commentsXml: null,
232
- numberingXmlParts: [],
233
- results: [],
234
- receipts,
235
- executionOrder: [],
236
- authorsUsed: [],
237
- ...(options.atomic === true ? { rolledBack: true } : {}),
238
- status: 'error',
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
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
- });
257
- }
224
+ 'not_attempted'
225
+ ));
226
+ const failedBeforeExecution = (error, extra = {}) => {
227
+ const receipts = emptyReceipts();
228
+ return {
229
+ documentXml,
230
+ hasChanges: false,
231
+ commentsXml: null,
232
+ numberingXmlParts: [],
233
+ results: [],
234
+ receipts,
235
+ executionOrder: [],
236
+ authorsUsed: [],
237
+ ...(options.atomic === true ? { rolledBack: true } : {}),
238
+ status: 'error',
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
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
+ });
257
+ }
258
258
 
259
259
  if (options?.expectedRevision) {
260
260
  const tokenValidation = validateRevisionToken(options.expectedRevision);
261
261
  if (!tokenValidation.valid) {
262
- return failedBeforeExecution({
263
- code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
264
- message: tokenValidation.error?.message || 'Invalid revision token.'
265
- });
262
+ return failedBeforeExecution({
263
+ code: tokenValidation.error?.code || 'INVALID_REVISION_TOKEN',
264
+ message: tokenValidation.error?.message || 'Invalid revision token.'
265
+ });
266
266
  }
267
267
  if (options.expectedRevision.scope !== 'document-parts') {
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
- });
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
+ });
274
274
  }
275
275
  const currentToken = await computeDocumentPartsRevisionToken({
276
276
  documentXml,
@@ -280,12 +280,12 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
280
280
  stylesXml: runtimeContext?.stylesXml || options.stylesXml
281
281
  }, options);
282
282
  if (!areRevisionTokensEqual(currentToken.value, options.expectedRevision.value)) {
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
- });
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
+ });
289
289
  }
290
290
  }
291
291
 
@@ -296,73 +296,73 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
296
296
  _deferDocumentSerialization: true
297
297
  });
298
298
  if (!session.valid) {
299
- return failedBeforeExecution(session.parseResult.error, { warnings: session.parseResult.warnings });
299
+ return failedBeforeExecution(session.parseResult.error, { warnings: session.parseResult.warnings });
300
+ }
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
+ };
300
361
  }
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);
362
+ const executableOperations = compilation.compiledOperations;
363
+ const dependencyPlan = buildOperationDependencyPlan(executableOperations);
364
364
  if (!dependencyPlan.valid) {
365
- return failedBeforeExecution(dependencyPlan.error);
365
+ return failedBeforeExecution(dependencyPlan.error);
366
366
  }
367
367
  const scheduled = dependencyPlan.scheduled;
368
368
 
@@ -373,7 +373,7 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
373
373
  context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
374
374
  }
375
375
  session.runtimeContext = context;
376
- if (sourceOperations.some(op => op?.type === 'comment_reply')) {
376
+ if (sourceOperations.some(op => op?.type === 'comment_reply' || op?.type === 'comment_resolve')) {
377
377
  session.commentsXml = context.commentsXml || options.commentsXml || null;
378
378
  session.commentsExtendedXml = context.commentsExtendedXml || options.commentsExtendedXml || null;
379
379
  session.commentsXmlMode = 'replace';
@@ -386,37 +386,37 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
386
386
  const authorsUsed = session.authorsUsed;
387
387
  let operationFailed = false;
388
388
 
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 {
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 {
420
420
  const result = await applyOperationToDocumentXml(
421
421
  session.currentDocumentXml,
422
422
  operation,
@@ -450,18 +450,21 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
450
450
  authorUsed: result.authorUsed,
451
451
  ...(result.resolvedBy ? { resolvedBy: result.resolvedBy } : {}),
452
452
  ...(result.resolvedTarget ? { resolvedTarget: result.resolvedTarget } : {}),
453
- ...(result.resolvedAnchor ? { resolvedAnchor: result.resolvedAnchor } : {}),
454
- ...(result.change ? { change: result.change } : {}),
455
- ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
453
+ ...(result.resolvedAnchor ? { resolvedAnchor: result.resolvedAnchor } : {}),
454
+ ...(result.change ? { change: result.change } : {}),
455
+ ...(result.operationType === 'comment_resolve' && !isError
456
+ ? { resolved: result.resolved, threadRootId: result.threadRootId, commentIds: result.commentIds }
457
+ : {}),
458
+ ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
456
459
  ...(result.error ? { error: result.error } : {}),
457
460
  ...(result.receipt ? { receipt: result.receipt } : {})
458
461
  });
459
462
  if (isError && !continueOnError) break;
460
- } catch (error) {
461
- const normalizedError = normalizeOperationError(error, {
462
- operationIndex: index + 1,
463
- ...(operation?.operationId ? { operationId: operation.operationId } : {})
464
- });
463
+ } catch (error) {
464
+ const normalizedError = normalizeOperationError(error, {
465
+ operationIndex: index + 1,
466
+ ...(operation?.operationId ? { operationId: operation.operationId } : {})
467
+ });
465
468
  operationFailed = true;
466
469
  const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
467
470
  const errorReceipt = createEmptyReceipt(
@@ -532,34 +535,34 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
532
535
  }
533
536
  }
534
537
 
535
- if (rolledBack) {
536
- for (const r of results) {
537
- if (r.receipt && r.receipt.attemptedDisposition === 'applied') {
538
- r.receipt.finalDisposition = 'rolled_back';
539
- r.receipt.committed = false;
540
- }
541
- if (r.change) {
542
- r.change.finalDisposition = 'rolled_back';
543
- r.change.committed = false;
544
- }
545
- }
538
+ if (rolledBack) {
539
+ for (const r of results) {
540
+ if (r.receipt && r.receipt.attemptedDisposition === 'applied') {
541
+ r.receipt.finalDisposition = 'rolled_back';
542
+ r.receipt.committed = false;
543
+ }
544
+ if (r.change) {
545
+ r.change.finalDisposition = 'rolled_back';
546
+ r.change.committed = false;
547
+ }
548
+ }
546
549
  for (const receipt of allReceipts) {
547
550
  if (receipt.attemptedDisposition === 'applied') {
548
551
  receipt.finalDisposition = 'rolled_back';
549
552
  receipt.committed = false;
550
553
  }
551
554
  }
552
- } else {
553
- for (const r of results) {
554
- if (r.receipt && r.receipt.attemptedDisposition === 'applied') {
555
- r.receipt.committed = true;
556
- r.receipt.finalDisposition = 'applied';
557
- }
558
- if (r.change) {
559
- r.change.finalDisposition = 'applied';
560
- r.change.committed = true;
561
- }
562
- }
555
+ } else {
556
+ for (const r of results) {
557
+ if (r.receipt && r.receipt.attemptedDisposition === 'applied') {
558
+ r.receipt.committed = true;
559
+ r.receipt.finalDisposition = 'applied';
560
+ }
561
+ if (r.change) {
562
+ r.change.finalDisposition = 'applied';
563
+ r.change.committed = true;
564
+ }
565
+ }
563
566
  for (const receipt of allReceipts) {
564
567
  if (receipt.attemptedDisposition === 'applied') {
565
568
  receipt.committed = true;
@@ -570,8 +573,8 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
570
573
 
571
574
  if (!rolledBack) commitBatchRuntimeContext(runtimeContext, context);
572
575
 
573
- const batchResult = {
574
- documentXml: rolledBack ? session.rollback() : outputDocumentXml,
576
+ const batchResult = {
577
+ documentXml: rolledBack ? session.rollback() : outputDocumentXml,
575
578
  hasChanges: rolledBack ? false : hasChanges,
576
579
  commentsXml: rolledBack ? null : session.commentsXml,
577
580
  commentsExtendedXml: rolledBack ? null : session.commentsExtendedXml,
@@ -585,33 +588,33 @@ export async function applyOperationsToDocumentXml(documentXml, operations, auth
585
588
  ...(rolledBack ? {
586
589
  rolledBack: true,
587
590
  status: 'error',
588
- error: normalizeOperationError({
589
- code: reconciliationError ? reconciliationError.code : (serializationError ? 'DOCUMENT_SERIALIZATION_FAILED' : 'BATCH_OPERATION_FAILED'),
590
- message: reconciliationError?.message
591
- || serializationError?.message
592
- || 'Atomic batch rolled back because one or more operations failed.'
593
- })
594
- } : (reconciliationError ? {
595
- status: 'error',
596
- error: normalizeOperationError(reconciliationError)
597
- } : (operationFailed ? {
598
- status: hasChanges ? 'partial' : 'error',
599
- error: normalizeOperationError({
600
- code: 'BATCH_OPERATION_FAILED',
601
- message: 'One or more operations failed.'
602
- })
603
- } : {
604
- status: 'ok'
605
- })))
606
- };
607
- if (batchResult.status === 'error' || batchResult.status === 'partial') {
608
- batchResult.retryPlan = createRetryPlan({
609
- atomic,
610
- rolledBack,
611
- results: batchResult.results,
612
- receipts: batchResult.receipts,
613
- operationCount: sourceOperations.length
614
- });
615
- }
616
- return batchResult;
617
- }
591
+ error: normalizeOperationError({
592
+ code: reconciliationError ? reconciliationError.code : (serializationError ? 'DOCUMENT_SERIALIZATION_FAILED' : 'BATCH_OPERATION_FAILED'),
593
+ message: reconciliationError?.message
594
+ || serializationError?.message
595
+ || 'Atomic batch rolled back because one or more operations failed.'
596
+ })
597
+ } : (reconciliationError ? {
598
+ status: 'error',
599
+ error: normalizeOperationError(reconciliationError)
600
+ } : (operationFailed ? {
601
+ status: hasChanges ? 'partial' : 'error',
602
+ error: normalizeOperationError({
603
+ code: 'BATCH_OPERATION_FAILED',
604
+ message: 'One or more operations failed.'
605
+ })
606
+ } : {
607
+ status: 'ok'
608
+ })))
609
+ };
610
+ if (batchResult.status === 'error' || batchResult.status === 'partial') {
611
+ batchResult.retryPlan = createRetryPlan({
612
+ atomic,
613
+ rolledBack,
614
+ results: batchResult.results,
615
+ receipts: batchResult.receipts,
616
+ operationCount: sourceOperations.length
617
+ });
618
+ }
619
+ return batchResult;
620
+ }