@ansonlai/docx-redline-js 0.5.4 → 0.6.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 (55) hide show
  1. package/AGENTS.md +82 -697
  2. package/ARCHITECTURE.md +13 -1
  3. package/CHANGELOG.md +8 -0
  4. package/README.md +177 -45
  5. package/core/paragraph-targeting.js +14 -2
  6. package/dist/docx-redline-js.esm.js +184 -51
  7. package/dist/docx-redline-js.esm.js.map +3 -3
  8. package/dist/docx-redline-js.esm.min.js +77 -77
  9. package/dist/docx-redline-js.esm.min.js.map +4 -4
  10. package/docs/AGENT_FAST_START.md +59 -0
  11. package/docs/AGENT_KNOWLEDGE_BASE.md +878 -0
  12. package/docs/SKILL_AUTHORING.md +126 -0
  13. package/docs/TESTING.md +35 -1
  14. package/docs/schemas/document-operations.schema.json +5 -1
  15. package/docs/validation-reports/2026-09-12-agent-cli-discovery-baseline.md +56 -0
  16. package/docs/validation-reports/2026-09-12-agent-protocol-rollout.md +86 -0
  17. package/docs/validation-reports/2026-09-13-agent-cli-efficiency-rollout.md +86 -0
  18. package/engine/oxml-engine.js +80 -13
  19. package/engine/run-builders.js +5 -15
  20. package/index.d.ts +28 -3
  21. package/node/cli-help.js +209 -0
  22. package/node/cli.js +323 -65
  23. package/node/docx-document.js +120 -69
  24. package/node/index.d.ts +6 -2
  25. package/package.json +15 -3
  26. package/scripts/generate-cross-author-slicing-fixtures.ps1 +25 -25
  27. package/services/batch-operation-orchestrator.js +215 -120
  28. package/services/document-inspection.js +89 -11
  29. package/services/document-operation-applier.js +52 -34
  30. package/services/document-operation-contract.js +10 -6
  31. package/services/document-operation-mutations.js +51 -5
  32. package/services/document-operation-session.js +4 -0
  33. package/services/error-recovery.js +174 -0
  34. package/services/operation-batch-compiler.js +394 -0
  35. package/services/operation-preflight.js +91 -72
  36. package/services/standalone-operation-runner.d.ts +17 -1
  37. package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +0 -1669
  38. package/docs/plans/2026-09-08-cross-author-revision-slicing.md +0 -1399
  39. package/docs/plans/completed/2026-03-01-release-0.1.4-design.md +0 -33
  40. package/docs/plans/completed/2026-03-01-release-0.1.4.md +0 -110
  41. package/docs/plans/completed/2026-05-31-architectural changes.md +0 -593
  42. package/docs/plans/completed/2026-08-02-reliability-improvements.md +0 -1155
  43. package/docs/plans/completed/2026-08-30-reliability-testing-improvements.md +0 -488
  44. package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +0 -669
  45. package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +0 -427
  46. package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +0 -519
  47. package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +0 -69
  48. package/docs/plans/completed/structural-revision-capability-matrix.md +0 -115
  49. package/docs/test-comparison-dashboard.html +0 -4338
  50. package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +0 -22
  51. package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +0 -24
  52. package/docs/validation-reports/2026-08-30-phase-3-coverage.md +0 -73
  53. package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +0 -82
  54. package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +0 -114
  55. package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +0 -79
package/node/cli.js CHANGED
@@ -6,29 +6,34 @@ import { validateDocxPackage } from '../services/standalone-docx-plumbing.js';
6
6
  import { validateRedlineOoxml } from '../core/redline-validation.js';
7
7
  import { configureLogger } from '../adapters/logger.js';
8
8
  import { isExistingRevisionsPolicy } from '../services/document-operation-contract.js';
