@ansonlai/docx-redline-js 0.7.0 → 0.7.2
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 +22 -27
- package/CHANGELOG.md +21 -1
- package/README.md +43 -22
- package/dist/docx-redline-js.esm.js +3 -4
- package/dist/docx-redline-js.esm.js.map +2 -2
- package/dist/docx-redline-js.esm.min.js +51 -51
- package/dist/docx-redline-js.esm.min.js.map +2 -2
- package/docs/AGENT_FAST_START.md +31 -33
- package/docs/AGENT_KNOWLEDGE_BASE.md +67 -55
- package/docs/SKILL_AUTHORING.md +82 -100
- package/docs/releases/0.7.0.md +56 -36
- package/docs/schemas/document-operations.schema.json +1 -1
- package/docs/validation-reports/2026-09-14-agent-surface-simplification.md +75 -0
- package/node/cli-help.js +8 -6
- package/node/cli.js +57 -9
- package/package.json +4 -1
- package/services/document-inspection.js +2 -3
- package/services/document-operation-applier.js +12 -2
- package/services/document-operation-contract.js +49 -2
- package/services/document-operation-mutations.js +36 -36
- package/services/localized-replacement-compiler.js +73 -7
- package/services/operation-batch-compiler.js +9 -0
- package/services/standalone-operation-runner.d.ts +25 -4
package/docs/releases/0.7.0.md
CHANGED
|
@@ -70,11 +70,12 @@ Globally unique literal edits no longer require preliminary extraction:
|
|
|
70
70
|
docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
When global matching is too broad, use a longer, fairly unique nearby phrase and
|
|
74
|
+
a directional paragraph scope. Longer anchors generally have a higher chance of
|
|
75
|
+
being unique:
|
|
75
76
|
|
|
76
77
|
```bash
|
|
77
|
-
docx-redline apply contract.docx --search "
|
|
78
|
+
docx-redline apply contract.docx --search "distinctive nearby heading or phrase" --context-range 1:3 --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
|
|
78
79
|
```
|
|
79
80
|
|
|
80
81
|
`1:3` means strictly the next three physical paragraphs, excluding the heading
|
|
@@ -84,10 +85,13 @@ document boundaries. `--around 3` is shorthand for `-3:3`.
|
|
|
84
85
|
|
|
85
86
|
Every matching search anchor contributes a window. The library unions and
|
|
86
87
|
deduplicates those windows, then requires exactly one eligible paragraph to
|
|
87
|
-
contain the exact `find` literal.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
contain the exact `find` literal. A generic or repeated anchor can therefore
|
|
89
|
+
widen the scope until multiple paragraphs contain `find`, producing a safe
|
|
90
|
+
`AMBIGUOUS_TARGET` failure and an extra recovery turn. On success,
|
|
91
|
+
`results[i].change.context.anchorMatchCount > 1` means the anchor was repeated;
|
|
92
|
+
confirm the returned location and excerpts. It is a specificity signal, not an
|
|
93
|
+
automatic failure. `--occurrence` may disambiguate only inside the uniquely
|
|
94
|
+
selected paragraph; it never chooses between paragraphs.
|
|
91
95
|
|
|
92
96
|
### 3. Refusal Without Output Damage
|
|
93
97
|
|
|
@@ -106,43 +110,59 @@ overwritten. The source remains immutable unless `--in-place` is explicit.
|
|
|
106
110
|
|
|
107
111
|
### 4. Explicit Post-Mutation Change Evidence
|
|
108
112
|
|
|
109
|
-
Successful localized operations return a compact evidence object
|
|
113
|
+
Successful localized operations return a compact evidence object inside the
|
|
114
|
+
corresponding operation result:
|
|
110
115
|
|
|
111
116
|
```json
|
|
112
117
|
{
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
118
|
+
"status": "ok",
|
|
119
|
+
"written": true,
|
|
120
|
+
"completion": true,
|
|
121
|
+
"results": [
|
|
122
|
+
{
|
|
123
|
+
"index": 1,
|
|
124
|
+
"status": "applied",
|
|
125
|
+
"change": {
|
|
126
|
+
"kind": "localized_replacement",
|
|
127
|
+
"committed": true,
|
|
128
|
+
"finalDisposition": "applied",
|
|
129
|
+
"target": {
|
|
130
|
+
"paragraphId": "1A2B3C4D",
|
|
131
|
+
"humanReference": "Relevant heading or text lead"
|
|
132
|
+
},
|
|
133
|
+
"context": {
|
|
134
|
+
"search": "distinctive nearby heading or phrase",
|
|
135
|
+
"range": "1:3",
|
|
136
|
+
"anchorMatchCount": 1
|
|
137
|
+
},
|
|
138
|
+
"replacements": [
|
|
139
|
+
{
|
|
140
|
+
"find": "thirty (30) days",
|
|
141
|
+
"replace": "sixty (60) days",
|
|
142
|
+
"beforeExcerpt": "...thirty (30) days...",
|
|
143
|
+
"afterExcerpt": "...sixty (60) days..."
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"verification": {
|
|
147
|
+
"acceptedViewMatchesCompiledText": true
|
|
148
|
+
}
|
|
132
149
|
}
|
|
133
|
-
],
|
|
134
|
-
"verification": {
|
|
135
|
-
"acceptedViewMatchesCompiledText": true
|
|
136
150
|
}
|
|
137
|
-
|
|
151
|
+
]
|
|
138
152
|
}
|
|
139
153
|
```
|
|
140
154
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
The excerpts come from the resolved batch-start source and compiled desired
|
|
156
|
+
text; the complete actual post-mutation accepted view is then compared with that
|
|
157
|
+
desired text. Positive verification therefore authenticates the reported after
|
|
158
|
+
state without returning the full paragraph. Excerpts are deterministically
|
|
159
|
+
bounded to 120 characters.
|
|
160
|
+
|
|
161
|
+
Report success only when `completion: true`, `written: true`, the output path is
|
|
162
|
+
non-null, every operation succeeded, and every localized result has
|
|
163
|
+
`change.committed: true`, `change.finalDisposition: "applied"`, and
|
|
164
|
+
`change.verification.acceptedViewMatchesCompiledText: true`. Atomic rollback
|
|
165
|
+
marks previously attempted evidence `committed: false` and `rolled_back`.
|
|
146
166
|
|
|
147
167
|
### 5. Agent Guidance and Development Wrapper
|
|
148
168
|
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"allOf": [ { "$ref": "#/$defs/base" }, { "type": "object", "required": ["modified"], "properties": { "type": { "enum": ["redline", "replace", "format", "list-change", "table-reconciliation", "insert"] }, "modified": { "type": "string" }, "structuredContent": { "type": "boolean" }, "targetEnd": { "$ref": "#/$defs/target" }, "targetEndRef": { "type": ["integer", "string", "null"] }, "anchor": { "$ref": "#/$defs/rejectedTextInsertionAnchor" } } } ]
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
|
-
"allOf": [ { "$ref": "#/$defs/base" }, { "type": "object", "
|
|
119
|
+
"allOf": [ { "$ref": "#/$defs/base" }, { "type": "object", "properties": { "type": { "const": "restore" }, "modified": { "oneOf": [ { "type": "string", "minLength": 1 }, { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } } ] }, "replacements": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/localizedReplacement" } }, "targetEnd": { "$ref": "#/$defs/target" }, "targetEndRef": { "type": ["integer", "string", "null"] } }, "oneOf": [ { "required": ["modified"], "not": { "required": ["replacements"] } }, { "required": ["replacements"], "not": { "required": ["modified"] } }, { "not": { "anyOf": [ { "required": ["modified"] }, { "required": ["replacements"] } ] } } ] } ]
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"allOf": [ { "$ref": "#/$defs/base" }, { "type": "object", "properties": { "type": { "const": "delete" }, "modified": { "const": "" } } } ]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Agent Surface Simplification Replay
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-14
|
|
4
|
+
**Runtime:** Node.js 24.11.1 on Windows x64
|
|
5
|
+
**Engine:** Current post-0.7.1 source for every policy
|
|
6
|
+
**Command:** `npm run benchmark:agent-surface`
|
|
7
|
+
**Configuration:** 7 measured iterations and 1 warmup per scenario/policy
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
This repository-controlled replay compares three workflow policies without
|
|
12
|
+
changing the underlying engine:
|
|
13
|
+
|
|
14
|
+
1. `v0.6.2-extract-full`: extraction followed by complete modified text.
|
|
15
|
+
2. `v0.7.1-speculation-led`: speculative literal edits with extraction/apply
|
|
16
|
+
recovery, and extraction for semantic or batch work.
|
|
17
|
+
3. `simplified-extract-localized`: extraction followed by a compact localized
|
|
18
|
+
operation against `paragraphId` plus `fingerprint`.
|
|
19
|
+
|
|
20
|
+
The six wholly synthetic scenarios cover a long literal edit, semantic drafting,
|
|
21
|
+
repeated visible text, rejected-view restoration, an NBSP-bearing target, and an
|
|
22
|
+
independent two-target batch. Every output is re-extracted and compared with its
|
|
23
|
+
predetermined accepted-view text. Benchmark-only verification calls are not
|
|
24
|
+
counted as workflow calls.
|
|
25
|
+
|
|
26
|
+
## Aggregate Results
|
|
27
|
+
|
|
28
|
+
| Policy | Correct runs | Total calls | Calls/run | Failed calls | Edit request bytes | Median local ms |
|
|
29
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
30
|
+
| v0.6.2 extract/full | 42/42 | 84 | 2.00 | 0 | 27,034 | 3.86 |
|
|
31
|
+
| v0.7.1 speculation-led | 42/42 | 77 | 1.83 | 7 | 8,988 | 3.86 |
|
|
32
|
+
| simplified extract/localized | 42/42 | 84 | 2.00 | 0 | 10,661 | 3.77 |
|
|
33
|
+
|
|
34
|
+
The simplified policy reduced generated edit-request bytes by 60.6% relative to
|
|
35
|
+
the complete-paragraph baseline while preserving a fixed two-call workflow and
|
|
36
|
+
zero failed calls. On the long-paragraph case, its median edit request was 193
|
|
37
|
+
bytes rather than 2,104 bytes, a 90.8% reduction.
|
|
38
|
+
|
|
39
|
+
The speculation-led policy saved seven total calls because two unique literal
|
|
40
|
+
scenarios completed in one call. The repeated-text scenario cost three calls and
|
|
41
|
+
one failure on every iteration. This demonstrates both the legitimate one-turn
|
|
42
|
+
benefit and the recovery cost that the simplified default is intended to avoid.
|
|
43
|
+
|
|
44
|
+
The localized form is not always smaller for a short paragraph. In the NBSP
|
|
45
|
+
scenario it used 344 bytes versus 343 bytes for complete modified text. The
|
|
46
|
+
benefit is strongest when the inspected source is long relative to the changed
|
|
47
|
+
span.
|
|
48
|
+
|
|
49
|
+
## Interpretation
|
|
50
|
+
|
|
51
|
+
The repository evidence supports the surface decision:
|
|
52
|
+
|
|
53
|
+
- keep speculative apply as an available expert/harness capability;
|
|
54
|
+
- do not make ordinary agents classify whether to use it;
|
|
55
|
+
- lead with deterministic extraction and one apply;
|
|
56
|
+
- retain localized replacements after inspection to avoid long output payloads;
|
|
57
|
+
- use compact `paragraphId` plus `fingerprint` targets for localized requests,
|
|
58
|
+
while retaining extracted text locally for drafting; and
|
|
59
|
+
- keep restoration as a request/hint-driven branch.
|
|
60
|
+
|
|
61
|
+
Local execution time is effectively neutral at this scale and remains far below
|
|
62
|
+
agent deliberation or provider latency. No performance claim should be based on
|
|
63
|
+
the small local timing differences in this report.
|
|
64
|
+
|
|
65
|
+
## Limitations
|
|
66
|
+
|
|
67
|
+
This is a deterministic workflow replay, not a model evaluation. It cannot
|
|
68
|
+
measure reasoning tokens, provider tokenization, transport latency, stochastic
|
|
69
|
+
instruction following, or subjective drafting quality. Repeated external agent
|
|
70
|
+
runs using the same model, prompt, document, and engine are still required before
|
|
71
|
+
claiming end-to-end latency improvement.
|
|
72
|
+
|
|
73
|
+
The machine-readable output is written to
|
|
74
|
+
`tmp/benchmarks/agent-surface-simplification-latest.json` and is intentionally
|
|
75
|
+
not committed.
|
package/node/cli-help.js
CHANGED
|
@@ -12,7 +12,7 @@ const COMPACT = option('compact', '--compact', 'Emit one-line JSON to reduce pro
|
|
|
12
12
|
const INSPECTION_OPTIONS = Object.freeze([
|
|
13
13
|
HELP,
|
|
14
14
|
option('search', '--search <text>', 'Case-insensitive substring search.'),
|
|
15
|
-
option('revised', '--revised', 'Select paragraphs containing tracked revisions.'),
|
|
15
|
+
option('revised', '--revised', 'Select paragraphs containing tracked revisions; extract identifies authors and fully hidden deleted/inserted paragraphs.'),
|
|
16
16
|
option('table', '--table', 'Select paragraphs inside tables.'),
|
|
17
17
|
option('body', '--body', 'Select paragraphs outside tables.'),
|
|
18
18
|
option('nonEmpty', '--non-empty', 'Exclude empty paragraphs; this is not a narrow document scope.'),
|
|
@@ -56,15 +56,14 @@ const APPLY_EXAMPLES = Object.freeze([
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
|
-
description: '
|
|
59
|
+
description: 'Restore one wholly foreign-deleted paragraph verbatim from a strong rejected-view target.',
|
|
60
60
|
operation: {
|
|
61
61
|
type: 'restore',
|
|
62
62
|
target: {
|
|
63
63
|
exactText: 'Deleted source paragraph.',
|
|
64
64
|
paragraphId: '3A2B3C4D',
|
|
65
65
|
revisionView: 'rejected'
|
|
66
|
-
}
|
|
67
|
-
modified: 'Restored and revised paragraph.'
|
|
66
|
+
}
|
|
68
67
|
}
|
|
69
68
|
}
|
|
70
69
|
]);
|
|
@@ -133,11 +132,12 @@ export const CLI_COMMAND_HELP = Object.freeze({
|
|
|
133
132
|
option('expectedRevision', '--expected-revision <token|json>', 'Reject a stale package revision.'),
|
|
134
133
|
option('target', '--target <text>', 'Inline one-operation target text.'),
|
|
135
134
|
option('targetId', '--target-id <paragraphId>', 'Strong inline target using a paragraphId from fresh inspection.'),
|
|
135
|
+
option('restore', '--restore', 'Restore one rejected-view paragraph identified by --target-id; omit --modified for verbatim restoration.'),
|
|
136
136
|
option('modified', '--modified <text>', 'Complete desired accepted-view target content.'),
|
|
137
|
-
option('find', '--find <exact text>', '
|
|
137
|
+
option('find', '--find <exact text>', 'Case-sensitive source span; exact match first, then ASCII-space/NBSP equivalence.'),
|
|
138
138
|
option('replace', '--replace <text>', 'Replacement text for --find; an empty string deletes the span.'),
|
|
139
139
|
option('occurrence', '--occurrence <N>', 'Positive 1-based --find occurrence within the uniquely resolved target paragraph.'),
|
|
140
|
-
option('search', '--search <text>', 'Case-insensitive context anchor for speculative find/replace; all
|
|
140
|
+
option('search', '--search <text>', 'Case-insensitive context anchor for speculative find/replace; prefer a longer, fairly unique phrase because all matches contribute scope.'),
|
|
141
141
|
option('contextRange', '--context-range <START:END>', 'Signed inclusive paragraph offsets (-20..20) from each --search anchor; 1:3 means strictly the next three paragraphs.'),
|
|
142
142
|
option('around', '--around <N>', 'With --search, shorthand for symmetric --context-range -N:N (0-20).'),
|
|
143
143
|
option('comment', '--comment <text>', 'Create an inline comment operation.'),
|
|
@@ -154,7 +154,9 @@ export const CLI_COMMAND_HELP = Object.freeze({
|
|
|
154
154
|
notes: [
|
|
155
155
|
'modified is complete desired accepted-view content, not only inserted words.',
|
|
156
156
|
'Use either modified or find/replace. A strong accepted-view target is preferred when already known.',
|
|
157
|
+
'--restore requires --target-id from rejected-view extraction; omit modified for verbatim restore or use one inline find/replace.',
|
|
157
158
|
'Without a strong target, find/replace may resolve globally or within --search plus a directional context range; it fails unless exactly one paragraph is eligible.',
|
|
159
|
+
'Generic or repeated search anchors widen the unioned scope and may cause AMBIGUOUS_TARGET; on success, anchorMatchCount greater than 1 signals that the returned location and excerpts should be confirmed.',
|
|
158
160
|
'--occurrence disambiguates repeated text only within one uniquely selected paragraph, never between paragraphs.',
|
|
159
161
|
'Inline find/replace accepts one replacement; use operations JSON for multiple simultaneous replacements.',
|
|
160
162
|
'Use a structured JSON file or serializer-backed stdin; never interpolate legal text through raw shell quoting.',
|
package/node/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ 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
|
+
import { localizedReplacementOffsets } from '../services/localized-replacement-compiler.js';
|
|
10
11
|
import { buildCliHelp, CLI_COMMANDS, commandOptionKeys } from './cli-help.js';
|
|
11
12
|
|
|
12
13
|
const suffixes = { apply: 'redlined', accept: 'accepted', reject: 'rejected', 'delete-comments': 'comments-removed' };
|
|
@@ -31,6 +32,8 @@ const CLI_CAPABILITIES = [
|
|
|
31
32
|
'localized-replacements-v1',
|
|
32
33
|
'speculative-search-apply-v1',
|
|
33
34
|
'localized-change-summary-v1',
|
|
35
|
+
'localized-space-equivalence-v1',
|
|
36
|
+
'restore-shortcuts-v1',
|
|
34
37
|
'deduplicated-cli-receipts',
|
|
35
38
|
'compact-cli-json-v1'
|
|
36
39
|
];
|
|
@@ -241,21 +244,27 @@ function resolveSpeculativePatchTarget(document, flags) {
|
|
|
241
244
|
}
|
|
242
245
|
|
|
243
246
|
const find = String(flags.find);
|
|
244
|
-
const
|
|
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);
|
|
245
254
|
const context = {
|
|
246
255
|
...(search ? { search, range: range.text, anchorMatchCount: anchors.length } : {})
|
|
247
256
|
};
|
|
248
257
|
if (matches.length === 0) {
|
|
249
258
|
throw speculativeError('PATCH_SOURCE_NOT_FOUND', search
|
|
250
|
-
? `No paragraph in the contextual range contains the
|
|
251
|
-
: `No paragraph contains the
|
|
259
|
+
? `No paragraph in the contextual range contains the patch source: "${find}".`
|
|
260
|
+
: `No paragraph contains the patch source: "${find}".`, {
|
|
252
261
|
context,
|
|
253
262
|
...(anchors.length ? { candidates: anchors.map(speculativeCandidate) } : {}),
|
|
254
263
|
recovery: { action: 'reinspect', requiresReinspection: true, sameArgumentsSafe: false }
|
|
255
264
|
});
|
|
256
265
|
}
|
|
257
266
|
if (matches.length > 1) {
|
|
258
|
-
throw speculativeError('AMBIGUOUS_TARGET', `The
|
|
267
|
+
throw speculativeError('AMBIGUOUS_TARGET', `The patch source matched ${matches.length} eligible paragraphs.`, {
|
|
259
268
|
context,
|
|
260
269
|
candidates: matches.map(speculativeCandidate),
|
|
261
270
|
recovery: { action: 'choose_candidate', requiresReinspection: false, sameArgumentsSafe: false }
|
|
@@ -278,6 +287,7 @@ function resolveSpeculativePatchTarget(document, flags) {
|
|
|
278
287
|
}
|
|
279
288
|
|
|
280
289
|
async function readOperations(file, flags = {}, stdin = process.stdin, document = null) {
|
|
290
|
+
const inlineRestore = flags?.restore !== undefined;
|
|
281
291
|
const hasInlineTarget = flags?.target !== undefined
|
|
282
292
|
|| flags?.targetId !== undefined
|
|
283
293
|
|| flags?.targetRef !== undefined;
|
|
@@ -287,8 +297,8 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
287
297
|
const hasSpeculativeScope = flags?.search !== undefined
|
|
288
298
|
|| flags?.contextRange !== undefined
|
|
289
299
|
|| flags?.around !== undefined;
|
|
290
|
-
if (file && (hasInlinePatch || hasSpeculativeScope)) {
|
|
291
|
-
throw Object.assign(new Error('Use --operations or inline
|
|
300
|
+
if (file && (hasInlinePatch || hasSpeculativeScope || inlineRestore)) {
|
|
301
|
+
throw Object.assign(new Error('Use --operations or inline restore/find/replace options, not both.'), {
|
|
292
302
|
code: 'INVALID_OPERATION'
|
|
293
303
|
});
|
|
294
304
|
}
|
|
@@ -297,7 +307,12 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
297
307
|
code: 'INVALID_OPERATION'
|
|
298
308
|
});
|
|
299
309
|
}
|
|
300
|
-
if (!file && (hasInlineTarget || hasInlinePatch || hasSpeculativeScope)) {
|
|
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
|
+
}
|
|
301
316
|
if (hasSpeculativeScope && !hasInlinePatch) {
|
|
302
317
|
throw Object.assign(new Error('--search, --context-range, and --around are only valid with inline --find/--replace.'), {
|
|
303
318
|
code: 'INVALID_OPERATION'
|
|
@@ -319,9 +334,22 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
319
334
|
let target = targetId
|
|
320
335
|
? { ...(targetText == null ? {} : { exactText: targetText }), paragraphId: targetId }
|
|
321
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
|
+
}
|
|
322
345
|
let speculativeContext = null;
|
|
323
346
|
let op;
|
|
324
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
|
+
}
|
|
325
353
|
if (hasInlinePatch) {
|
|
326
354
|
throw Object.assign(new Error('Inline comments cannot be combined with --find/--replace.'), {
|
|
327
355
|
code: 'INVALID_OPERATION'
|
|
@@ -364,7 +392,7 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
364
392
|
speculativeContext = resolution.context;
|
|
365
393
|
}
|
|
366
394
|
op = {
|
|
367
|
-
type: 'redline',
|
|
395
|
+
type: inlineRestore ? 'restore' : 'redline',
|
|
368
396
|
...(target == null ? {} : { target }),
|
|
369
397
|
...(targetRef == null ? {} : { targetRef }),
|
|
370
398
|
replacements: [{
|
|
@@ -376,6 +404,14 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
376
404
|
...(flags.author ? { author: String(flags.author) } : {}),
|
|
377
405
|
...(flags.existingRevisions ? { existingRevisions: String(flags.existingRevisions) } : {})
|
|
378
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
|
+
};
|
|
379
415
|
} else {
|
|
380
416
|
op = {
|
|
381
417
|
type: 'replace',
|
|
@@ -430,8 +466,14 @@ function compactExtractParagraph(paragraph) {
|
|
|
430
466
|
const {
|
|
431
467
|
humanReference, provision, nearestHeading, index, ref, paragraphId,
|
|
432
468
|
fingerprint, revisionView, exactText, inTable, list, selectionRole,
|
|
433
|
-
contextFor
|
|
469
|
+
contextFor, hasRevisions, revisionAuthors, segments
|
|
434
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
|
+
));
|
|
435
477
|
return {
|
|
436
478
|
humanReference,
|
|
437
479
|
provision,
|
|
@@ -444,6 +486,12 @@ function compactExtractParagraph(paragraph) {
|
|
|
444
486
|
exactText,
|
|
445
487
|
inTable,
|
|
446
488
|
list,
|
|
489
|
+
...(hasRevisions ? {
|
|
490
|
+
hasRevisions: true,
|
|
491
|
+
revisionAuthors,
|
|
492
|
+
...(hiddenRevisionContent && revisionView === 'accepted' ? { deleted: true } : {}),
|
|
493
|
+
...(hiddenRevisionContent && revisionView === 'rejected' ? { inserted: true } : {})
|
|
494
|
+
} : {}),
|
|
447
495
|
...(selectionRole ? { selectionRole } : {}),
|
|
448
496
|
...(Array.isArray(contextFor) ? { contextFor } : {})
|
|
449
497
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ansonlai/docx-redline-js",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Host-independent OOXML reconciliation engine for .docx manipulation with track changes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"!scripts/benchmark-agent-workflow.mjs",
|
|
48
48
|
"!scripts/benchmark-agent-cli-discovery.mjs",
|
|
49
49
|
"!scripts/benchmark-localized-turn-reduction.mjs",
|
|
50
|
+
"!scripts/benchmark-agent-surface-simplification.mjs",
|
|
50
51
|
"!scripts/lib/agent-performance-cases.mjs",
|
|
51
52
|
"docs/AGENT_FAST_START.md",
|
|
52
53
|
"docs/AGENT_KNOWLEDGE_BASE.md",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"docs/validation-reports/2026-09-12-agent-cli-discovery-baseline.md",
|
|
59
60
|
"docs/validation-reports/2026-09-13-agent-cli-efficiency-rollout.md",
|
|
60
61
|
"docs/validation-reports/2026-09-14-localized-patching-rollout.md",
|
|
62
|
+
"docs/validation-reports/2026-09-14-agent-surface-simplification.md",
|
|
61
63
|
"index.js",
|
|
62
64
|
"index.d.ts",
|
|
63
65
|
"dist/",
|
|
@@ -97,6 +99,7 @@
|
|
|
97
99
|
"benchmark:agent": "node scripts/benchmark-agent-workflow.mjs",
|
|
98
100
|
"benchmark:agent-cli": "node scripts/benchmark-agent-cli-discovery.mjs",
|
|
99
101
|
"benchmark:localized": "node scripts/benchmark-localized-turn-reduction.mjs",
|
|
102
|
+
"benchmark:agent-surface": "node scripts/benchmark-agent-surface-simplification.mjs",
|
|
100
103
|
"benchmark:targeting": "node scripts/benchmark-targeting-hot-paths.mjs",
|
|
101
104
|
"benchmark:tests": "node scripts/benchmark-test-runner.mjs",
|
|
102
105
|
"profile:routes": "node scripts/profile-route-selection.mjs",
|
|
@@ -324,9 +324,8 @@ export function inspectDocumentParts(parts, options = {}) {
|
|
|
324
324
|
if (options.range && (index < rangeStart || index > rangeEnd)) continue;
|
|
325
325
|
if (Array.isArray(options.indexes) && !options.indexes.includes(index)) continue;
|
|
326
326
|
const pNode = paragraphNodes[i];
|
|
327
|
-
if (options.
|
|
328
|
-
if (options.
|
|
329
|
-
if (options.revised && revisionAuthors(pNode).length === 0) continue;
|
|
327
|
+
if (options.inTable != null && hasAncestor(pNode, 'tc') !== !!options.inTable) continue;
|
|
328
|
+
if (options.revisedOnly && revisionAuthors(pNode).length === 0) continue;
|
|
330
329
|
const altText = extractCanonicalParagraphText(pNode, { revisionView: alternateView });
|
|
331
330
|
if (options.skipEmpty && altText.length === 0) continue;
|
|
332
331
|
if (altText.toLowerCase().includes(needle)) {
|
|
@@ -160,7 +160,10 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
if (
|
|
163
|
+
if (
|
|
164
|
+
Array.isArray(operation.replacements)
|
|
165
|
+
|| (operation.operationKind === 'restore' && op?.modified === undefined)
|
|
166
|
+
) {
|
|
164
167
|
const compilation = compileOperationBatch(session.document, [op], {
|
|
165
168
|
strictTargets: options.strictTargets !== false,
|
|
166
169
|
onInfo: options.onInfo,
|
|
@@ -415,7 +418,14 @@ export async function applyOperationToDocumentXml(documentXml, op, author, runti
|
|
|
415
418
|
}
|
|
416
419
|
if (operation._localizedReplacementCompilation) {
|
|
417
420
|
let outputText = null;
|
|
418
|
-
if (operation.
|
|
421
|
+
if (operation.operationKind === 'restore') {
|
|
422
|
+
const restoredParagraph = operationOptions._mutationLiveNodes.find(node => node?.localName === 'p');
|
|
423
|
+
if (restoredParagraph) {
|
|
424
|
+
outputText = extractCanonicalParagraphText(restoredParagraph, {
|
|
425
|
+
revisionView: 'accepted'
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
} else if (operation._compiledSourceId && session.sourceTargetRegistry) {
|
|
419
429
|
const bound = session.sourceTargetRegistry.resolve(
|
|
420
430
|
operation._compiledSourceId,
|
|
421
431
|
session.document
|
|
@@ -307,19 +307,66 @@ export function validateDocumentOperation(operation) {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
if (normalized.operationKind === 'restore') {
|
|
310
|
+
const hasModified = operation.modified !== undefined;
|
|
311
|
+
const hasReplacements = operation.replacements !== undefined;
|
|
310
312
|
const validSingle = nonEmptyString(normalized.modified);
|
|
311
313
|
const validRange = Array.isArray(normalized.modified)
|
|
312
314
|
&& normalized.modified.length > 0
|
|
313
315
|
&& normalized.modified.every(nonEmptyString);
|
|
314
|
-
if (
|
|
316
|
+
if (hasModified && hasReplacements) {
|
|
315
317
|
return {
|
|
316
318
|
valid: false,
|
|
317
319
|
error: {
|
|
318
320
|
code: 'INVALID_OPERATION',
|
|
319
|
-
message: 'Restore operations
|
|
321
|
+
message: 'Restore operations accept modified or replacements, not both.'
|
|
320
322
|
}
|
|
321
323
|
};
|
|
322
324
|
}
|
|
325
|
+
if (hasModified && !validSingle && !validRange) {
|
|
326
|
+
return {
|
|
327
|
+
valid: false,
|
|
328
|
+
error: {
|
|
329
|
+
code: 'INVALID_OPERATION',
|
|
330
|
+
message: 'Restore modified must be a non-empty string or non-empty string array.'
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (!hasModified) {
|
|
335
|
+
if (
|
|
336
|
+
target.revisionView !== 'rejected'
|
|
337
|
+
|| (!target.paragraphId && !target.fingerprint)
|
|
338
|
+
|| target.captureRef
|
|
339
|
+
|| target.occurrence != null
|
|
340
|
+
|| normalized.captureKey
|
|
341
|
+
|| normalized.targetEndDescriptor
|
|
342
|
+
|| normalized.targetEndRef != null
|
|
343
|
+
|| /\r|\n/.test(target.text || '')
|
|
344
|
+
) {
|
|
345
|
+
return {
|
|
346
|
+
valid: false,
|
|
347
|
+
error: {
|
|
348
|
+
code: 'INVALID_OPERATION',
|
|
349
|
+
message: 'Verbatim and localized restore require one strong batch-start rejected-view paragraph target without captures, target occurrence, or a target range.'
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (hasReplacements) {
|
|
355
|
+
const replacementValidation = validateExactReplacementRequests(normalized.replacements);
|
|
356
|
+
if (!replacementValidation.ok) {
|
|
357
|
+
return { valid: false, error: replacementValidation.error };
|
|
358
|
+
}
|
|
359
|
+
if (replacementValidation.replacements.some(replacement => /\r|\n/.test(replacement.replace))) {
|
|
360
|
+
return {
|
|
361
|
+
valid: false,
|
|
362
|
+
error: {
|
|
363
|
+
code: 'INVALID_OPERATION',
|
|
364
|
+
message: 'Localized restore cannot insert paragraph breaks.'
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
normalized.replacements = replacementValidation.replacements;
|
|
369
|
+
}
|
|
323
370
|
if (normalized.generateRedlines === false) {
|
|
324
371
|
return {
|
|
325
372
|
valid: false,
|