@ansonlai/docx-redline-js 0.9.0 → 0.10.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.
- package/ARCHITECTURE.md +679 -669
- package/CHANGELOG.md +32 -1
- package/README.md +29 -6
- package/core/word-xml.js +160 -118
- package/dist/docx-redline-js.esm.js +171 -51
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +93 -93
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +171 -51
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +171 -51
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_KNOWLEDGE_BASE.md +30 -4
- package/docs/SKILL_AUTHORING.md +5 -1
- package/docs/TESTING.md +4 -0
- package/docs/schemas/document-operations.schema.json +1 -1
- package/engine/oxml-engine.js +748 -736
- package/index.d.ts +6 -1
- package/index.js +2 -0
- package/node/cli.js +993 -992
- package/package.json +1 -1
- package/pipeline/markdown-processor.js +5 -2
- package/services/comment-replies.js +1 -1
- package/services/document-operation-applier.js +1 -0
- package/services/document-operation-contract.js +1 -0
- package/services/document-operation-mutations.js +3358 -3346
- package/services/inspection-comparison.js +82 -0
- package/services/operation-preflight.js +42 -33
- package/services/revision-fidelity-verifier.js +5 -19
package/node/cli.js
CHANGED
|
@@ -1,992 +1,993 @@
|
|
|
1
|
-
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { openDocx } from './docx-document.js';
|
|
4
|
-
import { MemoryZip, unzipDocx } from './zip-archive.js';
|
|
5
|
-
import { validateDocxPackage } from '../services/standalone-docx-plumbing.js';
|
|
6
|
-
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
7
|
-
import { configureLogger } from '../adapters/logger.js';
|
|
8
|
-
import { isExistingRevisionsPolicy } from '../services/document-operation-contract.js';
|
|
9
|
-
import { normalizeErrorWithRecovery } from '../services/error-recovery.js';
|
|
10
|
-
import { REVISION_FIDELITY_MODES, resolveRevisionFidelityMode } from '../services/revision-fidelity-verifier.js';
|
|
11
|
-
import { localizedReplacementOffsets } from '../services/localized-replacement-compiler.js';
|
|
12
|
-
import { buildCliHelp, CLI_COMMANDS, commandOptionKeys } from './cli-help.js';
|
|
13
|
-
|
|
14
|
-
const suffixes = { apply: 'redlined', accept: 'accepted', reject: 'rejected', 'delete-comments': 'comments-removed' };
|
|
15
|
-
const CLI_CONTRACT_VERSION = 9;
|
|
16
|
-
const DEFAULT_INSPECTION_LIMIT = 20;
|
|
17
|
-
const INSPECTION_SOFT_BYTE_LIMIT = 48 * 1024;
|
|
18
|
-
const CLI_CAPABILITIES = [
|
|
19
|
-
'atomic-batch-results-on-package-failure',
|
|
20
|
-
'baseline-aware-validation',
|
|
21
|
-
'compact-mutation-results',
|
|
22
|
-
'cross-author-revision-slicing',
|
|
23
|
-
'document-scoped-list-revision-ids',
|
|
24
|
-
'batch-start-source-binding',
|
|
25
|
-
'recovery-envelope-v1',
|
|
26
|
-
'require-complete-exit',
|
|
27
|
-
'operations-stdin',
|
|
28
|
-
'agent-safety-profile-v2',
|
|
29
|
-
'command-help-v1',
|
|
30
|
-
'inspection-context-v1',
|
|
31
|
-
'bounded-inspection-v1',
|
|
32
|
-
'human-document-references-v1',
|
|
33
|
-
'localized-replacements-v1',
|
|
34
|
-
'speculative-search-apply-v1',
|
|
35
|
-
'localized-change-summary-v1',
|
|
36
|
-
'localized-space-equivalence-v1',
|
|
37
|
-
'restore-shortcuts-v1',
|
|
38
|
-
'deduplicated-cli-receipts',
|
|
39
|
-
'compact-cli-json-v1',
|
|
40
|
-
'comment-resolve-operation-v1',
|
|
41
|
-
'delete-comments-by-id-v1',
|
|
42
|
-
'header-footer-parts-v1',
|
|
43
|
-
'format-text-occurrence-v1',
|
|
44
|
-
'revision-fidelity-v1',
|
|
45
|
-
'list-conversion-diagnostics-v1',
|
|
46
|
-
'paragraph-numbering-binding-v1',
|
|
47
|
-
'list-format-v1',
|
|
48
|
-
'list-format-change-v1'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
['
|
|
66
|
-
['
|
|
67
|
-
['
|
|
68
|
-
['
|
|
69
|
-
['
|
|
70
|
-
['
|
|
71
|
-
['no-
|
|
72
|
-
['no-
|
|
73
|
-
['
|
|
74
|
-
['
|
|
75
|
-
['
|
|
76
|
-
['
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
else flags[key] =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
error
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (flags.
|
|
138
|
-
if (flags.
|
|
139
|
-
if (flags.
|
|
140
|
-
if (flags.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (flags.
|
|
150
|
-
if (flags.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (flags.
|
|
160
|
-
if (flags.
|
|
161
|
-
if (
|
|
162
|
-
if (flags.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (flags.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
let
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|| flags?.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|| flags?.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|| flags?.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
: targetText
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
let
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
...(
|
|
374
|
-
...(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
||
|
|
386
|
-
|| flags.
|
|
387
|
-
|| flags.replace ===
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
...(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
...(
|
|
415
|
-
...(flags.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
...(flags.
|
|
423
|
-
...(flags.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
...(
|
|
432
|
-
...(flags.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (flags.
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
...(hiddenRevisionContent && revisionView === '
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
...(
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
: item)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
'
|
|
605
|
-
'
|
|
606
|
-
'
|
|
607
|
-
'
|
|
608
|
-
'
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
...(comment?.
|
|
618
|
-
...(comment?.
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
...
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
?? error.context.currentTarget.
|
|
641
|
-
??
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
if (
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
...
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
return
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
...
|
|
704
|
-
...(result.
|
|
705
|
-
...(result.
|
|
706
|
-
...(
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
const
|
|
718
|
-
const
|
|
719
|
-
current.count
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
&& result.change.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
...
|
|
755
|
-
...(Array.isArray(compact.
|
|
756
|
-
...(compact.
|
|
757
|
-
...(
|
|
758
|
-
...(compact.
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
&& status !== '
|
|
767
|
-
&&
|
|
768
|
-
&&
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
const
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (!
|
|
813
|
-
if (command
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
return
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if (
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
try {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
{
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|| item.change.
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
...(
|
|
967
|
-
...(
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
:
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1
|
+
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { openDocx } from './docx-document.js';
|
|
4
|
+
import { MemoryZip, unzipDocx } from './zip-archive.js';
|
|
5
|
+
import { validateDocxPackage } from '../services/standalone-docx-plumbing.js';
|
|
6
|
+
import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
7
|
+
import { configureLogger } from '../adapters/logger.js';
|
|
8
|
+
import { isExistingRevisionsPolicy } from '../services/document-operation-contract.js';
|
|
9
|
+
import { normalizeErrorWithRecovery } from '../services/error-recovery.js';
|
|
10
|
+
import { REVISION_FIDELITY_MODES, resolveRevisionFidelityMode } from '../services/revision-fidelity-verifier.js';
|
|
11
|
+
import { localizedReplacementOffsets } from '../services/localized-replacement-compiler.js';
|
|
12
|
+
import { buildCliHelp, CLI_COMMANDS, commandOptionKeys } from './cli-help.js';
|
|
13
|
+
|
|
14
|
+
const suffixes = { apply: 'redlined', accept: 'accepted', reject: 'rejected', 'delete-comments': 'comments-removed' };
|
|
15
|
+
const CLI_CONTRACT_VERSION = 9;
|
|
16
|
+
const DEFAULT_INSPECTION_LIMIT = 20;
|
|
17
|
+
const INSPECTION_SOFT_BYTE_LIMIT = 48 * 1024;
|
|
18
|
+
const CLI_CAPABILITIES = [
|
|
19
|
+
'atomic-batch-results-on-package-failure',
|
|
20
|
+
'baseline-aware-validation',
|
|
21
|
+
'compact-mutation-results',
|
|
22
|
+
'cross-author-revision-slicing',
|
|
23
|
+
'document-scoped-list-revision-ids',
|
|
24
|
+
'batch-start-source-binding',
|
|
25
|
+
'recovery-envelope-v1',
|
|
26
|
+
'require-complete-exit',
|
|
27
|
+
'operations-stdin',
|
|
28
|
+
'agent-safety-profile-v2',
|
|
29
|
+
'command-help-v1',
|
|
30
|
+
'inspection-context-v1',
|
|
31
|
+
'bounded-inspection-v1',
|
|
32
|
+
'human-document-references-v1',
|
|
33
|
+
'localized-replacements-v1',
|
|
34
|
+
'speculative-search-apply-v1',
|
|
35
|
+
'localized-change-summary-v1',
|
|
36
|
+
'localized-space-equivalence-v1',
|
|
37
|
+
'restore-shortcuts-v1',
|
|
38
|
+
'deduplicated-cli-receipts',
|
|
39
|
+
'compact-cli-json-v1',
|
|
40
|
+
'comment-resolve-operation-v1',
|
|
41
|
+
'delete-comments-by-id-v1',
|
|
42
|
+
'header-footer-parts-v1',
|
|
43
|
+
'format-text-occurrence-v1',
|
|
44
|
+
'revision-fidelity-v1',
|
|
45
|
+
'list-conversion-diagnostics-v1',
|
|
46
|
+
'paragraph-numbering-binding-v1',
|
|
47
|
+
'list-format-v1',
|
|
48
|
+
'list-format-change-v1',
|
|
49
|
+
'existing-revisions-auto-v1'
|
|
50
|
+
];
|
|
51
|
+
const commandOptions = Object.fromEntries(CLI_COMMANDS.map(command => [command, new Set(commandOptionKeys(command))]));
|
|
52
|
+
|
|
53
|
+
function cliError(code, message, exitCode = 2, details) {
|
|
54
|
+
return {
|
|
55
|
+
status: 'error',
|
|
56
|
+
error: normalizeErrorWithRecovery({
|
|
57
|
+
...(details && typeof details === 'object' ? details : {}),
|
|
58
|
+
code,
|
|
59
|
+
message
|
|
60
|
+
}),
|
|
61
|
+
exitCode
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const optionAliases = new Map([
|
|
65
|
+
['operationsFile', 'operations'],
|
|
66
|
+
['o', 'output'],
|
|
67
|
+
['a', 'author'],
|
|
68
|
+
['i', 'inPlace'],
|
|
69
|
+
['f', 'force'],
|
|
70
|
+
['h', 'help'],
|
|
71
|
+
['no-overwrite', 'noOverwrite'],
|
|
72
|
+
['no-clobber', 'noClobber'],
|
|
73
|
+
['no-redlines', 'noRedlines'],
|
|
74
|
+
['generate-redlines', 'generateRedlines'],
|
|
75
|
+
['require-complete', 'requireComplete'],
|
|
76
|
+
['context', 'around'],
|
|
77
|
+
['C', 'around']
|
|
78
|
+
]);
|
|
79
|
+
function parseArgs(argv) {
|
|
80
|
+
const positionals = []; const flags = {};
|
|
81
|
+
for (let index = 0; index < argv.length; index++) {
|
|
82
|
+
const token = argv[index];
|
|
83
|
+
if (!token.startsWith('-') || token === '-') { positionals.push(token); continue; }
|
|
84
|
+
const prefixLength = token.startsWith('--') ? 2 : 1;
|
|
85
|
+
const [rawKey, inline] = token.slice(prefixLength).split(/=(.*)/s);
|
|
86
|
+
const normalizedKey = rawKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
87
|
+
const key = optionAliases.get(normalizedKey) || normalizedKey;
|
|
88
|
+
if (inline !== undefined) flags[key] = inline;
|
|
89
|
+
else if (argv[index + 1] !== undefined && (argv[index + 1] === '-' || !argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) flags[key] = argv[++index];
|
|
90
|
+
else flags[key] = true;
|
|
91
|
+
}
|
|
92
|
+
return { command: positionals[0], input: positionals[1], extraPositionals: positionals.slice(2), flags };
|
|
93
|
+
}
|
|
94
|
+
function positiveInteger(value) {
|
|
95
|
+
const text = String(value).trim();
|
|
96
|
+
const parsed = /^\d+$/.test(text) ? Number(text) : null;
|
|
97
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
98
|
+
}
|
|
99
|
+
function invalidFilter(message) {
|
|
100
|
+
const error = new Error(message);
|
|
101
|
+
error.code = 'INVALID_FILTER';
|
|
102
|
+
return error;
|
|
103
|
+
}
|
|
104
|
+
function boundedPositiveInteger(value, optionName, maximum = Number.MAX_SAFE_INTEGER) {
|
|
105
|
+
const parsed = positiveInteger(value);
|
|
106
|
+
if (parsed == null || parsed > maximum) {
|
|
107
|
+
const upperBound = maximum < Number.MAX_SAFE_INTEGER ? ` no greater than ${maximum}` : '';
|
|
108
|
+
throw invalidFilter(`${optionName} must be a positive integer${upperBound}.`);
|
|
109
|
+
}
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
function parseIndexes(value) {
|
|
113
|
+
const tokens = String(value).split(',');
|
|
114
|
+
if (!tokens.length || tokens.some(token => positiveInteger(token) == null)) {
|
|
115
|
+
throw invalidFilter('--indexes must be a comma-separated list of positive 1-based integers.');
|
|
116
|
+
}
|
|
117
|
+
return tokens.map(positiveInteger);
|
|
118
|
+
}
|
|
119
|
+
function parseRange(value) {
|
|
120
|
+
const match = String(value).match(/^\s*(\d+)\s*([:,\-])\s*(\d+)\s*$/);
|
|
121
|
+
if (!match) throw invalidFilter('--range must use START:END with positive 1-based integers.');
|
|
122
|
+
const start = positiveInteger(match[1]);
|
|
123
|
+
const end = positiveInteger(match[3]);
|
|
124
|
+
if (start == null || end == null || end < start) {
|
|
125
|
+
throw invalidFilter('--range must have positive 1-based endpoints with END greater than or equal to START.');
|
|
126
|
+
}
|
|
127
|
+
return { start, end };
|
|
128
|
+
}
|
|
129
|
+
function validateCommandOptions(command, flags, extraPositionals) {
|
|
130
|
+
if (extraPositionals.length > 0) return cliError('UNEXPECTED_ARGUMENT', `Unexpected argument: ${extraPositionals[0]}`);
|
|
131
|
+
const allowed = commandOptions[command];
|
|
132
|
+
const unknown = Object.keys(flags).find(option => !allowed.has(option));
|
|
133
|
+
return unknown ? cliError('UNKNOWN_OPTION', `Unknown option for ${command}: --${unknown.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`) : null;
|
|
134
|
+
}
|
|
135
|
+
function inspectionOptions(flags) {
|
|
136
|
+
const options = {};
|
|
137
|
+
if (flags.search) options.search = flags.search;
|
|
138
|
+
if (flags.revised) options.revisedOnly = true;
|
|
139
|
+
if (flags.table) options.inTable = true;
|
|
140
|
+
if (flags.body) options.inTable = false;
|
|
141
|
+
if (flags.nonEmpty) options.skipEmpty = true;
|
|
142
|
+
const selectors = ['index', 'indexes', 'range'].filter(name => flags[name] !== undefined);
|
|
143
|
+
if (selectors.length > 1) throw invalidFilter('Use only one of --index, --indexes, or --range.');
|
|
144
|
+
if (flags.index !== undefined) {
|
|
145
|
+
const index = positiveInteger(flags.index);
|
|
146
|
+
if (index == null) throw invalidFilter('--index must be a positive 1-based integer.');
|
|
147
|
+
options.indexes = [index];
|
|
148
|
+
}
|
|
149
|
+
if (flags.indexes !== undefined) options.indexes = parseIndexes(flags.indexes);
|
|
150
|
+
if (flags.range !== undefined) options.range = parseRange(flags.range);
|
|
151
|
+
if (flags.around !== undefined) {
|
|
152
|
+
const text = String(flags.around).trim();
|
|
153
|
+
if (!/^\d+$/.test(text) || Number(text) > 20) {
|
|
154
|
+
throw invalidFilter('--around must be an integer from 0 through 20.');
|
|
155
|
+
}
|
|
156
|
+
options.around = Number(text);
|
|
157
|
+
if (!flags.search) throw invalidFilter('--around requires --search.');
|
|
158
|
+
}
|
|
159
|
+
if (flags.limit !== undefined) options.limit = boundedPositiveInteger(flags.limit, '--limit', 200);
|
|
160
|
+
if (flags.after !== undefined) options.after = boundedPositiveInteger(flags.after, '--after');
|
|
161
|
+
if (flags.all && flags.limit !== undefined) throw invalidFilter('Use --all or --limit, not both.');
|
|
162
|
+
if (!flags.all && flags.limit === undefined && selectors.length === 0) options.limit = DEFAULT_INSPECTION_LIMIT;
|
|
163
|
+
if (flags.view) {
|
|
164
|
+
if (!['accepted', 'rejected', 'current'].includes(String(flags.view))) {
|
|
165
|
+
throw invalidFilter('--view must be accepted, rejected, or current.');
|
|
166
|
+
}
|
|
167
|
+
options.revisionView = flags.view;
|
|
168
|
+
}
|
|
169
|
+
return options;
|
|
170
|
+
}
|
|
171
|
+
async function readUtf8Stream(stream) {
|
|
172
|
+
if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') {
|
|
173
|
+
throw new Error('No readable stdin stream was provided.');
|
|
174
|
+
}
|
|
175
|
+
const chunks = [];
|
|
176
|
+
for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
177
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseContextRange(value) {
|
|
181
|
+
const match = String(value).match(/^\s*(-?\d+)\s*:\s*(-?\d+)\s*$/);
|
|
182
|
+
if (!match) throw invalidFilter('--context-range must use START:END with signed integer offsets from -20 through 20.');
|
|
183
|
+
const start = Number(match[1]);
|
|
184
|
+
const end = Number(match[2]);
|
|
185
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < -20 || end > 20 || start > end) {
|
|
186
|
+
throw invalidFilter('--context-range requires START <= END and both offsets from -20 through 20.');
|
|
187
|
+
}
|
|
188
|
+
return { start, end, text: `${start}:${end}` };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function speculativeCandidate(paragraph) {
|
|
192
|
+
return {
|
|
193
|
+
excerpt: boundedText(paragraph.exactText, 240),
|
|
194
|
+
index: paragraph.index,
|
|
195
|
+
ref: paragraph.ref,
|
|
196
|
+
paragraphId: paragraph.paragraphId,
|
|
197
|
+
fingerprint: paragraph.fingerprint,
|
|
198
|
+
revisionView: 'accepted',
|
|
199
|
+
inTable: paragraph.inTable,
|
|
200
|
+
humanReference: paragraph.humanReference,
|
|
201
|
+
provision: paragraph.provision,
|
|
202
|
+
nearestHeading: paragraph.nearestHeading
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function speculativeError(code, message, details = {}) {
|
|
207
|
+
return Object.assign(new Error(message), { code, ...details });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resolveSpeculativePatchTarget(document, flags) {
|
|
211
|
+
if (!document) throw speculativeError('INVALID_OPERATION', 'Speculative localized replacement requires an open document.');
|
|
212
|
+
if (flags.contextRange !== undefined && flags.around !== undefined) {
|
|
213
|
+
throw invalidFilter('Use --context-range or --around, not both.');
|
|
214
|
+
}
|
|
215
|
+
const search = flags.search === undefined ? null : String(flags.search).trim();
|
|
216
|
+
if (flags.search !== undefined && (!search || flags.search === true)) {
|
|
217
|
+
throw invalidFilter('--search must be non-empty text.');
|
|
218
|
+
}
|
|
219
|
+
if ((flags.contextRange !== undefined || flags.around !== undefined) && !search) {
|
|
220
|
+
throw invalidFilter('--context-range and --around require --search.');
|
|
221
|
+
}
|
|
222
|
+
let range = { start: 0, end: 0, text: '0:0' };
|
|
223
|
+
if (flags.contextRange !== undefined) range = parseContextRange(flags.contextRange);
|
|
224
|
+
if (flags.around !== undefined) {
|
|
225
|
+
const text = String(flags.around).trim();
|
|
226
|
+
if (!/^\d+$/.test(text) || Number(text) > 20) {
|
|
227
|
+
throw invalidFilter('--around must be an integer from 0 through 20.');
|
|
228
|
+
}
|
|
229
|
+
const around = Number(text);
|
|
230
|
+
range = { start: -around, end: around, text: `${-around}:${around}` };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const inspected = document.inspect({ revisionView: 'accepted' });
|
|
234
|
+
const paragraphs = Array.isArray(inspected.paragraphs) ? inspected.paragraphs : [];
|
|
235
|
+
let anchors = [];
|
|
236
|
+
let eligible = paragraphs;
|
|
237
|
+
if (search) {
|
|
238
|
+
const folded = search.toLowerCase();
|
|
239
|
+
anchors = paragraphs.filter(paragraph => paragraph.exactText.toLowerCase().includes(folded));
|
|
240
|
+
if (anchors.length === 0) {
|
|
241
|
+
throw speculativeError('TARGET_NOT_FOUND', `No paragraph matched search query: "${search}".`, {
|
|
242
|
+
context: { search, range: range.text },
|
|
243
|
+
recovery: { action: 'reinspect', requiresReinspection: true, sameArgumentsSafe: false }
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const eligibleIndexes = new Set();
|
|
247
|
+
for (const anchor of anchors) {
|
|
248
|
+
const anchorPosition = anchor.index - 1;
|
|
249
|
+
for (let offset = range.start; offset <= range.end; offset += 1) {
|
|
250
|
+
const position = anchorPosition + offset;
|
|
251
|
+
if (position >= 0 && position < paragraphs.length) eligibleIndexes.add(position);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
eligible = [...eligibleIndexes].sort((a, b) => a - b).map(position => paragraphs[position]);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const find = String(flags.find);
|
|
258
|
+
const patchCandidates = eligible.map(paragraph => ({
|
|
259
|
+
paragraph,
|
|
260
|
+
match: localizedReplacementOffsets(paragraph.exactText, find)
|
|
261
|
+
})).filter(candidate => candidate.match.offsets.length > 0);
|
|
262
|
+
const exactCandidates = patchCandidates.filter(candidate => candidate.match.matchMode === 'exact');
|
|
263
|
+
const matches = (exactCandidates.length > 0 ? exactCandidates : patchCandidates)
|
|
264
|
+
.map(candidate => candidate.paragraph);
|
|
265
|
+
const context = {
|
|
266
|
+
...(search ? { search, range: range.text, anchorMatchCount: anchors.length } : {})
|
|
267
|
+
};
|
|
268
|
+
if (matches.length === 0) {
|
|
269
|
+
throw speculativeError('PATCH_SOURCE_NOT_FOUND', search
|
|
270
|
+
? `No paragraph in the contextual range contains the patch source: "${find}".`
|
|
271
|
+
: `No paragraph contains the patch source: "${find}".`, {
|
|
272
|
+
context,
|
|
273
|
+
...(anchors.length ? { candidates: anchors.map(speculativeCandidate) } : {}),
|
|
274
|
+
recovery: { action: 'reinspect', requiresReinspection: true, sameArgumentsSafe: false }
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (matches.length > 1) {
|
|
278
|
+
throw speculativeError('AMBIGUOUS_TARGET', `The patch source matched ${matches.length} eligible paragraphs.`, {
|
|
279
|
+
context,
|
|
280
|
+
candidates: matches.map(speculativeCandidate),
|
|
281
|
+
recovery: { action: 'choose_candidate', requiresReinspection: false, sameArgumentsSafe: false }
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const selected = matches[0];
|
|
285
|
+
return {
|
|
286
|
+
target: {
|
|
287
|
+
exactText: selected.exactText,
|
|
288
|
+
...(selected.paragraphId ? { paragraphId: selected.paragraphId } : {}),
|
|
289
|
+
fingerprint: selected.fingerprint,
|
|
290
|
+
revisionView: 'accepted',
|
|
291
|
+
inTable: selected.inTable
|
|
292
|
+
},
|
|
293
|
+
context: {
|
|
294
|
+
...context,
|
|
295
|
+
humanReference: selected.humanReference
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function readOperations(file, flags = {}, stdin = process.stdin, document = null) {
|
|
301
|
+
const inlineRestore = flags?.restore !== undefined;
|
|
302
|
+
const hasInlineTarget = flags?.target !== undefined
|
|
303
|
+
|| flags?.targetId !== undefined
|
|
304
|
+
|| flags?.targetRef !== undefined;
|
|
305
|
+
const hasInlinePatch = flags?.find !== undefined
|
|
306
|
+
|| flags?.replace !== undefined
|
|
307
|
+
|| flags?.occurrence !== undefined;
|
|
308
|
+
const hasSpeculativeScope = flags?.search !== undefined
|
|
309
|
+
|| flags?.contextRange !== undefined
|
|
310
|
+
|| flags?.around !== undefined;
|
|
311
|
+
if (file && (hasInlinePatch || hasSpeculativeScope || inlineRestore)) {
|
|
312
|
+
throw Object.assign(new Error('Use --operations or inline restore/find/replace options, not both.'), {
|
|
313
|
+
code: 'INVALID_OPERATION'
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (hasInlineTarget && hasSpeculativeScope) {
|
|
317
|
+
throw Object.assign(new Error('Use a strong inline target or --search/--context-range, not both.'), {
|
|
318
|
+
code: 'INVALID_OPERATION'
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (!file && (hasInlineTarget || hasInlinePatch || hasSpeculativeScope || inlineRestore)) {
|
|
322
|
+
if (inlineRestore && flags.restore !== true) {
|
|
323
|
+
throw Object.assign(new Error('--restore is a boolean flag and does not take a value.'), {
|
|
324
|
+
code: 'INVALID_OPERATION'
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (hasSpeculativeScope && !hasInlinePatch) {
|
|
328
|
+
throw Object.assign(new Error('--search, --context-range, and --around are only valid with inline --find/--replace.'), {
|
|
329
|
+
code: 'INVALID_OPERATION'
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const targetRef = flags.targetRef === undefined ? null : positiveInteger(flags.targetRef);
|
|
333
|
+
if (flags.targetRef !== undefined && targetRef == null) {
|
|
334
|
+
throw Object.assign(new Error('--target-ref must be a positive 1-based integer.'), {
|
|
335
|
+
code: 'INVALID_OPERATION'
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
const targetId = flags.targetId === undefined ? null : String(flags.targetId).trim();
|
|
339
|
+
if (flags.targetId !== undefined && (!targetId || flags.targetId === true)) {
|
|
340
|
+
throw Object.assign(new Error('--target-id must be a non-empty paragraphId.'), {
|
|
341
|
+
code: 'INVALID_OPERATION'
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const targetText = flags.target === undefined ? null : String(flags.target);
|
|
345
|
+
let target = targetId
|
|
346
|
+
? { ...(targetText == null ? {} : { exactText: targetText }), paragraphId: targetId }
|
|
347
|
+
: targetText;
|
|
348
|
+
if (inlineRestore) {
|
|
349
|
+
if (!targetId) {
|
|
350
|
+
throw Object.assign(new Error('Inline --restore requires --target-id from rejected-view extraction.'), {
|
|
351
|
+
code: 'INVALID_OPERATION'
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
target = { ...target, revisionView: 'rejected' };
|
|
355
|
+
}
|
|
356
|
+
let speculativeContext = null;
|
|
357
|
+
let op;
|
|
358
|
+
if (flags.comment) {
|
|
359
|
+
if (inlineRestore) {
|
|
360
|
+
throw Object.assign(new Error('Use --restore or --comment, not both.'), {
|
|
361
|
+
code: 'INVALID_OPERATION'
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (hasInlinePatch) {
|
|
365
|
+
throw Object.assign(new Error('Inline comments cannot be combined with --find/--replace.'), {
|
|
366
|
+
code: 'INVALID_OPERATION'
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
op = {
|
|
370
|
+
type: 'comment',
|
|
371
|
+
...(target == null ? {} : { target }),
|
|
372
|
+
commentContent: String(flags.comment),
|
|
373
|
+
...(flags.textToComment ? { textToComment: String(flags.textToComment) } : {}),
|
|
374
|
+
...(targetRef == null ? {} : { targetRef }),
|
|
375
|
+
...(flags.author ? { author: String(flags.author) } : {})
|
|
376
|
+
};
|
|
377
|
+
} else if (hasInlinePatch) {
|
|
378
|
+
if (flags.modified !== undefined) {
|
|
379
|
+
throw Object.assign(new Error('Use --modified or --find/--replace, not both.'), {
|
|
380
|
+
code: 'INVALID_OPERATION'
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
if (
|
|
384
|
+
flags.find === undefined
|
|
385
|
+
|| flags.find === true
|
|
386
|
+
|| String(flags.find).length === 0
|
|
387
|
+
|| flags.replace === undefined
|
|
388
|
+
|| flags.replace === true
|
|
389
|
+
) {
|
|
390
|
+
throw Object.assign(new Error('Inline localized replacement requires non-empty --find and string --replace values.'), {
|
|
391
|
+
code: 'INVALID_OPERATION'
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const occurrence = flags.occurrence === undefined ? null : positiveInteger(flags.occurrence);
|
|
395
|
+
if (flags.occurrence !== undefined && occurrence == null) {
|
|
396
|
+
throw Object.assign(new Error('--occurrence must be a positive 1-based integer.'), {
|
|
397
|
+
code: 'INVALID_OPERATION'
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (!hasInlineTarget) {
|
|
401
|
+
const resolution = resolveSpeculativePatchTarget(document, flags);
|
|
402
|
+
target = resolution.target;
|
|
403
|
+
speculativeContext = resolution.context;
|
|
404
|
+
}
|
|
405
|
+
op = {
|
|
406
|
+
type: inlineRestore ? 'restore' : 'redline',
|
|
407
|
+
...(target == null ? {} : { target }),
|
|
408
|
+
...(targetRef == null ? {} : { targetRef }),
|
|
409
|
+
replacements: [{
|
|
410
|
+
find: String(flags.find),
|
|
411
|
+
replace: String(flags.replace),
|
|
412
|
+
...(occurrence == null ? {} : { occurrence })
|
|
413
|
+
}],
|
|
414
|
+
...(speculativeContext ? { _speculativeContext: speculativeContext } : {}),
|
|
415
|
+
...(flags.author ? { author: String(flags.author) } : {}),
|
|
416
|
+
...(flags.existingRevisions ? { existingRevisions: String(flags.existingRevisions) } : {})
|
|
417
|
+
};
|
|
418
|
+
} else if (inlineRestore) {
|
|
419
|
+
op = {
|
|
420
|
+
type: 'restore',
|
|
421
|
+
target,
|
|
422
|
+
...(flags.modified === undefined ? {} : { modified: String(flags.modified) }),
|
|
423
|
+
...(flags.author ? { author: String(flags.author) } : {}),
|
|
424
|
+
...(flags.existingRevisions ? { existingRevisions: String(flags.existingRevisions) } : {})
|
|
425
|
+
};
|
|
426
|
+
} else {
|
|
427
|
+
op = {
|
|
428
|
+
type: 'replace',
|
|
429
|
+
...(target == null ? {} : { target }),
|
|
430
|
+
modified: flags.modified !== undefined ? String(flags.modified) : '',
|
|
431
|
+
...(targetRef == null ? {} : { targetRef }),
|
|
432
|
+
...(flags.author ? { author: String(flags.author) } : {}),
|
|
433
|
+
...(flags.existingRevisions ? { existingRevisions: String(flags.existingRevisions) } : {})
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return { operations: [op], expectedRevision: null };
|
|
437
|
+
}
|
|
438
|
+
if (!file) throw Object.assign(new Error('Use --operations <file.json> or an inline operation.'), { code: 'OPERATIONS_REQUIRED' });
|
|
439
|
+
let parsed;
|
|
440
|
+
try {
|
|
441
|
+
const source = file === '-' ? await readUtf8Stream(stdin) : await readFile(file, 'utf8');
|
|
442
|
+
parsed = JSON.parse(source);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
const location = file === '-' ? ' from stdin' : '';
|
|
445
|
+
throw Object.assign(new Error(`Could not read operations JSON${location}: ${error.message}`), { code: 'INVALID_OPERATIONS_FILE' });
|
|
446
|
+
}
|
|
447
|
+
const operations = Array.isArray(parsed) ? parsed : (parsed?.operations || parsed?.changes);
|
|
448
|
+
if (!Array.isArray(operations)) throw Object.assign(new Error('Operations JSON must be an array or an object with an operations or changes array.'), { code: 'INVALID_OPERATIONS_FILE' });
|
|
449
|
+
return { operations, expectedRevision: parsed?.expectedRevision || null };
|
|
450
|
+
}
|
|
451
|
+
function outputPath(command, input, flags) {
|
|
452
|
+
if (flags.inPlace) return input;
|
|
453
|
+
if (flags.output) return path.resolve(String(flags.output));
|
|
454
|
+
const parsed = path.parse(input); return path.join(parsed.dir, `${parsed.name}.${suffixes[command]}${parsed.ext || '.docx'}`);
|
|
455
|
+
}
|
|
456
|
+
async function writeMutation(command, input, flags, result) {
|
|
457
|
+
if (!result.written) return { status: result.status || 'ok', ...result, outputPath: null };
|
|
458
|
+
const destination = outputPath(command, input, flags);
|
|
459
|
+
const protectExisting = Boolean(flags.noOverwrite || flags.noClobber) && !flags.force;
|
|
460
|
+
if (!flags.inPlace && protectExisting) {
|
|
461
|
+
try {
|
|
462
|
+
await access(destination);
|
|
463
|
+
throw Object.assign(new Error(`Output already exists: ${destination}`), { code: 'OUTPUT_EXISTS' });
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error.code !== 'ENOENT') throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
await writeFile(destination, result.toBuffer());
|
|
469
|
+
return { status: result.status || 'ok', ...result, outputPath: destination };
|
|
470
|
+
}
|
|
471
|
+
function serializable(value) {
|
|
472
|
+
const { buffer: _buffer, toBuffer: _toBuffer, ...rest } = value || {};
|
|
473
|
+
return rest;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function compactExtractParagraph(paragraph) {
|
|
477
|
+
const {
|
|
478
|
+
humanReference, provision, nearestHeading, index, ref, paragraphId,
|
|
479
|
+
fingerprint, revisionView, exactText, inTable, list, selectionRole,
|
|
480
|
+
contextFor, hasRevisions, revisionAuthors, segments
|
|
481
|
+
} = paragraph;
|
|
482
|
+
const hiddenRevisionContent = exactText.length === 0 && Array.isArray(segments)
|
|
483
|
+
&& segments.some(segment => segment.text.length > 0 && (
|
|
484
|
+
revisionView === 'rejected'
|
|
485
|
+
? segment.acceptedStart !== null
|
|
486
|
+
: segment.rejectedStart !== null
|
|
487
|
+
));
|
|
488
|
+
return {
|
|
489
|
+
humanReference,
|
|
490
|
+
provision,
|
|
491
|
+
nearestHeading,
|
|
492
|
+
index,
|
|
493
|
+
ref,
|
|
494
|
+
paragraphId,
|
|
495
|
+
fingerprint,
|
|
496
|
+
revisionView,
|
|
497
|
+
exactText,
|
|
498
|
+
inTable,
|
|
499
|
+
list,
|
|
500
|
+
...(hasRevisions ? {
|
|
501
|
+
hasRevisions: true,
|
|
502
|
+
revisionAuthors,
|
|
503
|
+
...(hiddenRevisionContent && revisionView === 'accepted' ? { deleted: true } : {}),
|
|
504
|
+
...(hiddenRevisionContent && revisionView === 'rejected' ? { inserted: true } : {})
|
|
505
|
+
} : {}),
|
|
506
|
+
...(selectionRole ? { selectionRole } : {}),
|
|
507
|
+
...(Array.isArray(contextFor) ? { contextFor } : {})
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function inspectionResponseBytes(value) {
|
|
512
|
+
return Buffer.byteLength(JSON.stringify(value, null, 2), 'utf8') + 1;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function boundInspectionResponse(value, { bypass = false, broadDetailed = false } = {}) {
|
|
516
|
+
const base = {
|
|
517
|
+
...value,
|
|
518
|
+
machineReferencesAreNotUserLocations: true,
|
|
519
|
+
...(broadDetailed ? {
|
|
520
|
+
notes: [
|
|
521
|
+
'This detailed inspection was bounded. Prefer extract --search or an explicit --range for ordinary targeting.'
|
|
522
|
+
]
|
|
523
|
+
} : {})
|
|
524
|
+
};
|
|
525
|
+
if (bypass) return base;
|
|
526
|
+
|
|
527
|
+
let paragraphs = [...(base.paragraphs || [])];
|
|
528
|
+
const sourceSelection = base.selection || {};
|
|
529
|
+
const totalMatches = Number.isInteger(sourceSelection.totalMatches)
|
|
530
|
+
? sourceSelection.totalMatches
|
|
531
|
+
: paragraphs.filter(item => item.selectionRole !== 'context').length;
|
|
532
|
+
const initialReturnedMatches = Number.isInteger(sourceSelection.returnedMatches)
|
|
533
|
+
? sourceSelection.returnedMatches
|
|
534
|
+
: paragraphs.filter(item => item.selectionRole !== 'context').length;
|
|
535
|
+
let contextTruncated = false;
|
|
536
|
+
|
|
537
|
+
const assemble = (oversizeItem = false) => {
|
|
538
|
+
const directIndexes = new Set(paragraphs
|
|
539
|
+
.filter(item => item.selectionRole !== 'context')
|
|
540
|
+
.map(item => item.index));
|
|
541
|
+
paragraphs = paragraphs
|
|
542
|
+
.map(item => item.selectionRole === 'context'
|
|
543
|
+
? { ...item, contextFor: (item.contextFor || []).filter(index => directIndexes.has(index)) }
|
|
544
|
+
: item)
|
|
545
|
+
.filter(item => item.selectionRole !== 'context' || item.contextFor.length > 0);
|
|
546
|
+
const direct = paragraphs.filter(item => item.selectionRole !== 'context');
|
|
547
|
+
const lastDirect = direct[direct.length - 1] || null;
|
|
548
|
+
const paragraphIndexes = new Set(paragraphs.map(item => item.index));
|
|
549
|
+
const comments = Array.isArray(base.comments)
|
|
550
|
+
? base.comments.filter(comment => paragraphIndexes.has(comment.paragraphIndex))
|
|
551
|
+
: base.comments;
|
|
552
|
+
const budgetTruncated = direct.length < initialReturnedMatches;
|
|
553
|
+
return {
|
|
554
|
+
...base,
|
|
555
|
+
paragraphs,
|
|
556
|
+
...(Array.isArray(base.comments) ? { comments } : {}),
|
|
557
|
+
selection: {
|
|
558
|
+
...sourceSelection,
|
|
559
|
+
totalMatches,
|
|
560
|
+
returnedMatches: direct.length,
|
|
561
|
+
returnedParagraphs: paragraphs.length,
|
|
562
|
+
truncated: sourceSelection.truncated === true || budgetTruncated || contextTruncated,
|
|
563
|
+
nextAfter: sourceSelection.truncated === true || budgetTruncated
|
|
564
|
+
? (lastDirect?.index ?? sourceSelection.nextAfter ?? null)
|
|
565
|
+
: null,
|
|
566
|
+
softByteLimit: INSPECTION_SOFT_BYTE_LIMIT,
|
|
567
|
+
oversizeItem,
|
|
568
|
+
...(contextTruncated ? { contextTruncated: true } : {})
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
let result = assemble();
|
|
574
|
+
while (inspectionResponseBytes(result) > INSPECTION_SOFT_BYTE_LIMIT) {
|
|
575
|
+
const directPositions = paragraphs
|
|
576
|
+
.map((item, position) => item.selectionRole !== 'context' ? position : -1)
|
|
577
|
+
.filter(position => position >= 0);
|
|
578
|
+
if (directPositions.length > 1) {
|
|
579
|
+
paragraphs.splice(directPositions[directPositions.length - 1], 1);
|
|
580
|
+
result = assemble();
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const contextPosition = paragraphs.findLastIndex(item => item.selectionRole === 'context');
|
|
584
|
+
if (contextPosition >= 0) {
|
|
585
|
+
paragraphs.splice(contextPosition, 1);
|
|
586
|
+
contextTruncated = true;
|
|
587
|
+
result = assemble();
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
result = assemble(true);
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function boundedText(value, limit = 512) {
|
|
597
|
+
const text = String(value ?? '');
|
|
598
|
+
return text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function compactError(error) {
|
|
602
|
+
if (!error || typeof error !== 'object') return error;
|
|
603
|
+
const fields = [
|
|
604
|
+
'code', 'stage', 'mismatchOffset', 'expectedExcerpt', 'actualExcerpt',
|
|
605
|
+
'expectedCodePoint', 'actualCodePoint', 'ownerAuthor', 'commentIds',
|
|
606
|
+
'recoveryVersion', 'category', 'field', 'captureRef', 'operationIndexes',
|
|
607
|
+
'consumedByOperation', 'expectedScope', 'actualScope',
|
|
608
|
+
'invariants', 'violations', 'verificationMode', 'baselineView',
|
|
609
|
+
'listMarker', 'requestedList', 'sourceList', 'targetExcerpt'
|
|
610
|
+
];
|
|
611
|
+
const compact = {};
|
|
612
|
+
for (const field of fields) {
|
|
613
|
+
if (error[field] !== undefined) compact[field] = error[field];
|
|
614
|
+
}
|
|
615
|
+
if (Array.isArray(error.comments)) {
|
|
616
|
+
compact.comments = error.comments.map(comment => ({
|
|
617
|
+
...(comment?.id !== undefined ? { id: comment.id } : {}),
|
|
618
|
+
...(comment?.author !== undefined ? { author: boundedText(comment.author, 160) } : {}),
|
|
619
|
+
...(comment?.text !== undefined ? { text: boundedText(comment.text, 512) } : {})
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
if (Array.isArray(error.candidates)) {
|
|
623
|
+
compact.candidates = error.candidates.map(candidate => {
|
|
624
|
+
if (!candidate || typeof candidate !== 'object') return candidate;
|
|
625
|
+
const excerpt = boundedText(candidate.excerpt ?? candidate.exactText ?? candidate.text ?? '', 240);
|
|
626
|
+
return { ...compactResolvedTarget(candidate, { preserveMatchDetails: true }), ...(excerpt ? { excerpt } : {}) };
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
for (const field of ['recovery', 'issueSummary', 'expectedRevision', 'currentRevision']) {
|
|
630
|
+
if (error[field] !== undefined) compact[field] = error[field];
|
|
631
|
+
}
|
|
632
|
+
if (error.context && typeof error.context === 'object') {
|
|
633
|
+
compact.context = {
|
|
634
|
+
...error.context,
|
|
635
|
+
...(error.context.currentTarget ? {
|
|
636
|
+
currentTarget: {
|
|
637
|
+
...compactResolvedTarget(error.context.currentTarget, { preserveMatchDetails: true }),
|
|
638
|
+
excerpt: boundedText(
|
|
639
|
+
error.context.currentTarget.excerpt
|
|
640
|
+
?? error.context.currentTarget.exactText
|
|
641
|
+
?? error.context.currentTarget.text
|
|
642
|
+
?? '',
|
|
643
|
+
240
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
} : {})
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (error.sourceTarget && typeof error.sourceTarget === 'object') {
|
|
650
|
+
compact.sourceTarget = {
|
|
651
|
+
...compactResolvedTarget(error.sourceTarget, { preserveMatchDetails: true }),
|
|
652
|
+
excerpt: boundedText(error.sourceTarget.text ?? error.sourceTarget.exactText ?? '', 240)
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
compact.message = boundedText(error.message || String(error));
|
|
656
|
+
return compact;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function compactTargetTextMatch(match, preserveDetails = false) {
|
|
660
|
+
if (!match || typeof match !== 'object') return match;
|
|
661
|
+
if (preserveDetails) return match;
|
|
662
|
+
if (match.mode === 'exact') return undefined;
|
|
663
|
+
const differenceCount = Number.isInteger(match.differenceCount)
|
|
664
|
+
? match.differenceCount
|
|
665
|
+
: (Array.isArray(match.differences) ? match.differences.length : 0);
|
|
666
|
+
return {
|
|
667
|
+
...(match.mode ? { mode: match.mode } : {}),
|
|
668
|
+
differenceCount
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function compactResolvedTarget(target, { preserveMatchDetails = false } = {}) {
|
|
673
|
+
if (!target || typeof target !== 'object') return target;
|
|
674
|
+
const {
|
|
675
|
+
text: _text,
|
|
676
|
+
exactText: _exactText,
|
|
677
|
+
targetTextMatch,
|
|
678
|
+
...compact
|
|
679
|
+
} = target;
|
|
680
|
+
const compactMatch = compactTargetTextMatch(targetTextMatch, preserveMatchDetails);
|
|
681
|
+
return {
|
|
682
|
+
...compact,
|
|
683
|
+
...(compactMatch ? { targetTextMatch: compactMatch } : {})
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function compactReceipt(receipt) {
|
|
688
|
+
if (!receipt || typeof receipt !== 'object') return receipt;
|
|
689
|
+
return {
|
|
690
|
+
...receipt,
|
|
691
|
+
affectedTargets: Array.isArray(receipt.affectedTargets)
|
|
692
|
+
? receipt.affectedTargets.map(compactResolvedTarget)
|
|
693
|
+
: [],
|
|
694
|
+
warnings: Array.isArray(receipt.warnings) ? receipt.warnings.map(warning => boundedText(warning)) : []
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function compactOperationResult(result) {
|
|
699
|
+
if (!result || typeof result !== 'object') return result;
|
|
700
|
+
const { receipt: _receipt, ...withoutReceipt } = result;
|
|
701
|
+
const preserveMatchDetails = result.status === 'error' || !!result.error;
|
|
702
|
+
return {
|
|
703
|
+
...withoutReceipt,
|
|
704
|
+
...(result.resolvedTarget ? { resolvedTarget: compactResolvedTarget(result.resolvedTarget, { preserveMatchDetails }) } : {}),
|
|
705
|
+
...(result.resolvedAnchor ? { resolvedAnchor: compactResolvedTarget(result.resolvedAnchor, { preserveMatchDetails }) } : {}),
|
|
706
|
+
...(result.error ? { error: compactError(result.error) } : {}),
|
|
707
|
+
...(Array.isArray(result.warnings) ? { warnings: result.warnings.map(warning => boundedText(warning)) } : {})
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function summarizeIssues(issues) {
|
|
712
|
+
const list = Array.isArray(issues) ? issues : [];
|
|
713
|
+
const grouped = new Map();
|
|
714
|
+
for (const issue of list) {
|
|
715
|
+
const code = issue?.code || 'UNKNOWN';
|
|
716
|
+
const source = issue?.source || 'unknown';
|
|
717
|
+
const severity = issue?.severity || 'error';
|
|
718
|
+
const key = `${source}:${severity}:${code}`;
|
|
719
|
+
const current = grouped.get(key) || { source, severity, code, count: 0 };
|
|
720
|
+
current.count++;
|
|
721
|
+
grouped.set(key, current);
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
total: list.length,
|
|
725
|
+
errors: list.filter(issue => issue?.severity === 'error').length,
|
|
726
|
+
warnings: list.filter(issue => issue?.severity === 'warning').length,
|
|
727
|
+
byCode: Array.from(grouped.values()).sort((a, b) => (
|
|
728
|
+
a.source.localeCompare(b.source) || a.code.localeCompare(b.code) || a.severity.localeCompare(b.severity)
|
|
729
|
+
))
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function compactMutationResult(value) {
|
|
734
|
+
const serialized = serializable(value);
|
|
735
|
+
const {
|
|
736
|
+
documentXml: _documentXml,
|
|
737
|
+
oxml: _oxml,
|
|
738
|
+
commentsXml: _commentsXml,
|
|
739
|
+
commentsExtendedXml: _commentsExtendedXml,
|
|
740
|
+
numberingXml: _numberingXml,
|
|
741
|
+
numberingXmlParts: _numberingXmlParts,
|
|
742
|
+
inspection: _inspection,
|
|
743
|
+
issues: _issues,
|
|
744
|
+
...compact
|
|
745
|
+
} = serialized;
|
|
746
|
+
const results = Array.isArray(compact.results) ? compact.results.map(compactOperationResult) : [];
|
|
747
|
+
const status = compact.status || 'ok';
|
|
748
|
+
const localizedChangesVerified = results.every(result => !result?.change || (
|
|
749
|
+
result.change.committed === true
|
|
750
|
+
&& result.change.finalDisposition === 'applied'
|
|
751
|
+
&& result.change.verification?.acceptedViewMatchesCompiledText === true
|
|
752
|
+
));
|
|
753
|
+
return {
|
|
754
|
+
...compact,
|
|
755
|
+
...(Array.isArray(compact.results) ? { results } : {}),
|
|
756
|
+
...(Array.isArray(compact.receipts) ? { receipts: compact.receipts.map(compactReceipt) } : {}),
|
|
757
|
+
...(compact.error ? { error: compactError(compact.error) } : {}),
|
|
758
|
+
...(Array.isArray(compact.warnings) ? { warnings: compact.warnings.map(warning => boundedText(warning)) } : {}),
|
|
759
|
+
...(compact.validation ? {
|
|
760
|
+
validation: {
|
|
761
|
+
originalIssues: summarizeIssues(compact.validation.originalIssues),
|
|
762
|
+
generatedIssues: summarizeIssues(compact.validation.generatedIssues)
|
|
763
|
+
}
|
|
764
|
+
} : {}),
|
|
765
|
+
completion: compact.written === true
|
|
766
|
+
&& status !== 'error'
|
|
767
|
+
&& status !== 'partial'
|
|
768
|
+
&& results.every(result => result?.status !== 'error')
|
|
769
|
+
&& localizedChangesVerified
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function collectValidationIssues(buffer) {
|
|
774
|
+
const entries = unzipDocx(buffer);
|
|
775
|
+
const documentXml = entries.get('word/document.xml')?.toString('utf8') || '';
|
|
776
|
+
const revision = validateRedlineOoxml(documentXml);
|
|
777
|
+
const issues = revision.issues.map(issue => ({ source: 'word/document.xml', ...issue }));
|
|
778
|
+
try {
|
|
779
|
+
await validateDocxPackage(new MemoryZip(entries));
|
|
780
|
+
} catch (error) {
|
|
781
|
+
issues.push({ source: 'package', code: 'PACKAGE_VALIDATION', severity: 'error', message: error.message });
|
|
782
|
+
}
|
|
783
|
+
return issues;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function validationIssueKey(issue) {
|
|
787
|
+
return `${issue.source || ''}:${issue.code}:${issue.message}`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function subtractValidationIssues(issues, baselineIssues) {
|
|
791
|
+
const remainingBaseline = new Map();
|
|
792
|
+
for (const issue of baselineIssues) {
|
|
793
|
+
const key = validationIssueKey(issue);
|
|
794
|
+
remainingBaseline.set(key, (remainingBaseline.get(key) || 0) + 1);
|
|
795
|
+
}
|
|
796
|
+
return issues.filter(issue => {
|
|
797
|
+
const key = validationIssueKey(issue);
|
|
798
|
+
const remaining = remainingBaseline.get(key) || 0;
|
|
799
|
+
if (remaining === 0) return true;
|
|
800
|
+
remainingBaseline.set(key, remaining - 1);
|
|
801
|
+
return false;
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export async function executeCli(argv, io = process) {
|
|
806
|
+
const { command, input: rawInput, extraPositionals, flags } = parseArgs(argv);
|
|
807
|
+
if (command === 'help' || flags.help) {
|
|
808
|
+
const requestedCommand = command === 'help' ? rawInput : command;
|
|
809
|
+
const help = buildCliHelp(requestedCommand || null);
|
|
810
|
+
return help || cliError('UNKNOWN_COMMAND', `Unknown command: ${requestedCommand}`);
|
|
811
|
+
}
|
|
812
|
+
if (!command) return cliError('COMMAND_REQUIRED', 'A command is required.');
|
|
813
|
+
if (!CLI_COMMANDS.includes(command)) return cliError('UNKNOWN_COMMAND', `Unknown command: ${command}`);
|
|
814
|
+
if (command === 'version') {
|
|
815
|
+
if (rawInput || extraPositionals.length > 0) return cliError('UNEXPECTED_ARGUMENT', `Unexpected argument: ${rawInput || extraPositionals[0]}`);
|
|
816
|
+
const optionError = validateCommandOptions(command, flags, []);
|
|
817
|
+
if (optionError) return optionError;
|
|
818
|
+
return {
|
|
819
|
+
status: 'ok',
|
|
820
|
+
command,
|
|
821
|
+
contractVersion: CLI_CONTRACT_VERSION,
|
|
822
|
+
capabilities: CLI_CAPABILITIES
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
if (!rawInput) return cliError('INPUT_REQUIRED', 'An input .docx path is required.');
|
|
826
|
+
const optionError = validateCommandOptions(command, flags, extraPositionals);
|
|
827
|
+
if (optionError) return optionError;
|
|
828
|
+
if (flags.existingRevisions != null && !isExistingRevisionsPolicy(flags.existingRevisions)) {
|
|
829
|
+
return cliError('INVALID_OPERATION', `Unsupported existing-revisions policy: "${String(flags.existingRevisions)}".`);
|
|
830
|
+
}
|
|
831
|
+
const profile = flags.profile == null ? null : String(flags.profile);
|
|
832
|
+
if (profile && profile !== 'agent') {
|
|
833
|
+
return cliError('INVALID_PROFILE', `Unknown execution profile: "${profile}". Supported profiles: agent.`);
|
|
834
|
+
}
|
|
835
|
+
if (flags.revisionFidelity != null && !REVISION_FIDELITY_MODES.includes(String(flags.revisionFidelity))) {
|
|
836
|
+
return cliError('INVALID_OPERATION', `Unsupported revision-fidelity mode: "${String(flags.revisionFidelity)}". Use enforce, warn or off.`);
|
|
837
|
+
}
|
|
838
|
+
if (profile === 'agent' && String(flags.revisionFidelity) === 'off') {
|
|
839
|
+
return cliError('INVALID_PROFILE', 'The agent profile requires Reject All verification; --revision-fidelity off is only for diagnosis outside the agent profile.');
|
|
840
|
+
}
|
|
841
|
+
let inspectOptions = null;
|
|
842
|
+
if (command === 'inspect' || command === 'extract') {
|
|
843
|
+
try { inspectOptions = inspectionOptions(flags); }
|
|
844
|
+
catch (error) { return cliError(error.code || 'INVALID_FILTER', error.message); }
|
|
845
|
+
}
|
|
846
|
+
const input = path.resolve(rawInput);
|
|
847
|
+
let buffer; try { buffer = await readFile(input); } catch (error) { return cliError('INPUT_READ_FAILED', error.message); }
|
|
848
|
+
try {
|
|
849
|
+
const document = openDocx(buffer);
|
|
850
|
+
if (command === 'inspect') {
|
|
851
|
+
const inspected = document.inspect(inspectOptions);
|
|
852
|
+
const broadDetailed = !flags.search && flags.index === undefined
|
|
853
|
+
&& flags.indexes === undefined && flags.range === undefined;
|
|
854
|
+
return boundInspectionResponse(
|
|
855
|
+
{ ...inspected, command, input, indexBase: 1 },
|
|
856
|
+
{ bypass: !!flags.all, broadDetailed }
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
if (command === 'extract') {
|
|
860
|
+
const inspected = document.inspect(inspectOptions);
|
|
861
|
+
return boundInspectionResponse({
|
|
862
|
+
status: inspected.status,
|
|
863
|
+
command,
|
|
864
|
+
input,
|
|
865
|
+
indexBase: 1,
|
|
866
|
+
paragraphs: inspected.paragraphs.map(compactExtractParagraph),
|
|
867
|
+
...(inspected.selection ? { selection: inspected.selection } : {}),
|
|
868
|
+
warnings: inspected.warnings
|
|
869
|
+
}, { bypass: !!flags.all });
|
|
870
|
+
}
|
|
871
|
+
if (command === 'validate') {
|
|
872
|
+
const issues = await collectValidationIssues(buffer);
|
|
873
|
+
if (flags.baseline) {
|
|
874
|
+
const baseline = path.resolve(String(flags.baseline));
|
|
875
|
+
let baselineBuffer;
|
|
876
|
+
try { baselineBuffer = await readFile(baseline); }
|
|
877
|
+
catch (error) { return cliError('BASELINE_READ_FAILED', error.message); }
|
|
878
|
+
const baselineIssues = await collectValidationIssues(baselineBuffer);
|
|
879
|
+
const introducedIssues = subtractValidationIssues(issues, baselineIssues);
|
|
880
|
+
const hasIntroducedErrors = introducedIssues.some(issue => issue.severity === 'error');
|
|
881
|
+
return {
|
|
882
|
+
status: hasIntroducedErrors ? 'error' : 'ok',
|
|
883
|
+
command,
|
|
884
|
+
input,
|
|
885
|
+
baseline,
|
|
886
|
+
valid: !hasIntroducedErrors,
|
|
887
|
+
issues,
|
|
888
|
+
baselineIssues,
|
|
889
|
+
introducedIssues
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const hasErrors = issues.some(issue => issue.severity === 'error');
|
|
893
|
+
return { status: hasErrors ? 'error' : 'ok', command, input, valid: !hasErrors, issues };
|
|
894
|
+
}
|
|
895
|
+
const opsData = command === 'preflight' || command === 'apply'
|
|
896
|
+
? await readOperations(flags.operations, flags, io.stdin || process.stdin, document)
|
|
897
|
+
: null;
|
|
898
|
+
const operations = opsData?.operations || null;
|
|
899
|
+
let expectedRevision = opsData?.expectedRevision || null;
|
|
900
|
+
if (flags.expectedRevision) {
|
|
901
|
+
if (typeof flags.expectedRevision === 'string') {
|
|
902
|
+
try {
|
|
903
|
+
expectedRevision = JSON.parse(flags.expectedRevision);
|
|
904
|
+
} catch {
|
|
905
|
+
expectedRevision = {
|
|
906
|
+
algorithm: 'sha256',
|
|
907
|
+
version: 1,
|
|
908
|
+
scope: 'package',
|
|
909
|
+
value: flags.expectedRevision.trim()
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
} else if (typeof flags.expectedRevision === 'object') {
|
|
913
|
+
expectedRevision = flags.expectedRevision;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (command === 'preflight') return {
|
|
917
|
+
...document.preflight(operations, flags.author, {
|
|
918
|
+
strictTargets: flags.strictTargets !== 'false',
|
|
919
|
+
...(flags.existingRevisions ? { existingRevisions: flags.existingRevisions } : {})
|
|
920
|
+
}),
|
|
921
|
+
command,
|
|
922
|
+
input
|
|
923
|
+
};
|
|
924
|
+
if (command === 'apply') {
|
|
925
|
+
const agentProfile = profile === 'agent';
|
|
926
|
+
const author = flags.author || process.env.DOCX_REDLINE_AUTHOR || 'AI Redliner';
|
|
927
|
+
const generateRedlines = flags.generateRedlines !== undefined
|
|
928
|
+
? (flags.generateRedlines !== 'false' && flags.generateRedlines !== false)
|
|
929
|
+
: (!flags.noRedlines);
|
|
930
|
+
const atomic = flags.atomic !== undefined
|
|
931
|
+
? (flags.atomic === true || flags.atomic === 'true')
|
|
932
|
+
: false;
|
|
933
|
+
const requireComplete = flags.requireComplete !== undefined
|
|
934
|
+
? (flags.requireComplete === true || flags.requireComplete === 'true')
|
|
935
|
+
: agentProfile;
|
|
936
|
+
const effectiveOptions = {
|
|
937
|
+
author,
|
|
938
|
+
atomic,
|
|
939
|
+
strictTargets: true,
|
|
940
|
+
validate: true,
|
|
941
|
+
generateRedlines,
|
|
942
|
+
existingRevisions: flags.existingRevisions || 'merge-same-author',
|
|
943
|
+
verifyRevisionFidelity: resolveRevisionFidelityMode(flags.revisionFidelity == null ? undefined : String(flags.revisionFidelity)),
|
|
944
|
+
requireComplete
|
|
945
|
+
};
|
|
946
|
+
const result = await document.applyOperations(operations, {
|
|
947
|
+
author,
|
|
948
|
+
atomic,
|
|
949
|
+
validate: true,
|
|
950
|
+
strictTargets: true,
|
|
951
|
+
strictListConversion: agentProfile || atomic,
|
|
952
|
+
generateRedlines,
|
|
953
|
+
...(flags.existingRevisions ? { existingRevisions: flags.existingRevisions } : {}),
|
|
954
|
+
verifyRevisionFidelity: effectiveOptions.verifyRevisionFidelity,
|
|
955
|
+
...(expectedRevision ? { expectedRevision } : {})
|
|
956
|
+
});
|
|
957
|
+
const localizedVerificationFailed = (result.results || []).some(item => item?.change && (
|
|
958
|
+
item.change.committed !== true
|
|
959
|
+
|| item.change.finalDisposition !== 'applied'
|
|
960
|
+
|| item.change.verification?.acceptedViewMatchesCompiledText !== true
|
|
961
|
+
));
|
|
962
|
+
const mutationResult = await writeMutation(command, input, flags, result);
|
|
963
|
+
return compactMutationResult({
|
|
964
|
+
command,
|
|
965
|
+
input,
|
|
966
|
+
...serializable(mutationResult),
|
|
967
|
+
...(profile ? { executionProfile: profile, effectiveOptions } : {}),
|
|
968
|
+
...(result.status === 'error'
|
|
969
|
+
? { exitCode: 2 }
|
|
970
|
+
: ((result.status === 'partial' || localizedVerificationFailed) && requireComplete
|
|
971
|
+
? { exitCode: 3 }
|
|
972
|
+
: {}))
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
const commentIds = command === 'delete-comments' && flags.commentId != null && flags.commentId !== true
|
|
976
|
+
? String(flags.commentId).split(',').map(id => id.trim()).filter(Boolean)
|
|
977
|
+
: null;
|
|
978
|
+
const filter = commentIds?.length ? { ids: commentIds } : flags.allAuthors ? { allAuthors: true } : flags.author ? { author: String(flags.author) } : null;
|
|
979
|
+
if (!filter) return cliError('AUTHOR_REQUIRED', command === 'delete-comments' ? 'Use --author <name>, --all-authors, or --comment-id <id[,id]>.' : 'Use --author <name> or --all-authors.');
|
|
980
|
+
const result = command === 'delete-comments' ? await document.deleteComments(filter) : await document.resolveRevisions(command, filter);
|
|
981
|
+
return compactMutationResult({ command, input, ...serializable(await writeMutation(command, input, flags, result)) });
|
|
982
|
+
} catch (error) { return cliError(error.code || 'CLI_FAILED', error.message, 2, error); }
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export async function runCli(argv = process.argv.slice(2), io = process) {
|
|
986
|
+
configureLogger({}, { level: 'silent' });
|
|
987
|
+
const result = await executeCli(argv, io);
|
|
988
|
+
const compactJson = parseArgs(argv).flags.compact === true;
|
|
989
|
+
io.stdout.write(`${JSON.stringify(serializable(result), null, compactJson ? 0 : 2)}\n`);
|
|
990
|
+
return Number.isInteger(result.exitCode) && result.exitCode !== 0
|
|
991
|
+
? result.exitCode
|
|
992
|
+
: (result.status === 'error' ? 1 : 0);
|
|
993
|
+
}
|