@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.
- package/AGENTS.md +82 -697
- package/ARCHITECTURE.md +13 -1
- package/CHANGELOG.md +8 -0
- package/README.md +177 -45
- package/core/paragraph-targeting.js +14 -2
- package/dist/docx-redline-js.esm.js +184 -51
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +77 -77
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/AGENT_FAST_START.md +59 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +878 -0
- package/docs/SKILL_AUTHORING.md +126 -0
- package/docs/TESTING.md +35 -1
- package/docs/schemas/document-operations.schema.json +5 -1
- package/docs/validation-reports/2026-09-12-agent-cli-discovery-baseline.md +56 -0
- package/docs/validation-reports/2026-09-12-agent-protocol-rollout.md +86 -0
- package/docs/validation-reports/2026-09-13-agent-cli-efficiency-rollout.md +86 -0
- package/engine/oxml-engine.js +80 -13
- package/engine/run-builders.js +5 -15
- package/index.d.ts +28 -3
- package/node/cli-help.js +209 -0
- package/node/cli.js +323 -65
- package/node/docx-document.js +120 -69
- package/node/index.d.ts +6 -2
- package/package.json +15 -3
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +25 -25
- package/services/batch-operation-orchestrator.js +215 -120
- package/services/document-inspection.js +89 -11
- package/services/document-operation-applier.js +52 -34
- package/services/document-operation-contract.js +10 -6
- package/services/document-operation-mutations.js +51 -5
- package/services/document-operation-session.js +4 -0
- package/services/error-recovery.js +174 -0
- package/services/operation-batch-compiler.js +394 -0
- package/services/operation-preflight.js +91 -72
- package/services/standalone-operation-runner.d.ts +17 -1
- package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +0 -1669
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +0 -1399
- package/docs/plans/completed/2026-03-01-release-0.1.4-design.md +0 -33
- package/docs/plans/completed/2026-03-01-release-0.1.4.md +0 -110
- package/docs/plans/completed/2026-05-31-architectural changes.md +0 -593
- package/docs/plans/completed/2026-08-02-reliability-improvements.md +0 -1155
- package/docs/plans/completed/2026-08-30-reliability-testing-improvements.md +0 -488
- package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +0 -669
- package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +0 -427
- package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +0 -519
- package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +0 -69
- package/docs/plans/completed/structural-revision-capability-matrix.md +0 -115
- package/docs/test-comparison-dashboard.html +0 -4338
- package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +0 -22
- package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +0 -24
- package/docs/validation-reports/2026-08-30-phase-3-coverage.md +0 -73
- package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +0 -82
- package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +0 -114
- 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
|
-
|
|
11
|
-
|
|
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.
|
|
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
|
|
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;
|
|
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(
|
|
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
|
|
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 {
|
|
203
|
-
|
|
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
|
-
...
|
|
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)
|
|
320
|
-
|
|
321
|
-
|
|
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')
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
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'
|
|
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
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
...(
|
|
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
|
|
436
|
-
|
|
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
|
+
}
|