9
-
10
- const suffixes = { apply: 'redlined', accept: 'accepted', reject: 'rejected', 'delete-comments': 'comments-removed' };
11
- const CLI_CONTRACT_VERSION = 3;
9
+ import { normalizeErrorWithRecovery } from '../services/error-recovery.js';
10
+ import { buildCliHelp, CLI_COMMANDS, commandOptionKeys } from './cli-help.js';
11
+
12
+ const suffixes = { apply: 'redlined', accept: 'accepted', reject: 'rejected', 'delete-comments': 'comments-removed' };
13
+ const CLI_CONTRACT_VERSION = 7;
14
+ const DEFAULT_INSPECTION_LIMIT = 20;
15
+ const INSPECTION_SOFT_BYTE_LIMIT = 48 * 1024;
12
16
  const CLI_CAPABILITIES = [
13
17
  'atomic-batch-results-on-package-failure',
14
18
  'baseline-aware-validation',
15
19
  'compact-mutation-results',
16
20
  'cross-author-revision-slicing',
17
- 'document-scoped-list-revision-ids'
21
+ 'document-scoped-list-revision-ids',
22
+ 'batch-start-source-binding',
23
+ 'recovery-envelope-v1',
24
+ 'require-complete-exit',
25
+ 'operations-stdin',
26
+ 'agent-safety-profile-v2',
27
+ 'command-help-v1',
28
+ 'inspection-context-v1',
29
+ 'bounded-inspection-v1',
30
+ 'human-document-references-v1',
31
+ 'deduplicated-cli-receipts',
32
+ 'compact-cli-json-v1'
18
33
  ];
