@ansonlai/docx-redline-js 0.6.0 → 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 +9 -5
- package/CHANGELOG.md +3 -0
- package/README.md +44 -22
- package/dist/docx-redline-js.esm.js +72 -19
- package/dist/docx-redline-js.esm.js.map +2 -2
- package/dist/docx-redline-js.esm.min.js +44 -44
- package/dist/docx-redline-js.esm.min.js.map +3 -3
- package/docs/AGENT_FAST_START.md +7 -7
- package/docs/AGENT_KNOWLEDGE_BASE.md +33 -23
- package/docs/SKILL_AUTHORING.md +126 -0
- package/docs/TESTING.md +17 -2
- 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 +7 -3
- package/docs/validation-reports/2026-09-13-agent-cli-efficiency-rollout.md +86 -0
- package/index.d.ts +11 -2
- package/node/cli-help.js +209 -0
- package/node/cli.js +221 -47
- package/package.json +6 -1
- package/services/document-inspection.js +84 -8
package/node/cli-help.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const option = (key, name, description) => Object.freeze({ key, name, description });
|
|
2
|
+
|
|
3
|
+
const HELP = option('help', '--help, -h', 'Return machine-readable help without opening a document.');
|
|
4
|
+
const AUTHOR = option('author', '--author <name>, -a <name>', 'Reviewer name; falls back to DOCX_REDLINE_AUTHOR, then AI Redliner.');
|
|
5
|
+
const OUTPUT = option('output', '--output <file>, -o <file>', 'Write to this destination; the source is not overwritten.');
|
|
6
|
+
const IN_PLACE = option('inPlace', '--in-place, -i', 'Explicitly overwrite the source document.');
|
|
7
|
+
const FORCE = option('force', '--force, -f', 'Allow replacement of an existing destination.');
|
|
8
|
+
const NO_OVERWRITE = option('noOverwrite', '--no-overwrite, --no-clobber', 'Refuse replacement of an existing destination.');
|
|
9
|
+
const ALL_AUTHORS = option('allAuthors', '--all-authors', 'Resolve review content for every author; requires explicit user authorization.');
|
|
10
|
+
const COMPACT = option('compact', '--compact', 'Emit one-line JSON to reduce provider-visible output bytes.');
|
|
11
|
+
|
|
12
|
+
const INSPECTION_OPTIONS = Object.freeze([
|
|
13
|
+
HELP,
|
|
14
|
+
option('search', '--search <text>', 'Case-insensitive substring search.'),
|
|
15
|
+
option('revised', '--revised', 'Select paragraphs containing tracked revisions.'),
|
|
16
|
+
option('table', '--table', 'Select paragraphs inside tables.'),
|
|
17
|
+
option('body', '--body', 'Select paragraphs outside tables.'),
|
|
18
|
+
option('nonEmpty', '--non-empty', 'Exclude empty paragraphs; this is not a narrow document scope.'),
|
|
19
|
+
option('index', '--index <N>', 'Select one 1-based machine paragraph index.'),
|
|
20
|
+
option('indexes', '--indexes <N,N,...>', 'Select comma-separated 1-based machine paragraph indexes.'),
|
|
21
|
+
option('range', '--range <START:END>', 'Select an inclusive range of 1-based machine paragraph indexes.'),
|
|
22
|
+
option('view', '--view <accepted|rejected|current>', 'Select the revision view; restore discovery normally uses rejected.'),
|
|
23
|
+
option('around', '--around <N>, --context <N>, -C <N>', 'With --search, include 0-20 physical paragraphs around each returned match.'),
|
|
24
|
+
option('limit', '--limit <N>', 'Return at most N direct matches; surrounding context does not count.'),
|
|
25
|
+
option('after', '--after <INDEX>', 'Continue after this exclusive 1-based source paragraph index.'),
|
|
26
|
+
option('all', '--all', 'Explicitly bypass default result and soft output limits.')
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const MUTATION_DESTINATION_OPTIONS = Object.freeze([
|
|
30
|
+
AUTHOR, OUTPUT, IN_PLACE, FORCE, NO_OVERWRITE
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const APPLY_EXAMPLES = Object.freeze([
|
|
34
|
+
{
|
|
35
|
+
description: 'Ordinary tracked replacement; modified is the complete desired accepted-view paragraph.',
|
|
36
|
+
operation: {
|
|
37
|
+
type: 'redline',
|
|
38
|
+
target: { exactText: 'Original clause.', paragraphId: '1A2B3C4D' },
|
|
39
|
+
modified: 'Revised clause.'
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
description: 'Comment the complete target paragraph.',
|
|
44
|
+
operation: {
|
|
45
|
+
type: 'comment',
|
|
46
|
+
target: { exactText: 'Clause to review.', paragraphId: '2A2B3C4D' },
|
|
47
|
+
commentContent: 'Please confirm this language.'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
description: 'Counterpropose a wholly foreign-deleted paragraph found through --view rejected.',
|
|
52
|
+
operation: {
|
|
53
|
+
type: 'restore',
|
|
54
|
+
target: {
|
|
55
|
+
exactText: 'Deleted source paragraph.',
|
|
56
|
+
paragraphId: '3A2B3C4D',
|
|
57
|
+
revisionView: 'rejected'
|
|
58
|
+
},
|
|
59
|
+
modified: 'Restored and revised paragraph.'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
export const CLI_COMMAND_HELP = Object.freeze({
|
|
65
|
+
version: {
|
|
66
|
+
summary: 'Report the machine contract version and capabilities.',
|
|
67
|
+
usage: 'docx-redline version',
|
|
68
|
+
options: [HELP],
|
|
69
|
+
notes: ['Wrappers should require only the capabilities they use.'],
|
|
70
|
+
examples: [{ command: 'docx-redline version' }]
|
|
71
|
+
},
|
|
72
|
+
inspect: {
|
|
73
|
+
summary: 'Inspect detailed paragraph, revision, comment, and structure metadata.',
|
|
74
|
+
usage: 'docx-redline inspect <file.docx> [options]',
|
|
75
|
+
options: INSPECTION_OPTIONS,
|
|
76
|
+
notes: [
|
|
77
|
+
'Prefer a focused --search, --index, or --range. Use extract when only exact edit targets are needed.',
|
|
78
|
+
'P<number>, index, and paragraph ordinals are machine references, not user-facing Word locations.'
|
|
79
|
+
],
|
|
80
|
+
examples: [
|
|
81
|
+
{ command: 'docx-redline inspect contract.docx --search "force majeure" --around 3' },
|
|
82
|
+
{ command: 'docx-redline inspect contract.docx --range 10:25 --view rejected' }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
extract: {
|
|
86
|
+
summary: 'Return compact exact targets and human-facing legal locations.',
|
|
87
|
+
usage: 'docx-redline extract <file.docx> [options]',
|
|
88
|
+
options: INSPECTION_OPTIONS,
|
|
89
|
+
notes: [
|
|
90
|
+
'Search is always case-insensitive.',
|
|
91
|
+
'Copy exactText plus paragraphId or fingerprint into operations; do not cite P<number> to users.',
|
|
92
|
+
'Broad results are paginated; follow selection.nextAfter or pass --all deliberately.'
|
|
93
|
+
],
|
|
94
|
+
examples: [
|
|
95
|
+
{ command: 'docx-redline extract contract.docx --search "force majeure" --around 3' },
|
|
96
|
+
{ command: 'docx-redline extract contract.docx --range 10:25' }
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
preflight: {
|
|
100
|
+
summary: 'Check an operation batch without mutating or writing a document.',
|
|
101
|
+
usage: 'docx-redline preflight <file.docx> --operations <file.json|-> [options]',
|
|
102
|
+
options: [
|
|
103
|
+
HELP,
|
|
104
|
+
option('operations', '--operations <file.json|->, --operations-file <file>', 'Read an operation array/envelope from a UTF-8 file or stdin.'),
|
|
105
|
+
AUTHOR,
|
|
106
|
+
option('strictTargets', '--strict-targets', 'Require strict target descriptors.'),
|
|
107
|
+
option('target', '--target <text>', 'Inline one-operation target text.'),
|
|
108
|
+
option('modified', '--modified <text>', 'Complete desired accepted-view target content.'),
|
|
109
|
+
option('comment', '--comment <text>', 'Create an inline comment operation.'),
|
|
110
|
+
option('textToComment', '--text-to-comment <text>', 'Anchor an inline comment to an exact subspan.'),
|
|
111
|
+
option('targetRef', '--target-ref <N>', 'Disambiguate an inline target with a 1-based machine index.'),
|
|
112
|
+
option('existingRevisions', '--existing-revisions <policy>', 'Select the explicit existing-revision policy.')
|
|
113
|
+
],
|
|
114
|
+
notes: ['Normal apply already performs validation; preflight is optional.'],
|
|
115
|
+
examples: [{ command: 'docx-redline preflight contract.docx --operations operations.json --author "Editor"' }]
|
|
116
|
+
},
|
|
117
|
+
apply: {
|
|
118
|
+
summary: 'Apply canonical document operations and write a derived DOCX.',
|
|
119
|
+
usage: 'docx-redline apply <file.docx> --operations <file.json|-> [options]',
|
|
120
|
+
options: [
|
|
121
|
+
HELP,
|
|
122
|
+
option('operations', '--operations <file.json|->, --operations-file <file>', 'Read an operation array/envelope from a UTF-8 file or serializer-backed stdin.'),
|
|
123
|
+
...MUTATION_DESTINATION_OPTIONS,
|
|
124
|
+
option('noClobber', '--no-clobber', 'Alias of --no-overwrite.'),
|
|
125
|
+
option('expectedRevision', '--expected-revision <token|json>', 'Reject a stale package revision.'),
|
|
126
|
+
option('target', '--target <text>', 'Inline one-operation target text.'),
|
|
127
|
+
option('modified', '--modified <text>', 'Complete desired accepted-view target content.'),
|
|
128
|
+
option('comment', '--comment <text>', 'Create an inline comment operation.'),
|
|
129
|
+
option('textToComment', '--text-to-comment <text>', 'Anchor an inline comment to an exact subspan.'),
|
|
130
|
+
option('targetRef', '--target-ref <N>', 'Disambiguate an inline target with a 1-based machine index.'),
|
|
131
|
+
option('existingRevisions', '--existing-revisions <policy>', 'Select revision handling; cross-author slicing must be deliberate.'),
|
|
132
|
+
option('atomic', '--atomic[=true|false]', 'Choose all-or-nothing or progressive batch execution.'),
|
|
133
|
+
option('generateRedlines', '--generate-redlines[=true|false]', 'Control tracked-change generation.'),
|
|
134
|
+
option('noRedlines', '--no-redlines', 'Apply clean text without tracked-change markup.'),
|
|
135
|
+
option('requireComplete', '--require-complete', 'Return exit code 3 for progressive partial completion.'),
|
|
136
|
+
option('profile', '--profile agent', 'Require complete machine execution without selecting atomic/progressive or revision policy; explicit flags compose with it.'),
|
|
137
|
+
COMPACT
|
|
138
|
+
],
|
|
139
|
+
notes: [
|
|
140
|
+
'modified is complete desired accepted-view content, not only inserted words.',
|
|
141
|
+
'Use a structured JSON file or serializer-backed stdin; never interpolate legal text through raw shell quoting.',
|
|
142
|
+
'The source is never overwritten unless --in-place is explicit.',
|
|
143
|
+
'The agent profile keeps progressive execution unless --atomic is explicit and does not change existing-revision policy.',
|
|
144
|
+
'Do not accept/reject foreign revisions or remove comments without user authorization.',
|
|
145
|
+
'Inspect completion, written, outputPath, every result, error.recovery, and retryPlan.'
|
|
146
|
+
],
|
|
147
|
+
examples: APPLY_EXAMPLES
|
|
148
|
+
},
|
|
149
|
+
accept: {
|
|
150
|
+
summary: 'Accept tracked revisions by one author or all authors.',
|
|
151
|
+
usage: 'docx-redline accept <file.docx> (--author <name>|--all-authors) [options]',
|
|
152
|
+
options: [HELP, ...MUTATION_DESTINATION_OPTIONS, option('allAuthors', '--all-authors', ALL_AUTHORS.description), option('noClobber', '--no-clobber', 'Alias of --no-overwrite.'), COMPACT],
|
|
153
|
+
notes: ['Accepting foreign review content requires explicit user authorization.'],
|
|
154
|
+
examples: [{ command: 'docx-redline accept reviewed.docx --author "Editor"' }]
|
|
155
|
+
},
|
|
156
|
+
reject: {
|
|
157
|
+
summary: 'Reject tracked revisions by one author or all authors.',
|
|
158
|
+
usage: 'docx-redline reject <file.docx> (--author <name>|--all-authors) [options]',
|
|
159
|
+
options: [HELP, ...MUTATION_DESTINATION_OPTIONS, option('allAuthors', '--all-authors', ALL_AUTHORS.description), option('noClobber', '--no-clobber', 'Alias of --no-overwrite.'), COMPACT],
|
|
160
|
+
notes: ['Rejecting foreign review content requires explicit user authorization.'],
|
|
161
|
+
examples: [{ command: 'docx-redline reject reviewed.docx --author "Editor"' }]
|
|
162
|
+
},
|
|
163
|
+
'delete-comments': {
|
|
164
|
+
summary: 'Delete comments by one author or all authors.',
|
|
165
|
+
usage: 'docx-redline delete-comments <file.docx> (--author <name>|--all-authors) [options]',
|
|
166
|
+
options: [HELP, ...MUTATION_DESTINATION_OPTIONS, option('allAuthors', '--all-authors', ALL_AUTHORS.description), option('noClobber', '--no-clobber', 'Alias of --no-overwrite.'), COMPACT],
|
|
167
|
+
notes: ['Removing reviewer comments requires explicit user authorization.'],
|
|
168
|
+
examples: [{ command: 'docx-redline delete-comments reviewed.docx --author "Reviewer"' }]
|
|
169
|
+
},
|
|
170
|
+
validate: {
|
|
171
|
+
summary: 'Validate revision markup and DOCX package wiring.',
|
|
172
|
+
usage: 'docx-redline validate <file.docx> [--baseline <file.docx>]',
|
|
173
|
+
options: [HELP, option('baseline', '--baseline <file.docx>', 'Report only validation issues introduced relative to a baseline package.')],
|
|
174
|
+
notes: ['Apply validates before writing; use this command for an explicit audit.'],
|
|
175
|
+
examples: [{ command: 'docx-redline validate reviewed.docx --baseline contract.docx' }]
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export const CLI_COMMANDS = Object.freeze(Object.keys(CLI_COMMAND_HELP));
|
|
180
|
+
|
|
181
|
+
export function commandOptionKeys(command) {
|
|
182
|
+
return (CLI_COMMAND_HELP[command]?.options || []).map(item => item.key);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function buildCliHelp(command = null) {
|
|
186
|
+
if (!command) {
|
|
187
|
+
return {
|
|
188
|
+
status: 'ok',
|
|
189
|
+
command: 'help',
|
|
190
|
+
usage: 'docx-redline <command> [file.docx] [options]',
|
|
191
|
+
commands: CLI_COMMANDS.map(name => ({ name, summary: CLI_COMMAND_HELP[name].summary })),
|
|
192
|
+
notes: ['Run docx-redline <command> --help for flags, semantics, and bounded examples.'],
|
|
193
|
+
documentation: ['AGENTS.md', 'docs/AGENT_FAST_START.md', 'docs/SKILL_AUTHORING.md', 'docs/schemas/document-operations.schema.json']
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const entry = CLI_COMMAND_HELP[command];
|
|
197
|
+
if (!entry) return null;
|
|
198
|
+
return {
|
|
199
|
+
status: 'ok',
|
|
200
|
+
command: 'help',
|
|
201
|
+
forCommand: command,
|
|
202
|
+
summary: entry.summary,
|
|
203
|
+
usage: entry.usage,
|
|
204
|
+
options: entry.options.map(({ key: _key, ...publicOption }) => publicOption),
|
|
205
|
+
notes: entry.notes,
|
|
206
|
+
examples: entry.examples,
|
|
207
|
+
documentation: ['AGENTS.md', 'docs/AGENT_FAST_START.md', 'docs/SKILL_AUTHORING.md', 'docs/schemas/document-operations.schema.json']
|
|
208
|
+
};
|
|
209
|
+
}
|
package/node/cli.js
CHANGED
|
@@ -7,9 +7,12 @@ 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
9
|
import { normalizeErrorWithRecovery } from '../services/error-recovery.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
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;
|
|
13
16
|
const CLI_CAPABILITIES = [
|
|
14
17
|
'atomic-batch-results-on-package-failure',
|
|
15
18
|
'baseline-aware-validation',
|
|
@@ -20,19 +23,15 @@ const CLI_CAPABILITIES = [
|
|
|
20
23
|
'recovery-envelope-v1',
|
|
21
24
|
'require-complete-exit',
|
|
22
25
|
'operations-stdin',
|
|
23
|
-
'agent-profile-
|
|
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'
|
|
24
33
|
];
|
|
25
|
-
const commandOptions =
|
|
26
|
-
version: new Set(['help']),
|
|
27
|
-
inspect: new Set(['help', 'search', 'revised', 'table', 'body', 'nonEmpty', 'index', 'indexes', 'range', 'view']),
|
|
28
|
-
extract: new Set(['help', 'search', 'revised', 'table', 'body', 'nonEmpty', 'index', 'indexes', 'range', 'view']),
|
|
29
|
-
preflight: new Set(['help', 'operations', 'author', 'strictTargets', 'target', 'modified', 'comment', 'textToComment', 'targetRef', 'existingRevisions']),
|
|
30
|
-
apply: new Set(['help', 'operations', 'author', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber', 'expectedRevision', 'target', 'modified', 'comment', 'textToComment', 'targetRef', 'existingRevisions', 'atomic', 'generateRedlines', 'noRedlines', 'requireComplete', 'profile']),
|
|
31
|
-
accept: new Set(['help', 'author', 'allAuthors', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber']),
|
|
32
|
-
reject: new Set(['help', 'author', 'allAuthors', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber']),
|
|
33
|
-
'delete-comments': new Set(['help', 'author', 'allAuthors', 'output', 'inPlace', 'force', 'noOverwrite', 'noClobber']),
|
|
34
|
-
validate: new Set(['help', 'baseline'])
|
|
35
|
-
};
|
|
34
|
+
const commandOptions = Object.fromEntries(CLI_COMMANDS.map(command => [command, new Set(commandOptionKeys(command))]));
|
|
36
35
|
|
|
37
36
|
function cliError(code, message, exitCode = 2, details) { return { status: 'error', error: normalizeErrorWithRecovery({ code, message, ...(details ? { details } : {}) }), exitCode }; }
|
|
38
37
|
const optionAliases = new Map([
|
|
@@ -46,8 +45,10 @@ const optionAliases = new Map([
|
|
|
46
45
|
['no-clobber', 'noClobber'],
|
|
47
46
|
['no-redlines', 'noRedlines'],
|
|
48
47
|
['generate-redlines', 'generateRedlines'],
|
|
49
|
-
['require-complete', 'requireComplete']
|
|
50
|
-
]
|
|
48
|
+
['require-complete', 'requireComplete'],
|
|
49
|
+
['context', 'around'],
|
|
50
|
+
['C', 'around']
|
|
51
|
+
]);
|
|
51
52
|
function parseArgs(argv) {
|
|
52
53
|
const positionals = []; const flags = {};
|
|
53
54
|
for (let index = 0; index < argv.length; index++) {
|
|
@@ -68,11 +69,19 @@ function positiveInteger(value) {
|
|
|
68
69
|
const parsed = /^\d+$/.test(text) ? Number(text) : null;
|
|
69
70
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
70
71
|
}
|
|
71
|
-
function invalidFilter(message) {
|
|
72
|
-
const error = new Error(message);
|
|
73
|
-
error.code = 'INVALID_FILTER';
|
|
74
|
-
return error;
|
|
75
|
-
}
|
|
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
|
+
}
|
|
76
85
|
function parseIndexes(value) {
|
|
77
86
|
const tokens = String(value).split(',');
|
|
78
87
|
if (!tokens.length || tokens.some(token => positiveInteger(token) == null)) {
|
|
@@ -109,10 +118,22 @@ function inspectionOptions(flags) {
|
|
|
109
118
|
const index = positiveInteger(flags.index);
|
|
110
119
|
if (index == null) throw invalidFilter('--index must be a positive 1-based integer.');
|
|
111
120
|
options.indexes = [index];
|
|
112
|
-
}
|
|
113
|
-
if (flags.indexes !== undefined) options.indexes = parseIndexes(flags.indexes);
|
|
114
|
-
if (flags.range !== undefined) options.range = parseRange(flags.range);
|
|
115
|
-
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) {
|
|
116
137
|
if (!['accepted', 'rejected', 'current'].includes(String(flags.view))) {
|
|
117
138
|
throw invalidFilter('--view must be accepted, rejected, or current.');
|
|
118
139
|
}
|
|
@@ -191,6 +212,114 @@ function serializable(value) {
|
|
|
191
212
|
return rest;
|
|
192
213
|
}
|
|
193
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
|
+
|
|
194
323
|
function boundedText(value, limit = 512) {
|
|
195
324
|
const text = String(value ?? '');
|
|
196
325
|
return text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
@@ -219,7 +348,7 @@ function compactError(error) {
|
|
|
219
348
|
compact.candidates = error.candidates.map(candidate => {
|
|
220
349
|
if (!candidate || typeof candidate !== 'object') return candidate;
|
|
221
350
|
const excerpt = boundedText(candidate.excerpt ?? candidate.exactText ?? candidate.text ?? '', 240);
|
|
222
|
-
return { ...compactResolvedTarget(candidate), ...(excerpt ? { excerpt } : {}) };
|
|
351
|
+
return { ...compactResolvedTarget(candidate, { preserveMatchDetails: true }), ...(excerpt ? { excerpt } : {}) };
|
|
223
352
|
});
|
|
224
353
|
}
|
|
225
354
|
for (const field of ['recovery', 'issueSummary', 'expectedRevision', 'currentRevision']) {
|
|
@@ -230,7 +359,7 @@ function compactError(error) {
|
|
|
230
359
|
...error.context,
|
|
231
360
|
...(error.context.currentTarget ? {
|
|
232
361
|
currentTarget: {
|
|
233
|
-
...compactResolvedTarget(error.context.currentTarget),
|
|
362
|
+
...compactResolvedTarget(error.context.currentTarget, { preserveMatchDetails: true }),
|
|
234
363
|
excerpt: boundedText(
|
|
235
364
|
error.context.currentTarget.excerpt
|
|
236
365
|
?? error.context.currentTarget.exactText
|
|
@@ -244,7 +373,7 @@ function compactError(error) {
|
|
|
244
373
|
}
|
|
245
374
|
if (error.sourceTarget && typeof error.sourceTarget === 'object') {
|
|
246
375
|
compact.sourceTarget = {
|
|
247
|
-
...compactResolvedTarget(error.sourceTarget),
|
|
376
|
+
...compactResolvedTarget(error.sourceTarget, { preserveMatchDetails: true }),
|
|
248
377
|
excerpt: boundedText(error.sourceTarget.text ?? error.sourceTarget.exactText ?? '', 240)
|
|
249
378
|
};
|
|
250
379
|
}
|
|
@@ -252,10 +381,32 @@ function compactError(error) {
|
|
|
252
381
|
return compact;
|
|
253
382
|
}
|
|
254
383
|
|
|
255
|
-
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 } = {}) {
|
|
256
398
|
if (!target || typeof target !== 'object') return target;
|
|
257
|
-
const {
|
|
258
|
-
|
|
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
|
+
};
|
|
259
410
|
}
|
|
260
411
|
|
|
261
412
|
function compactReceipt(receipt) {
|
|
@@ -271,12 +422,13 @@ function compactReceipt(receipt) {
|
|
|
271
422
|
|
|
272
423
|
function compactOperationResult(result) {
|
|
273
424
|
if (!result || typeof result !== 'object') return result;
|
|
425
|
+
const { receipt: _receipt, ...withoutReceipt } = result;
|
|
426
|
+
const preserveMatchDetails = result.status === 'error' || !!result.error;
|
|
274
427
|
return {
|
|
275
|
-
...
|
|
276
|
-
...(result.resolvedTarget ? { resolvedTarget: compactResolvedTarget(result.resolvedTarget) } : {}),
|
|
277
|
-
...(result.resolvedAnchor ? { resolvedAnchor: compactResolvedTarget(result.resolvedAnchor) } : {}),
|
|
428
|
+
...withoutReceipt,
|
|
429
|
+
...(result.resolvedTarget ? { resolvedTarget: compactResolvedTarget(result.resolvedTarget, { preserveMatchDetails }) } : {}),
|
|
430
|
+
...(result.resolvedAnchor ? { resolvedAnchor: compactResolvedTarget(result.resolvedAnchor, { preserveMatchDetails }) } : {}),
|
|
278
431
|
...(result.error ? { error: compactError(result.error) } : {}),
|
|
279
|
-
...(result.receipt ? { receipt: compactReceipt(result.receipt) } : {}),
|
|
280
432
|
...(Array.isArray(result.warnings) ? { warnings: result.warnings.map(warning => boundedText(warning)) } : {})
|
|
281
433
|
};
|
|
282
434
|
}
|
|
@@ -370,10 +522,14 @@ function subtractValidationIssues(issues, baselineIssues) {
|
|
|
370
522
|
}
|
|
371
523
|
|
|
372
524
|
export async function executeCli(argv, io = process) {
|
|
373
|
-
const { command, input: rawInput, extraPositionals, flags } = parseArgs(argv);
|
|
374
|
-
if (command === 'help' || flags.help)
|
|
375
|
-
|
|
376
|
-
|
|
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}`);
|
|
377
533
|
if (command === 'version') {
|
|
378
534
|
if (rawInput || extraPositionals.length > 0) return cliError('UNEXPECTED_ARGUMENT', `Unexpected argument: ${rawInput || extraPositionals[0]}`);
|
|
379
535
|
const optionError = validateCommandOptions(command, flags, []);
|
|
@@ -404,11 +560,27 @@ export async function executeCli(argv, io = process) {
|
|
|
404
560
|
let buffer; try { buffer = await readFile(input); } catch (error) { return cliError('INPUT_READ_FAILED', error.message); }
|
|
405
561
|
try {
|
|
406
562
|
const document = openDocx(buffer);
|
|
407
|
-
if (command === 'inspect')
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
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
|
+
}
|
|
412
584
|
if (command === 'validate') {
|
|
413
585
|
const issues = await collectValidationIssues(buffer);
|
|
414
586
|
if (flags.baseline) {
|
|
@@ -470,7 +642,7 @@ export async function executeCli(argv, io = process) {
|
|
|
470
642
|
: (!flags.noRedlines);
|
|
471
643
|
const atomic = flags.atomic !== undefined
|
|
472
644
|
? (flags.atomic === true || flags.atomic === 'true')
|
|
473
|
-
:
|
|
645
|
+
: false;
|
|
474
646
|
const requireComplete = flags.requireComplete !== undefined
|
|
475
647
|
? (flags.requireComplete === true || flags.requireComplete === 'true')
|
|
476
648
|
: agentProfile;
|
|
@@ -514,7 +686,9 @@ export async function executeCli(argv, io = process) {
|
|
|
514
686
|
|
|
515
687
|
export async function runCli(argv = process.argv.slice(2), io = process) {
|
|
516
688
|
configureLogger({}, { level: 'silent' });
|
|
517
|
-
const result = await executeCli(argv, io);
|
|
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`);
|
|
518
692
|
return Number.isInteger(result.exitCode) && result.exitCode !== 0
|
|
519
693
|
? result.exitCode
|
|
520
694
|
: (result.status === 'error' ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ansonlai/docx-redline-js",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Host-independent OOXML reconciliation engine for .docx manipulation with track changes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,12 +45,16 @@
|
|
|
45
45
|
"orchestration/",
|
|
46
46
|
"scripts/",
|
|
47
47
|
"!scripts/benchmark-agent-workflow.mjs",
|
|
48
|
+
"!scripts/benchmark-agent-cli-discovery.mjs",
|
|
48
49
|
"!scripts/lib/agent-performance-cases.mjs",
|
|
49
50
|
"docs/AGENT_FAST_START.md",
|
|
50
51
|
"docs/AGENT_KNOWLEDGE_BASE.md",
|
|
52
|
+
"docs/SKILL_AUTHORING.md",
|
|
51
53
|
"docs/TESTING.md",
|
|
52
54
|
"docs/schemas/document-operations.schema.json",
|
|
53
55
|
"docs/validation-reports/2026-09-12-agent-protocol-rollout.md",
|
|
56
|
+
"docs/validation-reports/2026-09-12-agent-cli-discovery-baseline.md",
|
|
57
|
+
"docs/validation-reports/2026-09-13-agent-cli-efficiency-rollout.md",
|
|
54
58
|
"index.js",
|
|
55
59
|
"index.d.ts",
|
|
56
60
|
"dist/",
|
|
@@ -88,6 +92,7 @@
|
|
|
88
92
|
"coverage:gaps": "node scripts/report-coverage-gaps.mjs",
|
|
89
93
|
"benchmark:session": "node scripts/benchmark-operation-session.mjs",
|
|
90
94
|
"benchmark:agent": "node scripts/benchmark-agent-workflow.mjs",
|
|
95
|
+
"benchmark:agent-cli": "node scripts/benchmark-agent-cli-discovery.mjs",
|
|
91
96
|
"benchmark:targeting": "node scripts/benchmark-targeting-hot-paths.mjs",
|
|
92
97
|
"benchmark:tests": "node scripts/benchmark-test-runner.mjs",
|
|
93
98
|
"profile:routes": "node scripts/profile-route-selection.mjs",
|