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