@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.
- package/AGENTS.md +15 -10
- package/ARCHITECTURE.md +30 -10
- package/CHANGELOG.md +539 -431
- package/README.md +331 -309
- package/adapters/xml-adapter.js +84 -56
- package/core/sha256.js +120 -0
- package/dist/docx-redline-js.esm.js +8982 -693
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +102 -89
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +28305 -0
- package/dist/docx-redline.bundle.cjs.map +7 -0
- package/dist/docx-redline.bundle.js +28296 -0
- package/dist/docx-redline.bundle.js.map +7 -0
- package/docs/AGENT_FAST_START.md +2 -1
- package/docs/AGENT_KNOWLEDGE_BASE.md +480 -393
- package/docs/SKILL_AUTHORING.md +5 -2
- package/docs/TESTING.md +2 -1
- package/docs/schemas/document-operations.schema.json +29 -0
- package/document/docx-document.d.ts +39 -0
- package/document/docx-document.js +1010 -0
- package/document/zip-archive.js +258 -0
- package/engine/reconstruction-writer.js +18 -0
- package/index.d.ts +598 -531
- package/index.js +349 -340
- package/node/cli-help.js +5 -5
- package/node/cli.js +760 -754
- package/node/docx-document.js +11 -355
- package/node/index.d.ts +1 -1
- package/node/zip-archive.js +28 -38
- package/package.json +14 -11
- package/scripts/build.mjs +40 -5
- package/scripts/generate-word-comment-edge-case-fixture.ps1 +58 -0
- package/scripts/generate-word-comment-thread-fixture.ps1 +36 -0
- package/scripts/generate-word-header-footer-fixture.ps1 +32 -0
- package/scripts/generate-word-multi-section-fixture.ps1 +27 -0
- package/scripts/generate-word-package-integrity-cases.mjs +141 -0
- package/scripts/generate-word-thread-state-fixtures.ps1 +50 -0
- package/scripts/word-com-package-integrity.ps1 +98 -0
- package/services/batch-operation-orchestrator.js +262 -259
- package/services/comment-replies.js +96 -95
- package/services/comment-thread-parts.js +280 -0
- package/services/document-inspection.js +366 -365
- package/services/document-operation-applier.js +230 -202
- package/services/document-operation-contract.js +17 -2
- package/services/error-recovery.js +6 -0
- package/services/headers-footers.js +206 -0
- package/services/operation-batch-compiler.js +1 -0
- package/services/operation-preflight.js +231 -218
- package/services/package-parts.js +80 -0
- package/services/revision-token.js +11 -7
- package/services/standalone-docx-plumbing.js +631 -569
- 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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
954
|
-
|
|
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
|
+
}
|