@ansonlai/docx-redline-js 0.8.3 → 0.9.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 (71) hide show
  1. package/ARCHITECTURE.md +221 -12
  2. package/CHANGELOG.md +803 -586
  3. package/README.md +1018 -901
  4. package/core/current-properties.js +136 -0
  5. package/core/paragraph-text.js +211 -209
  6. package/core/types.js +9 -3
  7. package/core/word-revision-semantics.js +256 -0
  8. package/dist/docx-redline-js.esm.js +4551 -967
  9. package/dist/docx-redline-js.esm.js.map +4 -4
  10. package/dist/docx-redline-js.esm.min.js +112 -106
  11. package/dist/docx-redline-js.esm.min.js.map +4 -4
  12. package/dist/docx-redline.bundle.cjs +4690 -1106
  13. package/dist/docx-redline.bundle.cjs.map +4 -4
  14. package/dist/docx-redline.bundle.js +4690 -1106
  15. package/dist/docx-redline.bundle.js.map +4 -4
  16. package/docs/AGENT_FAST_START.md +2 -0
  17. package/docs/AGENT_KNOWLEDGE_BASE.md +23 -4
  18. package/docs/SKILL_AUTHORING.md +165 -150
  19. package/docs/TESTING.md +1142 -1059
  20. package/docs/schemas/document-operations.schema.json +221 -168
  21. package/document/docx-document.js +1022 -1010
  22. package/engine/oxml-engine.js +567 -560
  23. package/engine/paragraph-mark-revisions.js +292 -0
  24. package/engine/reconstruction-mapper.js +312 -311
  25. package/engine/reconstruction-mode.js +183 -183
  26. package/engine/reconstruction-writer.js +67 -10
  27. package/engine/rpr-helpers.js +42 -0
  28. package/engine/run-builders.js +0 -73
  29. package/engine/surgical-diff-application.js +604 -605
  30. package/engine/table-mode.js +63 -63
  31. package/index.d.ts +51 -1
  32. package/index.js +47 -3
  33. package/node/cli-help.js +1 -0
  34. package/node/cli.js +992 -974
  35. package/orchestration/list-structural-fallback.js +67 -9
  36. package/package.json +1 -1
  37. package/pipeline/diff-engine.js +130 -1
  38. package/pipeline/ingestion-export.js +227 -243
  39. package/pipeline/ingestion-paragraph.js +93 -98
  40. package/pipeline/list-generation.js +415 -419
  41. package/pipeline/pipeline.js +206 -204
  42. package/pipeline/serialization.js +111 -94
  43. package/scripts/export-validation-fixtures.mjs +19 -6
  44. package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
  45. package/scripts/lib/word-coverage-catalogue.mjs +7 -2
  46. package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
  47. package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
  48. package/scripts/word-com-differential.ps1 +443 -335
  49. package/scripts/word-com-visual-suite.ps1 +2 -2
  50. package/scripts/word-revision-semantics-matrix.ps1 +108 -0
  51. package/services/batch-operation-orchestrator.js +26 -0
  52. package/services/capture-engine.js +228 -226
  53. package/services/document-operation-applier.js +215 -0
  54. package/services/document-operation-contract.js +628 -537
  55. package/services/document-operation-mutations.js +789 -46
  56. package/services/error-recovery.js +17 -0
  57. package/services/list-conversion-diagnostics.js +296 -0
  58. package/services/list-format-change.js +586 -0
  59. package/services/list-format-mutation.js +416 -0
  60. package/services/list-format.js +305 -0
  61. package/services/numbering-allocation.js +642 -0
  62. package/services/numbering-helpers.js +354 -524
  63. package/services/operation-batch-compiler.js +612 -436
  64. package/services/operation-heuristics.js +197 -173
  65. package/services/operation-preflight.js +1 -0
  66. package/services/receipt-collector.js +288 -288
  67. package/services/revision-comment-management.js +634 -594
  68. package/services/revision-fidelity-verifier.js +498 -0
  69. package/services/standalone-docx-plumbing.js +627 -631
  70. package/services/standalone-operation-runner.d.ts +606 -427
  71. package/services/table-reconciliation.js +103 -49