19
- const commandOptions = {
20
- version: new Set(['help']),
21
- inspect: new Set(['help', 'search', 'revised', 'table', 'body', 'nonEmpty', 'index', 'indexes', 'range', 'view']),
22
- extract: new Set(['help', 'search', 'revised', 'table', 'body', 'nonEmpty', 'index', 'indexes', 'range', 'view']),
23
- preflight: new Set(['help', 'operations', 'author', 'strictTargets', 'target', 'modified', 'comment', 'textToComment', 'targetRef', 'existingRevisions']),
24
- apply: new Set(['help', 'operations', 'author', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber', 'expectedRevision', 'target', 'modified', 'comment', 'textToComment', 'targetRef', 'existingRevisions', 'atomic', 'generateRedlines', 'noRedlines']),
25
- accept: new Set(['help', 'author', 'allAuthors', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber']),
26
- reject: new Set(['help', 'author', 'allAuthors', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber']),
27
- 'delete-comments': new Set(['help', 'author', 'allAuthors', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber']),
28
- validate: new Set(['help', 'baseline'])
29
- };
34
+ const commandOptions = Object.fromEntries(CLI_COMMANDS.map(command => [command, new Set(commandOptionKeys(command))]));
30
35
 
31
- function cliError(code, message, exitCode = 2, details) { return { status: 'error', error: { code, message, ...(details ? { details } : {}) }, exitCode }; }
36
+ function cliError(code, message, exitCode = 2, details) { return { status: 'error', error: normalizeErrorWithRecovery({ code, message, ...(details ? { details } : {}) }), exitCode }; }
32
37
  const optionAliases = new Map([
33
38
  ['operationsFile', 'operations'],
34
39
  ['o', 'output'],
@@ -39,8 +44,11 @@ const optionAliases = new Map([
39
44
  ['no-overwrite', 'noOverwrite'],
40
45
  ['no-clobber', 'noClobber'],
41
46
  ['no-redlines', 'noRedlines'],
42
- ['generate-redlines', 'generateRedlines']
43
- ]);
47
+ ['generate-redlines', 'generateRedlines'],
48
+ ['require-complete', 'requireComplete'],
49
+ ['context', 'around'],
50
+ ['C', 'around']
51
+ ]);
44
52
  function parseArgs(argv) {
45
53
  const positionals = []; const flags = {};
46
54
  for (let index = 0; index < argv.length; index++) {
@@ -51,7 +59,7 @@ function parseArgs(argv) {
51
59
  const normalizedKey = rawKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
52
60
  const key = optionAliases.get(normalizedKey) || normalizedKey;
53
61
  if (inline !== undefined) flags[key] = inline;
54
- else if (argv[index + 1] && (!argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) flags[key] = argv[++index];
62
+ else if (argv[index + 1] && (argv[index + 1] === '-' || !argv[index + 1].startsWith('-') || /^-\d/.test(argv[index + 1]))) flags[key] = argv[++index];
55
63
  else flags[key] = true;
56
64
  }
57
65
  return { command: positionals[0], input: positionals[1], extraPositionals: positionals.slice(2), flags };
@@ -61,11 +69,19 @@ function positiveInteger(value) {
61
69
  const parsed = /^\d+$/.test(text) ? Number(text) : null;
62
70
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
63
71
  }
64
- function invalidFilter(message) {
65
- const error = new Error(message);
66
- error.code = 'INVALID_FILTER';
67
- return error;
68
- }
72
+ function invalidFilter(message) {
73
+ const error = new Error(message);
74
+ error.code = 'INVALID_FILTER';
75
+ return error;
76
+ }
77
+ function boundedPositiveInteger(value, optionName, maximum = Number.MAX_SAFE_INTEGER) {
78
+ const parsed = positiveInteger(value);
79
+ if (parsed == null || parsed > maximum) {
80
+ const upperBound = maximum < Number.MAX_SAFE_INTEGER ? ` no greater than ${maximum}` : '';
81
+ throw invalidFilter(`${optionName} must be a positive integer${upperBound}.`);
82
+ }
83
+ return parsed;
84
+ }
69
85
  function parseIndexes(value) {
70
86
  const tokens = String(value).split(',');
71
87
  if (!tokens.length || tokens.some(token => positiveInteger(token) == null)) {
@@ -102,10 +118,22 @@ function inspectionOptions(flags) {
102
118
  const index = positiveInteger(flags.index);
103
119
  if (index == null) throw invalidFilter('--index must be a positive 1-based integer.');
104
120
  options.indexes = [index];
105
- }
106
- if (flags.indexes !== undefined) options.indexes = parseIndexes(flags.indexes);
107
- if (flags.range !== undefined) options.range = parseRange(flags.range);
108
- if (flags.view) {
121
+ }
122
+ if (flags.indexes !== undefined) options.indexes = parseIndexes(flags.indexes);
123
+ if (flags.range !== undefined) options.range = parseRange(flags.range);
124
+ if (flags.around !== undefined) {
125
+ const text = String(flags.around).trim();
126
+ if (!/^\d+$/.test(text) || Number(text) > 20) {
127
+ throw invalidFilter('--around must be an integer from 0 through 20.');
128
+ }
129
+ options.around = Number(text);
130
+ if (!flags.search) throw invalidFilter('--around requires --search.');
131
+ }
132
+ if (flags.limit !== undefined) options.limit = boundedPositiveInteger(flags.limit, '--limit', 200);
133
+ if (flags.after !== undefined) options.after = boundedPositiveInteger(flags.after, '--after');
134
+ if (flags.all && flags.limit !== undefined) throw invalidFilter('Use --all or --limit, not both.');
135
+ if (!flags.all && flags.limit === undefined && selectors.length === 0) options.limit = DEFAULT_INSPECTION_LIMIT;
136
+ if (flags.view) {
109
137
  if (!['accepted', 'rejected', 'current'].includes(String(flags.view))) {
110
138
  throw invalidFilter('--view must be accepted, rejected, or current.');
111
139
  }
@@ -113,7 +141,16 @@ function inspectionOptions(flags) {
113
141
  }
114
142
  return options;
115
143
  }
116
- async function readOperations(file, flags = {}) {
144
+ async function readUtf8Stream(stream) {
145
+ if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') {
146
+ throw new Error('No readable stdin stream was provided.');
147
+ }
148
+ const chunks = [];
149
+ for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
150
+ return Buffer.concat(chunks).toString('utf8');
151
+ }
152
+
153
+ async function readOperations(file, flags = {}, stdin = process.stdin) {
117
154
  if (!file && flags?.target) {
118
155
  let op;
119
156
  if (flags.comment) {
@@ -138,7 +175,14 @@ async function readOperations(file, flags = {}) {
138
175
  return { operations: [op], expectedRevision: null };
139
176
  }
140
177
  if (!file) throw Object.assign(new Error('Use --operations <file.json> or --target <text>.'), { code: 'OPERATIONS_REQUIRED' });
141
- let parsed; try { parsed = JSON.parse(await readFile(file, 'utf8')); } catch (error) { throw Object.assign(new Error(`Could not read operations JSON: ${error.message}`), { code: 'INVALID_OPERATIONS_FILE' }); }
178
+ let parsed;
179
+ try {
180
+ const source = file === '-' ? await readUtf8Stream(stdin) : await readFile(file, 'utf8');
181
+ parsed = JSON.parse(source);
182
+ } catch (error) {
183
+ const location = file === '-' ? ' from stdin' : '';
184
+ throw Object.assign(new Error(`Could not read operations JSON${location}: ${error.message}`), { code: 'INVALID_OPERATIONS_FILE' });
185
+ }
142
186
  const operations = Array.isArray(parsed) ? parsed : (parsed?.operations || parsed?.changes);
143
187
  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' });
144
188
  return { operations, expectedRevision: parsed?.expectedRevision || null };
@@ -168,6 +212,114 @@ function serializable(value) {
168
212
  return rest;
169
213
  }
170
214
 
215
+ function compactExtractParagraph(paragraph) {
216
+ const {
217
+ humanReference, provision, nearestHeading, index, ref, paragraphId,
218
+ fingerprint, revisionView, exactText, inTable, list, selectionRole,
219
+ contextFor
220
+ } = paragraph;
221
+ return {
222
+ humanReference,
223
+ provision,
224
+ nearestHeading,
225
+ index,
226
+ ref,
227
+ paragraphId,
228
+ fingerprint,
229
+ revisionView,
230
+ exactText,
231
+ inTable,
232
+ list,
233
+ ...(selectionRole ? { selectionRole } : {}),
234
+ ...(Array.isArray(contextFor) ? { contextFor } : {})
235
+ };
236
+ }
237
+
238
+ function inspectionResponseBytes(value) {
239
+ return Buffer.byteLength(JSON.stringify(value, null, 2), 'utf8') + 1;
240
+ }
241
+
242
+ function boundInspectionResponse(value, { bypass = false, broadDetailed = false } = {}) {
243
+ const base = {
244
+ ...value,
245
+ machineReferencesAreNotUserLocations: true,
246
+ ...(broadDetailed ? {
247
+ notes: [
248
+ 'This detailed inspection was bounded. Prefer extract --search or an explicit --range for ordinary targeting.'
249
+ ]
250
+ } : {})
251
+ };
252
+ if (bypass) return base;
253
+
254
+ let paragraphs = [...(base.paragraphs || [])];
255
+ const sourceSelection = base.selection || {};
256
+ const totalMatches = Number.isInteger(sourceSelection.totalMatches)
257
+ ? sourceSelection.totalMatches
258
+ : paragraphs.filter(item => item.selectionRole !== 'context').length;
259
+ const initialReturnedMatches = Number.isInteger(sourceSelection.returnedMatches)
260
+ ? sourceSelection.returnedMatches
261
+ : paragraphs.filter(item => item.selectionRole !== 'context').length;
262
+ let contextTruncated = false;
263
+
264
+ const assemble = (oversizeItem = false) => {
265
+ const directIndexes = new Set(paragraphs
266
+ .filter(item => item.selectionRole !== 'context')
267
+ .map(item => item.index));
268
+ paragraphs = paragraphs
269
+ .map(item => item.selectionRole === 'context'
270
+ ? { ...item, contextFor: (item.contextFor || []).filter(index => directIndexes.has(index)) }
271
+ : item)
272
+ .filter(item => item.selectionRole !== 'context' || item.contextFor.length > 0);
273
+ const direct = paragraphs.filter(item => item.selectionRole !== 'context');
274
+ const lastDirect = direct[direct.length - 1] || null;
275
+ const paragraphIndexes = new Set(paragraphs.map(item => item.index));
276
+ const comments = Array.isArray(base.comments)
277
+ ? base.comments.filter(comment => paragraphIndexes.has(comment.paragraphIndex))
278
+ : base.comments;
279
+ const budgetTruncated = direct.length < initialReturnedMatches;
280
+ return {
281
+ ...base,
282
+ paragraphs,
283
+ ...(Array.isArray(base.comments) ? { comments } : {}),
284
+ selection: {
285
+ ...sourceSelection,
286
+ totalMatches,
287
+ returnedMatches: direct.length,
288
+ returnedParagraphs: paragraphs.length,
289
+ truncated: sourceSelection.truncated === true || budgetTruncated || contextTruncated,
290
+ nextAfter: sourceSelection.truncated === true || budgetTruncated
291
+ ? (lastDirect?.index ?? sourceSelection.nextAfter ?? null)
292
+ : null,
293
+ softByteLimit: INSPECTION_SOFT_BYTE_LIMIT,
294
+ oversizeItem,
295
+ ...(contextTruncated ? { contextTruncated: true } : {})
296
+ }
297
+ };
298
+ };
299
+
300
+ let result = assemble();
301
+ while (inspectionResponseBytes(result) > INSPECTION_SOFT_BYTE_LIMIT) {
302
+ const directPositions = paragraphs
303
+ .map((item, position) => item.selectionRole !== 'context' ? position : -1)
304
+ .filter(position => position >= 0);
305
+ if (directPositions.length > 1) {
306
+ paragraphs.splice(directPositions[directPositions.length - 1], 1);
307
+ result = assemble();
308
+ continue;
309
+ }
310
+ const contextPosition = paragraphs.findLastIndex(item => item.selectionRole === 'context');
311
+ if (contextPosition >= 0) {
312
+ paragraphs.splice(contextPosition, 1);
313
+ contextTruncated = true;
314
+ result = assemble();
315
+ continue;
316
+ }
317
+ result = assemble(true);
318
+ break;
319
+ }
320
+ return result;
321
+ }
322
+
171
323
  function boundedText(value, limit = 512) {
172
324
  const text = String(value ?? '');
173
325
  return text.length > limit ? `${text.slice(0, limit)}…` : text;
@@ -177,7 +329,9 @@ function compactError(error) {
177
329
  if (!error || typeof error !== 'object') return error;
178
330
  const fields = [
179
331
  'code', 'stage', 'mismatchOffset', 'expectedExcerpt', 'actualExcerpt',
180
- 'expectedCodePoint', 'actualCodePoint', 'ownerAuthor', 'commentIds'
332
+ 'expectedCodePoint', 'actualCodePoint', 'ownerAuthor', 'commentIds',
333
+ 'recoveryVersion', 'category', 'field', 'captureRef', 'operationIndexes',
334
+ 'consumedByOperation', 'expectedScope', 'actualScope'
181
335
  ];
182
336
  const compact = {};
183
337
  for (const field of fields) {
@@ -191,16 +345,68 @@ function compactError(error) {
191
345
  }));
192
346
  }
193
347
  if (Array.isArray(error.candidates)) {
194
- compact.candidates = error.candidates.map(compactResolvedTarget);
348
+ compact.candidates = error.candidates.map(candidate => {
349
+ if (!candidate || typeof candidate !== 'object') return candidate;
350
+ const excerpt = boundedText(candidate.excerpt ?? candidate.exactText ?? candidate.text ?? '', 240);
351
+ return { ...compactResolvedTarget(candidate, { preserveMatchDetails: true }), ...(excerpt ? { excerpt } : {}) };
352
+ });
353
+ }
354
+ for (const field of ['recovery', 'issueSummary', 'expectedRevision', 'currentRevision']) {
355
+ if (error[field] !== undefined) compact[field] = error[field];
356
+ }
357
+ if (error.context && typeof error.context === 'object') {
358
+ compact.context = {
359
+ ...error.context,
360
+ ...(error.context.currentTarget ? {
361
+ currentTarget: {
362
+ ...compactResolvedTarget(error.context.currentTarget, { preserveMatchDetails: true }),
363
+ excerpt: boundedText(
364
+ error.context.currentTarget.excerpt
365
+ ?? error.context.currentTarget.exactText
366
+ ?? error.context.currentTarget.text
367
+ ?? '',
368
+ 240
369
+ )
370
+ }
371
+ } : {})
372
+ };
373
+ }
374
+ if (error.sourceTarget && typeof error.sourceTarget === 'object') {
375
+ compact.sourceTarget = {
376
+ ...compactResolvedTarget(error.sourceTarget, { preserveMatchDetails: true }),
377
+ excerpt: boundedText(error.sourceTarget.text ?? error.sourceTarget.exactText ?? '', 240)
378
+ };
195
379
  }
196
380
  compact.message = boundedText(error.message || String(error));
197
381
  return compact;
198
382
  }
199
383
 
200
- function compactResolvedTarget(target) {
384
+ function compactTargetTextMatch(match, preserveDetails = false) {
385
+ if (!match || typeof match !== 'object') return match;
386
+ if (preserveDetails) return match;
387
+ if (match.mode === 'exact') return undefined;
388
+ const differenceCount = Number.isInteger(match.differenceCount)
389
+ ? match.differenceCount
390
+ : (Array.isArray(match.differences) ? match.differences.length : 0);
391
+ return {
392
+ ...(match.mode ? { mode: match.mode } : {}),
393
+ differenceCount
394
+ };
395
+ }
396
+
397
+ function compactResolvedTarget(target, { preserveMatchDetails = false } = {}) {
201
398
  if (!target || typeof target !== 'object') return target;
202
- const { text: _text, exactText: _exactText, ...compact } = target;
203
- return compact;
399
+ const {
400
+ text: _text,
401
+ exactText: _exactText,
402
+ targetTextMatch,
403
+ ...compact
404
+ } = target;
405
+ const compactMatch = compactTargetTextMatch(targetTextMatch, preserveMatchDetails);
406
+ return {
407
+ ...compact,
408
+ ...(compactMatch ? { targetTextMatch: compactMatch } : {})
409
+ };
204
410
  }
205
411
 
206
412
  function compactReceipt(receipt) {
@@ -216,12 +422,13 @@ function compactReceipt(receipt) {
216
422
 
217
423
  function compactOperationResult(result) {
218
424
  if (!result || typeof result !== 'object') return result;
425
+ const { receipt: _receipt, ...withoutReceipt } = result;
426
+ const preserveMatchDetails = result.status === 'error' || !!result.error;
219
427
  return {
220
- ...result,
221
- ...(result.resolvedTarget ? { resolvedTarget: compactResolvedTarget(result.resolvedTarget) } : {}),
222
- ...(result.resolvedAnchor ? { resolvedAnchor: compactResolvedTarget(result.resolvedAnchor) } : {}),
428
+ ...withoutReceipt,
429
+ ...(result.resolvedTarget ? { resolvedTarget: compactResolvedTarget(result.resolvedTarget, { preserveMatchDetails }) } : {}),
430
+ ...(result.resolvedAnchor ? { resolvedAnchor: compactResolvedTarget(result.resolvedAnchor, { preserveMatchDetails }) } : {}),
223
431
  ...(result.error ? { error: compactError(result.error) } : {}),
224
- ...(result.receipt ? { receipt: compactReceipt(result.receipt) } : {}),
225
432
  ...(Array.isArray(result.warnings) ? { warnings: result.warnings.map(warning => boundedText(warning)) } : {})
226
433
  };
227
434
  }
@@ -314,11 +521,15 @@ function subtractValidationIssues(issues, baselineIssues) {
314
521
  });
315
522
  }
316
523
 
317
- export async function executeCli(argv) {
318
- const { command, input: rawInput, extraPositionals, flags } = parseArgs(argv);
319
- if (command === 'help' || flags.help) return { status: 'ok', command: 'help', usage: 'docx-redline <version|inspect|extract|preflight|apply|accept|reject|delete-comments|validate> [file.docx] [options]' };
320
- if (!command) return cliError('COMMAND_REQUIRED', 'A command is required.');
321
- if (!['version','inspect','extract','preflight','apply','accept','reject','delete-comments','validate'].includes(command)) return cliError('UNKNOWN_COMMAND', `Unknown command: ${command}`);
524
+ export async function executeCli(argv, io = process) {
525
+ const { command, input: rawInput, extraPositionals, flags } = parseArgs(argv);
526
+ if (command === 'help' || flags.help) {
527
+ const requestedCommand = command === 'help' ? rawInput : command;
528
+ const help = buildCliHelp(requestedCommand || null);
529
+ return help || cliError('UNKNOWN_COMMAND', `Unknown command: ${requestedCommand}`);
530
+ }
531
+ if (!command) return cliError('COMMAND_REQUIRED', 'A command is required.');
532
+ if (!CLI_COMMANDS.includes(command)) return cliError('UNKNOWN_COMMAND', `Unknown command: ${command}`);
322
533
  if (command === 'version') {
323
534
  if (rawInput || extraPositionals.length > 0) return cliError('UNEXPECTED_ARGUMENT', `Unexpected argument: ${rawInput || extraPositionals[0]}`);
324
535
  const optionError = validateCommandOptions(command, flags, []);
@@ -336,6 +547,10 @@ export async function executeCli(argv) {
336
547
  if (flags.existingRevisions != null && !isExistingRevisionsPolicy(flags.existingRevisions)) {
337
548
  return cliError('INVALID_OPERATION', `Unsupported existing-revisions policy: "${String(flags.existingRevisions)}".`);
338
549
  }
550
+ const profile = flags.profile == null ? null : String(flags.profile);
551
+ if (profile && profile !== 'agent') {
552
+ return cliError('INVALID_PROFILE', `Unknown execution profile: "${profile}". Supported profiles: agent.`);
553
+ }
339
554
  let inspectOptions = null;
340
555
  if (command === 'inspect' || command === 'extract') {
341
556
  try { inspectOptions = inspectionOptions(flags); }
@@ -345,11 +560,27 @@ export async function executeCli(argv) {
345
560
  let buffer; try { buffer = await readFile(input); } catch (error) { return cliError('INPUT_READ_FAILED', error.message); }
346
561
  try {
347
562
  const document = openDocx(buffer);
348
- if (command === 'inspect') return { ...document.inspect(inspectOptions), command, input, indexBase: 1 };
349
- if (command === 'extract') {
350
- const inspected = document.inspect(inspectOptions);
351
- return { status: inspected.status, command, input, indexBase: 1, paragraphs: inspected.paragraphs.map(({ index, ref, paragraphId, fingerprint, exactText, inTable, list, nearestHeading }) => ({ index, ref, paragraphId, fingerprint, exactText, inTable, list, nearestHeading })), warnings: inspected.warnings };
352
- }
563
+ if (command === 'inspect') {
564
+ const inspected = document.inspect(inspectOptions);
565
+ const broadDetailed = !flags.search && flags.index === undefined
566
+ && flags.indexes === undefined && flags.range === undefined;
567
+ return boundInspectionResponse(
568
+ { ...inspected, command, input, indexBase: 1 },
569
+ { bypass: !!flags.all, broadDetailed }
570
+ );
571
+ }
572
+ if (command === 'extract') {
573
+ const inspected = document.inspect(inspectOptions);
574
+ return boundInspectionResponse({
575
+ status: inspected.status,
576
+ command,
577
+ input,
578
+ indexBase: 1,
579
+ paragraphs: inspected.paragraphs.map(compactExtractParagraph),
580
+ ...(inspected.selection ? { selection: inspected.selection } : {}),
581
+ warnings: inspected.warnings
582
+ }, { bypass: !!flags.all });
583
+ }
353
584
  if (command === 'validate') {
354
585
  const issues = await collectValidationIssues(buffer);
355
586
  if (flags.baseline) {
@@ -374,7 +605,9 @@ export async function executeCli(argv) {
374
605
  const hasErrors = issues.some(issue => issue.severity === 'error');
375
606
  return { status: hasErrors ? 'error' : 'ok', command, input, valid: !hasErrors, issues };
376
607
  }
377
- const opsData = command === 'preflight' || command === 'apply' ? await readOperations(flags.operations, flags) : null;
608
+ const opsData = command === 'preflight' || command === 'apply'
609
+ ? await readOperations(flags.operations, flags, io.stdin || process.stdin)
610
+ : null;
378
611
  const operations = opsData?.operations || null;
379
612
  let expectedRevision = opsData?.expectedRevision || null;
380
613
  if (flags.expectedRevision) {
@@ -400,15 +633,31 @@ export async function executeCli(argv) {
400
633
  }),
401
634
  command,
402
635
  input
403
- };
404
- if (command === 'apply') {
405
- const author = flags.author || process.env.DOCX_REDLINE_AUTHOR || 'AI Redliner';
406
- const generateRedlines = flags.generateRedlines !== undefined
407
- ? (flags.generateRedlines !== 'false' && flags.generateRedlines !== false)
408
- : (!flags.noRedlines);
409
- const result = await document.applyOperations(operations, {
410
- author,
411
- atomic: flags.atomic === true || flags.atomic === 'true',
636
+ };
637
+ if (command === 'apply') {
638
+ const agentProfile = profile === 'agent';
639
+ const author = flags.author || process.env.DOCX_REDLINE_AUTHOR || 'AI Redliner';
640
+ const generateRedlines = flags.generateRedlines !== undefined
641
+ ? (flags.generateRedlines !== 'false' && flags.generateRedlines !== false)
642
+ : (!flags.noRedlines);
643
+ const atomic = flags.atomic !== undefined
644
+ ? (flags.atomic === true || flags.atomic === 'true')
645
+ : false;
646
+ const requireComplete = flags.requireComplete !== undefined
647
+ ? (flags.requireComplete === true || flags.requireComplete === 'true')
648
+ : agentProfile;
649
+ const effectiveOptions = {
650
+ author,
651
+ atomic,
652
+ strictTargets: true,
653
+ validate: true,
654
+ generateRedlines,
655
+ existingRevisions: flags.existingRevisions || 'merge-same-author',
656
+ requireComplete
657
+ };
658
+ const result = await document.applyOperations(operations, {
659
+ author,
660
+ atomic,
412
661
  validate: true,
413
662
  strictTargets: true,
414
663
  generateRedlines,
@@ -420,7 +669,12 @@ export async function executeCli(argv) {
420
669
  command,
421
670
  input,
422
671
  ...serializable(mutationResult),
423
- ...(result.status === 'error' || result.error ? { exitCode: 2 } : {})
672
+ ...(profile ? { executionProfile: profile, effectiveOptions } : {}),
673
+ ...(result.status === 'error'
674
+ ? { exitCode: 2 }
675
+ : (result.status === 'partial' && requireComplete
676
+ ? { exitCode: 3 }
677
+ : {}))
424
678
  });
425
679
  }
426
680
  const filter = flags.allAuthors ? { allAuthors: true } : flags.author ? { author: String(flags.author) } : null;
@@ -430,8 +684,12 @@ export async function executeCli(argv) {
430
684
  } catch (error) { return cliError(error.code || 'CLI_FAILED', error.message); }
431
685
  }
432
686
 
433
- export async function runCli(argv = process.argv.slice(2), io = process) {
434
- configureLogger({}, { level: 'silent' });
435
- const result = await executeCli(argv); io.stdout.write(`${JSON.stringify(serializable(result), null, 2)}\n`);
436
- return result.status === 'error' ? (result.exitCode || 1) : 0;
437
- }
687
+ export async function runCli(argv = process.argv.slice(2), io = process) {
688
+ configureLogger({}, { level: 'silent' });
689
+ const result = await executeCli(argv, io);
690
+ const compactJson = parseArgs(argv).flags.compact === true;
691
+ io.stdout.write(`${JSON.stringify(serializable(result), null, compactJson ? 0 : 2)}\n`);
692
+ return Number.isInteger(result.exitCode) && result.exitCode !== 0
693
+ ? result.exitCode
694
+ : (result.status === 'error' ? 1 : 0);
695
+ }