@@ -1,288 +1,288 @@
1
- /**
2
- * Internal mutation receipt collector for tracking allocated revision,
3
- * comment, numbering, and relationship IDs per operation.
4
- */
5
-
6
- export class ReceiptCollector {
7
- constructor() {
8
- this.receipts = [];
9
- this.activeReceipt = null;
10
- }
11
-
12
- beginOperation(operationIndex, operationId = null, authorUsed = null) {
13
- this.activeReceipt = {
14
- operationIndex: typeof operationIndex === 'number' ? operationIndex : 1,
15
- ...(operationId ? { operationId: String(operationId) } : {}),
16
- attemptedDisposition: 'applied',
17
- finalDisposition: 'applied',
18
- committed: true,
19
- ...(authorUsed ? { authorUsed: String(authorUsed) } : {}),
20
- revisionItems: [],
21
- commentIds: [],
22
- numberingIds: [],
23
- relationshipIds: [],
24
- affectedTargets: [],
25
- warnings: []
26
- };
27
- }
28
-
29
- recordRevision(id, kind = 'structural', partName = 'word/document.xml') {
30
- if (!this.activeReceipt || id == null) return;
31
- const strId = String(id);
32
- if (!this.activeReceipt.revisionItems.some(item => item.id === strId && item.kind === kind && item.partName === partName)) {
33
- this.activeReceipt.revisionItems.push({
34
- id: strId,
35
- kind,
36
- partName
37
- });
38
- }
39
- }
40
-
41
- recordComment(id, _partName = 'word/comments.xml') {
42
- if (!this.activeReceipt || id == null) return;
43
- const strId = String(id);
44
- if (!this.activeReceipt.commentIds.includes(strId)) {
45
- this.activeReceipt.commentIds.push(strId);
46
- }
47
- }
48
-
49
- recordNumbering(id, _partName = 'word/numbering.xml') {
50
- if (!this.activeReceipt || id == null) return;
51
- const strId = String(id);
52
- if (!this.activeReceipt.numberingIds.includes(strId)) {
53
- this.activeReceipt.numberingIds.push(strId);
54
- }
55
- }
56
-
57
- recordRelationship(id, _partName = 'word/_rels/document.xml.rels') {
58
- if (!this.activeReceipt || id == null) return;
59
- const strId = String(id);
60
- if (!this.activeReceipt.relationshipIds.includes(strId)) {
61
- this.activeReceipt.relationshipIds.push(strId);
62
- }
63
- }
64
-
65
- recordAffectedTarget(target) {
66
- if (!this.activeReceipt || !target) return;
67
- this.activeReceipt.affectedTargets.push(JSON.parse(JSON.stringify(target)));
68
- }
69
-
70
- recordWarning(warning) {
71
- if (!this.activeReceipt || !warning) return;
72
- this.activeReceipt.warnings.push(String(warning));
73
- }
74
-
75
- commitOperation(disposition = 'applied') {
76
- if (!this.activeReceipt) return null;
77
- this.activeReceipt.attemptedDisposition = disposition;
78
- this.activeReceipt.finalDisposition = disposition;
79
- this.activeReceipt.committed = disposition === 'applied';
80
- const committed = JSON.parse(JSON.stringify(this.activeReceipt));
81
- this.receipts.push(committed);
82
- this.activeReceipt = null;
83
- return committed;
84
- }
85
-
86
- abortOperation(disposition = 'refused') {
87
- if (!this.activeReceipt) return null;
88
- this.activeReceipt.attemptedDisposition = disposition;
89
- this.activeReceipt.finalDisposition = disposition;
90
- this.activeReceipt.committed = false;
91
- const aborted = JSON.parse(JSON.stringify(this.activeReceipt));
92
- this.activeReceipt = null;
93
- return aborted;
94
- }
95
-
96
- createSavepoint() {
97
- return {
98
- receipts: JSON.parse(JSON.stringify(this.receipts)),
99
- activeReceipt: this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null
100
- };
101
- }
102
-
103
- restoreSavepoint(savepoint) {
104
- if (!savepoint) return;
105
- this.receipts = Array.isArray(savepoint.receipts)
106
- ? JSON.parse(JSON.stringify(savepoint.receipts))
107
- : [];
108
- this.activeReceipt = savepoint.activeReceipt
109
- ? JSON.parse(JSON.stringify(savepoint.activeReceipt))
110
- : null;
111
- }
112
-
113
- clear() {
114
- this.receipts = [];
115
- this.activeReceipt = null;
116
- }
117
-
118
- markRolledBack() {
119
- for (const receipt of this.receipts) {
120
- if (receipt.attemptedDisposition === 'applied') {
121
- receipt.finalDisposition = 'rolled_back';
122
- receipt.committed = false;
123
- }
124
- }
125
- this.activeReceipt = null;
126
- }
127
-
128
- getReceipts() {
129
- return JSON.parse(JSON.stringify(this.receipts));
130
- }
131
-
132
- getCurrentReceipt() {
133
- return this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null;
134
- }
135
- }
136
-
137
- /**
138
- * Creates an empty receipt structure for unattempted, refused, or no-op operations.
139
- *
140
- * @param {number} operationIndex
141
- * @param {string|null} [operationId]
142
- * @param {string|null} [authorUsed]
143
- * @param {'applied'|'no_change'|'refused'|'not_attempted'} [disposition]
144
- * @returns {Object}
145
- */
146
- export function createEmptyReceipt(operationIndex, operationId = null, authorUsed = null, disposition = 'not_attempted') {
147
- return {
148
- operationIndex: typeof operationIndex === 'number' ? operationIndex : 1,
149
- ...(operationId ? { operationId: String(operationId) } : {}),
150
- attemptedDisposition: disposition,
151
- finalDisposition: disposition,
152
- committed: false,
153
- ...(authorUsed ? { authorUsed: String(authorUsed) } : {}),
154
- revisionItems: [],
155
- commentIds: [],
156
- numberingIds: [],
157
- relationshipIds: [],
158
- affectedTargets: [],
159
- warnings: []
160
- };
161
- }
162
-
163
- /**
164
- * Reconciles committed mutation receipts against the generated/committed output parts.
165
- *
166
- * @param {Object} parts
167
- * @param {string} parts.documentXml - Serialized word/document.xml
168
- * @param {string|null} [parts.commentsXml] - Serialized word/comments.xml
169
- * @param {string|null} [parts.numberingXml] - Serialized word/numbering.xml
170
- * @param {string[]} [parts.numberingXmlParts] - Additional numbering XML parts
171
- * @param {string|null} [parts.relationshipsXml] - Serialized word/_rels/document.xml.rels
172
- * @param {Array<Object>} receipts - Array of MutationReceipt objects
173
- * @returns {{ valid: boolean, error?: { code: string, message: string } }}
174
- */
175
- export function reconcileReceiptsAgainstOutput(parts, receipts) {
176
- if (!Array.isArray(receipts) || receipts.length === 0) {
177
- return { valid: true };
178
- }
179
-
180
- const committedReceipts = receipts.filter(r => r && r.committed === true && r.finalDisposition === 'applied');
181
- if (committedReceipts.length === 0) {
182
- return { valid: true };
183
- }
184
-
185
- const revisionIdSet = new Set();
186
- if (parts?.documentXml && typeof parts.documentXml === 'string') {
187
- const revRegex = /<(?:w:)?(?:ins|del|rPrChange|pPrChange|moveFrom|moveTo)\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
188
- let m;
189
- while ((m = revRegex.exec(parts.documentXml)) !== null) {
190
- revisionIdSet.add(m[1]);
191
- }
192
- }
193
-
194
- const commentIdSet = new Set();
195
- if (parts?.commentsXml && typeof parts.commentsXml === 'string') {
196
- const comRegex = /<(?:w:)?comment\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
197
- let m;
198
- while ((m = comRegex.exec(parts.commentsXml)) !== null) {
199
- commentIdSet.add(m[1]);
200
- }
201
- }
202
-
203
- const numberingIdSet = new Set();
204
- const combinedNumbering = [parts?.numberingXml, ...(parts?.numberingXmlParts || [])].filter(Boolean).join('\n');
205
- if (combinedNumbering) {
206
- const numRegex = /<(?:w:)?num\b[^>]*?\b(?:w:)?numId="([^"]+)"/g;
207
- let m;
208
- while ((m = numRegex.exec(combinedNumbering)) !== null) {
209
- numberingIdSet.add(m[1]);
210
- }
211
- }
212
- if (parts?.documentXml && typeof parts.documentXml === 'string') {
213
- const docNumRegex = /<(?:w:)?numId\b[^>]*?\b(?:w:)?val="([^"]+)"/g;
214
- let m;
215
- while ((m = docNumRegex.exec(parts.documentXml)) !== null) {
216
- numberingIdSet.add(m[1]);
217
- }
218
- }
219
-
220
- const relIdSet = new Set();
221
- if (parts?.relationshipsXml && typeof parts.relationshipsXml === 'string') {
222
- const relRegex = /<Relationship\b[^>]*?\bId="([^"]+)"/g;
223
- let m;
224
- while ((m = relRegex.exec(parts.relationshipsXml)) !== null) {
225
- relIdSet.add(m[1]);
226
- }
227
- }
228
-
229
- for (const receipt of committedReceipts) {
230
- if (Array.isArray(receipt.revisionItems)) {
231
- for (const item of receipt.revisionItems) {
232
- if (item.partName === 'word/document.xml' && !revisionIdSet.has(String(item.id))) {
233
- return {
234
- valid: false,
235
- error: {
236
- code: 'RECEIPT_RECONCILIATION_FAILED',
237
- message: `Committed revision id '${item.id}' (kind: ${item.kind}) was not found in word/document.xml.`
238
- }
239
- };
240
- }
241
- }
242
- }
243
-
244
- if (Array.isArray(receipt.commentIds)) {
245
- for (const id of receipt.commentIds) {
246
- if (!commentIdSet.has(String(id))) {
247
- return {
248
- valid: false,
249
- error: {
250
- code: 'RECEIPT_RECONCILIATION_FAILED',
251
- message: `Committed comment id '${id}' was not found in word/comments.xml.`
252
- }
253
- };
254
- }
255
- }
256
- }
257
-
258
- if (Array.isArray(receipt.numberingIds)) {
259
- for (const id of receipt.numberingIds) {
260
- if (!numberingIdSet.has(String(id))) {
261
- return {
262
- valid: false,
263
- error: {
264
- code: 'RECEIPT_RECONCILIATION_FAILED',
265
- message: `Committed numbering id '${id}' was not found in numbering parts or document.`
266
- }
267
- };
268
- }
269
- }
270
- }
271
-
272
- if (parts?.relationshipsXml && Array.isArray(receipt.relationshipIds)) {
273
- for (const id of receipt.relationshipIds) {
274
- if (!relIdSet.has(String(id))) {
275
- return {
276
- valid: false,
277
- error: {
278
- code: 'RECEIPT_RECONCILIATION_FAILED',
279
- message: `Committed relationship id '${id}' was not found in document.xml.rels.`
280
- }
281
- };
282
- }
283
- }
284
- }
285
- }
286
-
287
- return { valid: true };
288
- }
1
+ /**
2
+ * Internal mutation receipt collector for tracking allocated revision,
3
+ * comment, numbering, and relationship IDs per operation.
4
+ */
5
+
6
+ export class ReceiptCollector {
7
+ constructor() {
8
+ this.receipts = [];
9
+ this.activeReceipt = null;
10
+ }
11
+
12
+ beginOperation(operationIndex, operationId = null, authorUsed = null) {
13
+ this.activeReceipt = {
14
+ operationIndex: typeof operationIndex === 'number' ? operationIndex : 1,
15
+ ...(operationId ? { operationId: String(operationId) } : {}),
16
+ attemptedDisposition: 'applied',
17
+ finalDisposition: 'applied',
18
+ committed: true,
19
+ ...(authorUsed ? { authorUsed: String(authorUsed) } : {}),
20
+ revisionItems: [],
21
+ commentIds: [],
22
+ numberingIds: [],
23
+ relationshipIds: [],
24
+ affectedTargets: [],
25
+ warnings: []
26
+ };
27
+ }
28
+
29
+ recordRevision(id, kind = 'structural', partName = 'word/document.xml') {
30
+ if (!this.activeReceipt || id == null) return;
31
+ const strId = String(id);
32
+ if (!this.activeReceipt.revisionItems.some(item => item.id === strId && item.kind === kind && item.partName === partName)) {
33
+ this.activeReceipt.revisionItems.push({
34
+ id: strId,
35
+ kind,
36
+ partName
37
+ });
38
+ }
39
+ }
40
+
41
+ recordComment(id, _partName = 'word/comments.xml') {
42
+ if (!this.activeReceipt || id == null) return;
43
+ const strId = String(id);
44
+ if (!this.activeReceipt.commentIds.includes(strId)) {
45
+ this.activeReceipt.commentIds.push(strId);
46
+ }
47
+ }
48
+
49
+ recordNumbering(id, _partName = 'word/numbering.xml') {
50
+ if (!this.activeReceipt || id == null) return;
51
+ const strId = String(id);
52
+ if (!this.activeReceipt.numberingIds.includes(strId)) {
53
+ this.activeReceipt.numberingIds.push(strId);
54
+ }
55
+ }
56
+
57
+ recordRelationship(id, _partName = 'word/_rels/document.xml.rels') {
58
+ if (!this.activeReceipt || id == null) return;
59
+ const strId = String(id);
60
+ if (!this.activeReceipt.relationshipIds.includes(strId)) {
61
+ this.activeReceipt.relationshipIds.push(strId);
62
+ }
63
+ }
64
+
65
+ recordAffectedTarget(target) {
66
+ if (!this.activeReceipt || !target) return;
67
+ this.activeReceipt.affectedTargets.push(JSON.parse(JSON.stringify(target)));
68
+ }
69
+
70
+ recordWarning(warning) {
71
+ if (!this.activeReceipt || !warning) return;
72
+ this.activeReceipt.warnings.push(String(warning));
73
+ }
74
+
75
+ commitOperation(disposition = 'applied') {
76
+ if (!this.activeReceipt) return null;
77
+ this.activeReceipt.attemptedDisposition = disposition;
78
+ this.activeReceipt.finalDisposition = disposition;
79
+ this.activeReceipt.committed = disposition === 'applied';
80
+ const committed = JSON.parse(JSON.stringify(this.activeReceipt));
81
+ this.receipts.push(committed);
82
+ this.activeReceipt = null;
83
+ return committed;
84
+ }
85
+
86
+ abortOperation(disposition = 'refused') {
87
+ if (!this.activeReceipt) return null;
88
+ this.activeReceipt.attemptedDisposition = disposition;
89
+ this.activeReceipt.finalDisposition = disposition;
90
+ this.activeReceipt.committed = false;
91
+ const aborted = JSON.parse(JSON.stringify(this.activeReceipt));
92
+ this.activeReceipt = null;
93
+ return aborted;
94
+ }
95
+
96
+ createSavepoint() {
97
+ return {
98
+ receipts: JSON.parse(JSON.stringify(this.receipts)),
99
+ activeReceipt: this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null
100
+ };
101
+ }
102
+
103
+ restoreSavepoint(savepoint) {
104
+ if (!savepoint) return;
105
+ this.receipts = Array.isArray(savepoint.receipts)
106
+ ? JSON.parse(JSON.stringify(savepoint.receipts))
107
+ : [];
108
+ this.activeReceipt = savepoint.activeReceipt
109
+ ? JSON.parse(JSON.stringify(savepoint.activeReceipt))
110
+ : null;
111
+ }
112
+
113
+ clear() {
114
+ this.receipts = [];
115
+ this.activeReceipt = null;
116
+ }
117
+
118
+ markRolledBack() {
119
+ for (const receipt of this.receipts) {
120
+ if (receipt.attemptedDisposition === 'applied') {
121
+ receipt.finalDisposition = 'rolled_back';
122
+ receipt.committed = false;
123
+ }
124
+ }
125
+ this.activeReceipt = null;
126
+ }
127
+
128
+ getReceipts() {
129
+ return JSON.parse(JSON.stringify(this.receipts));
130
+ }
131
+
132
+ getCurrentReceipt() {
133
+ return this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Creates an empty receipt structure for unattempted, refused, or no-op operations.
139
+ *
140
+ * @param {number} operationIndex
141
+ * @param {string|null} [operationId]
142
+ * @param {string|null} [authorUsed]
143
+ * @param {'applied'|'no_change'|'refused'|'not_attempted'} [disposition]
144
+ * @returns {Object}
145
+ */
146
+ export function createEmptyReceipt(operationIndex, operationId = null, authorUsed = null, disposition = 'not_attempted') {
147
+ return {
148
+ operationIndex: typeof operationIndex === 'number' ? operationIndex : 1,
149
+ ...(operationId ? { operationId: String(operationId) } : {}),
150
+ attemptedDisposition: disposition,
151
+ finalDisposition: disposition,
152
+ committed: false,
153
+ ...(authorUsed ? { authorUsed: String(authorUsed) } : {}),
154
+ revisionItems: [],
155
+ commentIds: [],
156
+ numberingIds: [],
157
+ relationshipIds: [],
158
+ affectedTargets: [],
159
+ warnings: []
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Reconciles committed mutation receipts against the generated/committed output parts.
165
+ *
166
+ * @param {Object} parts
167
+ * @param {string} parts.documentXml - Serialized word/document.xml
168
+ * @param {string|null} [parts.commentsXml] - Serialized word/comments.xml
169
+ * @param {string|null} [parts.numberingXml] - Serialized word/numbering.xml
170
+ * @param {string[]} [parts.numberingXmlParts] - Additional numbering XML parts
171
+ * @param {string|null} [parts.relationshipsXml] - Serialized word/_rels/document.xml.rels
172
+ * @param {Array<Object>} receipts - Array of MutationReceipt objects
173
+ * @returns {{ valid: boolean, error?: { code: string, message: string } }}
174
+ */
175
+ export function reconcileReceiptsAgainstOutput(parts, receipts) {
176
+ if (!Array.isArray(receipts) || receipts.length === 0) {
177
+ return { valid: true };
178
+ }
179
+
180
+ const committedReceipts = receipts.filter(r => r && r.committed === true && r.finalDisposition === 'applied');
181
+ if (committedReceipts.length === 0) {
182
+ return { valid: true };
183
+ }
184
+
185
+ const revisionIdSet = new Set();
186
+ if (parts?.documentXml && typeof parts.documentXml === 'string') {
187
+ const revRegex = /<(?:w:)?(?:ins|del|rPrChange|pPrChange|moveFrom|moveTo)\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
188
+ let m;
189
+ while ((m = revRegex.exec(parts.documentXml)) !== null) {
190
+ revisionIdSet.add(m[1]);
191
+ }
192
+ }
193
+
194
+ const commentIdSet = new Set();
195
+ if (parts?.commentsXml && typeof parts.commentsXml === 'string') {
196
+ const comRegex = /<(?:w:)?comment\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
197
+ let m;
198
+ while ((m = comRegex.exec(parts.commentsXml)) !== null) {
199
+ commentIdSet.add(m[1]);
200
+ }
201
+ }
202
+
203
+ const numberingIdSet = new Set();
204
+ const combinedNumbering = [parts?.numberingXml, ...(parts?.numberingXmlParts || [])].filter(Boolean).join('\n');
205
+ if (combinedNumbering) {
206
+ const numRegex = /<(?:w:)?num\b[^>]*?\b(?:w:)?numId="([^"]+)"/g;
207
+ let m;
208
+ while ((m = numRegex.exec(combinedNumbering)) !== null) {
209
+ numberingIdSet.add(m[1]);
210
+ }
211
+ }
212
+ if (parts?.documentXml && typeof parts.documentXml === 'string') {
213
+ const docNumRegex = /<(?:w:)?numId\b[^>]*?\b(?:w:)?val="([^"]+)"/g;
214
+ let m;
215
+ while ((m = docNumRegex.exec(parts.documentXml)) !== null) {
216
+ numberingIdSet.add(m[1]);
217
+ }
218
+ }
219
+
220
+ const relIdSet = new Set();
221
+ if (parts?.relationshipsXml && typeof parts.relationshipsXml === 'string') {
222
+ const relRegex = /<Relationship\b[^>]*?\bId="([^"]+)"/g;
223
+ let m;
224
+ while ((m = relRegex.exec(parts.relationshipsXml)) !== null) {
225
+ relIdSet.add(m[1]);
226
+ }
227
+ }
228
+
229
+ for (const receipt of committedReceipts) {
230
+ if (Array.isArray(receipt.revisionItems)) {
231
+ for (const item of receipt.revisionItems) {
232
+ if (item.partName === 'word/document.xml' && !revisionIdSet.has(String(item.id))) {
233
+ return {
234
+ valid: false,
235
+ error: {
236
+ code: 'RECEIPT_RECONCILIATION_FAILED',
237
+ message: `Committed revision id '${item.id}' (kind: ${item.kind}) was not found in word/document.xml.`
238
+ }
239
+ };
240
+ }
241
+ }
242
+ }
243
+
244
+ if (Array.isArray(receipt.commentIds)) {
245
+ for (const id of receipt.commentIds) {
246
+ if (!commentIdSet.has(String(id))) {
247
+ return {
248
+ valid: false,
249
+ error: {
250
+ code: 'RECEIPT_RECONCILIATION_FAILED',
251
+ message: `Committed comment id '${id}' was not found in word/comments.xml.`
252
+ }
253
+ };
254
+ }
255
+ }
256
+ }
257
+
258
+ if (Array.isArray(receipt.numberingIds)) {
259
+ for (const id of receipt.numberingIds) {
260
+ if (!numberingIdSet.has(String(id))) {
261
+ return {
262
+ valid: false,
263
+ error: {
264
+ code: 'RECEIPT_RECONCILIATION_FAILED',
265
+ message: `Committed numbering id '${id}' was not found in numbering parts or document.`
266
+ }
267
+ };
268
+ }
269
+ }
270
+ }
271
+
272
+ if (parts?.relationshipsXml && Array.isArray(receipt.relationshipIds)) {
273
+ for (const id of receipt.relationshipIds) {
274
+ if (!relIdSet.has(String(id))) {
275
+ return {
276
+ valid: false,
277
+ error: {
278
+ code: 'RECEIPT_RECONCILIATION_FAILED',
279
+ message: `Committed relationship id '${id}' was not found in document.xml.rels.`
280
+ }
281
+ };
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ return { valid: true };
288
+ }