@ansonlai/docx-redline-js 0.7.0 → 0.7.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 +17 -6
- package/CHANGELOG.md +16 -1
- package/README.md +15 -6
- 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 +6 -5
- package/docs/AGENT_KNOWLEDGE_BASE.md +17 -7
- package/docs/SKILL_AUTHORING.md +7 -6
- package/docs/releases/0.7.0.md +56 -36
- package/docs/schemas/document-operations.schema.json +1 -1
- package/node/cli-help.js +7 -5
- package/node/cli.js +45 -5
- package/package.json +1 -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/operation-batch-compiler.js +9 -0
- package/services/standalone-operation-runner.d.ts +24 -4
package/docs/AGENT_FAST_START.md
CHANGED
|
@@ -21,8 +21,8 @@ It is a development sample, not a package API.
|
|
|
21
21
|
docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The source must occur in one eligible paragraph.
|
|
25
|
-
|
|
24
|
+
The source must occur in one eligible paragraph. To narrow it, use a longer, fairly unique phrase: `--search "distinctive nearby heading or phrase" --context-range 1:3`.
|
|
25
|
+
Generic/repeated anchors can cause `AMBIGUOUS_TARGET` and waste a recovery turn. If `anchorMatchCount > 1`, confirm the returned location and excerpts. `--around 3` is symmetric.
|
|
26
26
|
|
|
27
27
|
2. For semantic drafting such as “make this provision mutual,” extract enough
|
|
28
28
|
context, then copy `exactText` with `paragraphId` or `fingerprint`:
|
|
@@ -31,7 +31,7 @@ It is a development sample, not a package API.
|
|
|
31
31
|
docx-redline extract contract.docx --search "termination" --around 3
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Search is case-insensitive; follow `selection.hint` and `selection.nextAfter`, and report `humanReference`.
|
|
34
|
+
Search is case-insensitive; follow `selection.hint` and `selection.nextAfter`, and report `humanReference`. With `--revised`, use `hasRevisions`, `revisionAuthors`, and `deleted` or `inserted` to interpret paragraphs whose active-view `exactText` is empty. Restore one rejected-view paragraph verbatim with `apply --restore --target-id ID`; add one `--find`/`--replace` pair for an exact correction. The shortcut never expands to adjacent paragraphs. Otherwise, apply once with a UTF-8 operations file or serializer-backed stdin:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
|
|
@@ -46,8 +46,9 @@ The `agent` profile preserves progressive execution and the ordinary revision po
|
|
|
46
46
|
- Strong inspected targets bind to the batch start; independent edits need no bottom-up sorting.
|
|
47
47
|
- Consolidate writes to one source; use captures for created-content dependencies.
|
|
48
48
|
- Require `completion: true`, `written: true`, a non-null `outputPath`, and no
|
|
49
|
-
per-operation error. For localized edits also require
|
|
50
|
-
|
|
49
|
+
per-operation error. For localized edits also require
|
|
50
|
+
`results[i].change.committed: true`, `finalDisposition: "applied"`, and
|
|
51
|
+
`verification.acceptedViewMatchesCompiledText: true`. The source is
|
|
51
52
|
not overwritten unless `--in-place` is explicit.
|
|
52
53
|
- On failure, follow `error.recovery.action`. Never retry unchanged arguments.
|
|
53
54
|
- `retryPlan.base: "original"` means replay the batch; `"output"` means retain committed work and retry reported indexes.
|
|
@@ -484,9 +484,10 @@ The short route for a document-editing request is:
|
|
|
484
484
|
|
|
485
485
|
1. For an exact mechanical change with known old and new literals, use localized
|
|
486
486
|
`--find`/`--replace`. Omit the target for global fail-closed resolution, or
|
|
487
|
-
scope a
|
|
488
|
-
`--context-range 1:3`.
|
|
489
|
-
|
|
487
|
+
scope it with a longer, fairly unique nearby `--search` phrase and a
|
|
488
|
+
directional range such as `--context-range 1:3`. Generic or repeated anchors
|
|
489
|
+
widen the unioned scope and may fail with `AMBIGUOUS_TARGET`, costing another
|
|
490
|
+
turn. `--occurrence` selects only within one uniquely resolved paragraph.
|
|
490
491
|
2. For semantic drafting, run one focused `extract`. Add `--around 3` when
|
|
491
492
|
surrounding context on either side is needed, then copy `exactText` plus
|
|
492
493
|
`paragraphId` or `fingerprint`.
|
|
@@ -499,8 +500,10 @@ The short route for a document-editing request is:
|
|
|
499
500
|
text created elsewhere in the batch is scheduled automatically; use explicit
|
|
500
501
|
captures/selectors for non-unique or advanced created-content dependencies.
|
|
501
502
|
5. Walk every result and require `completion: true`, `written: true`, and no
|
|
502
|
-
per-operation error. For localized patches also require
|
|
503
|
-
|
|
503
|
+
per-operation error. For localized patches also require
|
|
504
|
+
`results[i].change.committed: true`, `finalDisposition: "applied"`, and
|
|
505
|
+
positive accepted-view verification. If `anchorMatchCount > 1`, confirm the
|
|
506
|
+
returned location and excerpts because the anchor was repeated.
|
|
504
507
|
6. Run a focused `extract` on changed clauses only when placement or list/table
|
|
505
508
|
structure needs confirmation.
|
|
506
509
|
|
|
@@ -512,8 +515,8 @@ below and make one cause-specific correction.
|
|
|
512
515
|
# 1. Globally unique mechanical edit in one call
|
|
513
516
|
docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
|
|
514
517
|
|
|
515
|
-
# 2.
|
|
516
|
-
docx-redline apply contract.docx --search "
|
|
518
|
+
# 2. Directional edit using a longer phrase with a higher chance of uniqueness
|
|
519
|
+
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
|
|
517
520
|
|
|
518
521
|
# 3. Focused contextual discovery for semantic drafting
|
|
519
522
|
docx-redline extract contract.docx --search "termination" --around 3
|
|
@@ -590,6 +593,13 @@ Paragraph indexes are 1-based. Inspection filters are `--index 12`,
|
|
|
590
593
|
case-insensitive. When a search returns 0 matches in the active view but matches
|
|
591
594
|
exist in the alternate view (such as searching for a deleted clause to restore),
|
|
592
595
|
`selection.hint` provides immediate guidance to re-run with `--view rejected`.
|
|
596
|
+
Compact `extract --revised` results retain `hasRevisions`, `revisionAuthors`,
|
|
597
|
+
and `deleted`/`inserted` cues, so an empty `exactText` is identifiable as content
|
|
598
|
+
hidden by the selected revision view rather than a failed filter.
|
|
599
|
+
For one strongly identified rejected-view paragraph, `apply --restore
|
|
600
|
+
--target-id ID` restores the rejected text verbatim. One exact
|
|
601
|
+
`--find`/`--replace` pair may adjust it without restating the paragraph. The
|
|
602
|
+
shortcut refuses ranges, captures, weak targets, and inferred adjacent content.
|
|
593
603
|
Add `--around N` (`--context N` or `-C N`) to a search;
|
|
594
604
|
context records are labeled separately and do not consume the direct-hit
|
|
595
605
|
`--limit`. Continue a bounded result with `--after <paragraph-index>`. Ordinary
|
package/docs/SKILL_AUTHORING.md
CHANGED
|
@@ -101,12 +101,13 @@ the first executable example should use the one-turn fast path:
|
|
|
101
101
|
docx-redline apply input.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
If the literal is not globally unique,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
and `
|
|
104
|
+
If the literal is not globally unique, use a longer, fairly unique nearby phrase
|
|
105
|
+
as `--search` context. Generic or repeated anchors widen the unioned scope and
|
|
106
|
+
may cause `AMBIGUOUS_TARGET`, wasting a recovery turn. Use a directional window
|
|
107
|
+
such as `1:3`; use symmetric `--around 3` only when either side is eligible. On
|
|
108
|
+
success, `anchorMatchCount > 1` signals a repeated anchor: confirm the returned
|
|
109
|
+
location and excerpts. Require `results[i].change` with `committed: true`,
|
|
110
|
+
`finalDisposition: "applied"`, and positive accepted-view verification.
|
|
110
111
|
|
|
111
112
|
Semantic requests such as “make this provision mutual” still begin with focused
|
|
112
113
|
contextual extraction because the agent must identify and draft the complete
|
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": "" } } } ]
|
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
137
|
option('find', '--find <exact text>', 'Exact case-sensitive source span for one localized replacement.'),
|
|
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
|
@@ -31,6 +31,7 @@ const CLI_CAPABILITIES = [
|
|
|
31
31
|
'localized-replacements-v1',
|
|
32
32
|
'speculative-search-apply-v1',
|
|
33
33
|
'localized-change-summary-v1',
|
|
34
|
+
'restore-shortcuts-v1',
|
|
34
35
|
'deduplicated-cli-receipts',
|
|
35
36
|
'compact-cli-json-v1'
|
|
36
37
|
];
|
|
@@ -278,6 +279,7 @@ function resolveSpeculativePatchTarget(document, flags) {
|
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
async function readOperations(file, flags = {}, stdin = process.stdin, document = null) {
|
|
282
|
+
const inlineRestore = flags?.restore !== undefined;
|
|
281
283
|
const hasInlineTarget = flags?.target !== undefined
|
|
282
284
|
|| flags?.targetId !== undefined
|
|
283
285
|
|| flags?.targetRef !== undefined;
|
|
@@ -287,8 +289,8 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
287
289
|
const hasSpeculativeScope = flags?.search !== undefined
|
|
288
290
|
|| flags?.contextRange !== undefined
|
|
289
291
|
|| flags?.around !== undefined;
|
|
290
|
-
if (file && (hasInlinePatch || hasSpeculativeScope)) {
|
|
291
|
-
throw Object.assign(new Error('Use --operations or inline
|
|
292
|
+
if (file && (hasInlinePatch || hasSpeculativeScope || inlineRestore)) {
|
|
293
|
+
throw Object.assign(new Error('Use --operations or inline restore/find/replace options, not both.'), {
|
|
292
294
|
code: 'INVALID_OPERATION'
|
|
293
295
|
});
|
|
294
296
|
}
|
|
@@ -297,7 +299,12 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
297
299
|
code: 'INVALID_OPERATION'
|
|
298
300
|
});
|
|
299
301
|
}
|
|
300
|
-
if (!file && (hasInlineTarget || hasInlinePatch || hasSpeculativeScope)) {
|
|
302
|
+
if (!file && (hasInlineTarget || hasInlinePatch || hasSpeculativeScope || inlineRestore)) {
|
|
303
|
+
if (inlineRestore && flags.restore !== true) {
|
|
304
|
+
throw Object.assign(new Error('--restore is a boolean flag and does not take a value.'), {
|
|
305
|
+
code: 'INVALID_OPERATION'
|
|
306
|
+
});
|
|
307
|
+
}
|
|
301
308
|
if (hasSpeculativeScope && !hasInlinePatch) {
|
|
302
309
|
throw Object.assign(new Error('--search, --context-range, and --around are only valid with inline --find/--replace.'), {
|
|
303
310
|
code: 'INVALID_OPERATION'
|
|
@@ -319,9 +326,22 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
319
326
|
let target = targetId
|
|
320
327
|
? { ...(targetText == null ? {} : { exactText: targetText }), paragraphId: targetId }
|
|
321
328
|
: targetText;
|
|
329
|
+
if (inlineRestore) {
|
|
330
|
+
if (!targetId) {
|
|
331
|
+
throw Object.assign(new Error('Inline --restore requires --target-id from rejected-view extraction.'), {
|
|
332
|
+
code: 'INVALID_OPERATION'
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
target = { ...target, revisionView: 'rejected' };
|
|
336
|
+
}
|
|
322
337
|
let speculativeContext = null;
|
|
323
338
|
let op;
|
|
324
339
|
if (flags.comment) {
|
|
340
|
+
if (inlineRestore) {
|
|
341
|
+
throw Object.assign(new Error('Use --restore or --comment, not both.'), {
|
|
342
|
+
code: 'INVALID_OPERATION'
|
|
343
|
+
});
|
|
344
|
+
}
|
|
325
345
|
if (hasInlinePatch) {
|
|
326
346
|
throw Object.assign(new Error('Inline comments cannot be combined with --find/--replace.'), {
|
|
327
347
|
code: 'INVALID_OPERATION'
|
|
@@ -364,7 +384,7 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
364
384
|
speculativeContext = resolution.context;
|
|
365
385
|
}
|
|
366
386
|
op = {
|
|
367
|
-
type: 'redline',
|
|
387
|
+
type: inlineRestore ? 'restore' : 'redline',
|
|
368
388
|
...(target == null ? {} : { target }),
|
|
369
389
|
...(targetRef == null ? {} : { targetRef }),
|
|
370
390
|
replacements: [{
|
|
@@ -376,6 +396,14 @@ async function readOperations(file, flags = {}, stdin = process.stdin, document
|
|
|
376
396
|
...(flags.author ? { author: String(flags.author) } : {}),
|
|
377
397
|
...(flags.existingRevisions ? { existingRevisions: String(flags.existingRevisions) } : {})
|
|
378
398
|
};
|
|
399
|
+
} else if (inlineRestore) {
|
|
400
|
+
op = {
|
|
401
|
+
type: 'restore',
|
|
402
|
+
target,
|
|
403
|
+
...(flags.modified === undefined ? {} : { modified: String(flags.modified) }),
|
|
404
|
+
...(flags.author ? { author: String(flags.author) } : {}),
|
|
405
|
+
...(flags.existingRevisions ? { existingRevisions: String(flags.existingRevisions) } : {})
|
|
406
|
+
};
|
|
379
407
|
} else {
|
|
380
408
|
op = {
|
|
381
409
|
type: 'replace',
|
|
@@ -430,8 +458,14 @@ function compactExtractParagraph(paragraph) {
|
|
|
430
458
|
const {
|
|
431
459
|
humanReference, provision, nearestHeading, index, ref, paragraphId,
|
|
432
460
|
fingerprint, revisionView, exactText, inTable, list, selectionRole,
|
|
433
|
-
contextFor
|
|
461
|
+
contextFor, hasRevisions, revisionAuthors, segments
|
|
434
462
|
} = paragraph;
|
|
463
|
+
const hiddenRevisionContent = exactText.length === 0 && Array.isArray(segments)
|
|
464
|
+
&& segments.some(segment => segment.text.length > 0 && (
|
|
465
|
+
revisionView === 'rejected'
|
|
466
|
+
? segment.acceptedStart !== null
|
|
467
|
+
: segment.rejectedStart !== null
|
|
468
|
+
));
|
|
435
469
|
return {
|
|
436
470
|
humanReference,
|
|
437
471
|
provision,
|
|
@@ -444,6 +478,12 @@ function compactExtractParagraph(paragraph) {
|
|
|
444
478
|
exactText,
|
|
445
479
|
inTable,
|
|
446
480
|
list,
|
|
481
|
+
...(hasRevisions ? {
|
|
482
|
+
hasRevisions: true,
|
|
483
|
+
revisionAuthors,
|
|
484
|
+
...(hiddenRevisionContent && revisionView === 'accepted' ? { deleted: true } : {}),
|
|
485
|
+
...(hiddenRevisionContent && revisionView === 'rejected' ? { inserted: true } : {})
|
|
486
|
+
} : {}),
|
|
447
487
|
...(selectionRole ? { selectionRole } : {}),
|
|
448
488
|
...(Array.isArray(contextFor) ? { contextFor } : {})
|
|
449
489
|
};
|
package/package.json
CHANGED
|
@@ -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,
|
|
@@ -223,7 +223,37 @@ function describeTargetTextMatch(actualText, requestedText) {
|
|
|
223
223
|
requestedExcerpt: escapeInvisibleText(requested)
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
|
-
|
|
226
|
+
|
|
227
|
+
function resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options) {
|
|
228
|
+
const revisionView = options?.targetDescriptor?.revisionView === 'rejected'
|
|
229
|
+
? 'rejected'
|
|
230
|
+
: 'accepted';
|
|
231
|
+
const viewMetadata = metadata?.revisionView === revisionView ? metadata : null;
|
|
232
|
+
const index = viewMetadata?.index ?? getDocumentParagraphNodes(xmlDoc).indexOf(paragraph) + 1;
|
|
233
|
+
const paragraphId = viewMetadata?.paragraphId ?? getParagraphId(paragraph);
|
|
234
|
+
const inTable = viewMetadata?.inTable ?? !!findContainingWordElement(paragraph, 'tbl');
|
|
235
|
+
const text = viewMetadata?.text ?? extractCanonicalParagraphText(paragraph, { revisionView });
|
|
236
|
+
const fingerprint = viewMetadata?.fingerprint ?? createParagraphFingerprint(paragraph, {
|
|
237
|
+
text,
|
|
238
|
+
index,
|
|
239
|
+
paragraphId,
|
|
240
|
+
inTable,
|
|
241
|
+
revisionView
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
index,
|
|
245
|
+
paragraphId,
|
|
246
|
+
text,
|
|
247
|
+
fingerprint,
|
|
248
|
+
revisionView,
|
|
249
|
+
inTable,
|
|
250
|
+
targetTextMatch: describeTargetTextMatch(
|
|
251
|
+
text,
|
|
252
|
+
options?.targetDescriptor?.text ?? options?.targetDescriptor?.exactText ?? targetText
|
|
253
|
+
)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
227
257
|
function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeContext = null, options = {}) {
|
|
228
258
|
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
229
259
|
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
@@ -239,17 +269,7 @@ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeCo
|
|
|
239
269
|
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
240
270
|
Object.assign(options._resolutionCapture, {
|
|
241
271
|
resolvedBy: resolved.resolvedBy,
|
|
242
|
-
resolvedTarget:
|
|
243
|
-
index: metadata?.index ?? getDocumentParagraphNodes(xmlDoc).indexOf(paragraph) + 1,
|
|
244
|
-
paragraphId: metadata?.paragraphId ?? getParagraphId(paragraph),
|
|
245
|
-
text: metadata?.text ?? getParagraphText(paragraph),
|
|
246
|
-
fingerprint: metadata?.fingerprint ?? createParagraphFingerprint(paragraph),
|
|
247
|
-
inTable: metadata?.inTable ?? !!findContainingWordElement(paragraph, 'tbl'),
|
|
248
|
-
targetTextMatch: describeTargetTextMatch(
|
|
249
|
-
metadata?.text ?? getParagraphText(paragraph),
|
|
250
|
-
options?.targetDescriptor?.exactText ?? targetText
|
|
251
|
-
)
|
|
252
|
-
}
|
|
272
|
+
resolvedTarget: resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options)
|
|
253
273
|
});
|
|
254
274
|
}
|
|
255
275
|
return resolved;
|
|
@@ -263,17 +283,7 @@ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeCo
|
|
|
263
283
|
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
264
284
|
Object.assign(options._resolutionCapture, {
|
|
265
285
|
resolvedBy: resolved.resolvedBy,
|
|
266
|
-
resolvedTarget:
|
|
267
|
-
index: metadata?.index ?? Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'p')).indexOf(paragraph) + 1,
|
|
268
|
-
paragraphId: metadata?.paragraphId ?? getParagraphId(paragraph),
|
|
269
|
-
text: metadata?.text ?? getParagraphText(paragraph),
|
|
270
|
-
fingerprint: metadata?.fingerprint ?? createParagraphFingerprint(paragraph),
|
|
271
|
-
inTable: metadata?.inTable ?? !!findContainingWordElement(paragraph, 'tbl'),
|
|
272
|
-
targetTextMatch: describeTargetTextMatch(
|
|
273
|
-
metadata?.text ?? getParagraphText(paragraph),
|
|
274
|
-
options?.targetDescriptor?.exactText ?? targetText
|
|
275
|
-
)
|
|
276
|
-
}
|
|
286
|
+
resolvedTarget: resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options)
|
|
277
287
|
});
|
|
278
288
|
}
|
|
279
289
|
return resolved;
|
|
@@ -301,19 +311,9 @@ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeCo
|
|
|
301
311
|
if (options?._resolutionCapture && resolved?.paragraph) {
|
|
302
312
|
const paragraph = resolved.paragraph;
|
|
303
313
|
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
304
|
-
Object.assign(options._resolutionCapture, {
|
|
305
|
-
resolvedBy: resolved.resolvedBy,
|
|
306
|
-
resolvedTarget:
|
|
307
|
-
index: metadata?.index ?? Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'p')).indexOf(paragraph) + 1,
|
|
308
|
-
paragraphId: metadata?.paragraphId ?? getParagraphId(paragraph),
|
|
309
|
-
text: metadata?.text ?? getParagraphText(paragraph),
|
|
310
|
-
fingerprint: metadata?.fingerprint ?? createParagraphFingerprint(paragraph),
|
|
311
|
-
inTable: metadata?.inTable ?? !!findContainingWordElement(paragraph, 'tbl'),
|
|
312
|
-
targetTextMatch: describeTargetTextMatch(
|
|
313
|
-
metadata?.text ?? getParagraphText(paragraph),
|
|
314
|
-
options?.targetDescriptor?.exactText ?? targetText
|
|
315
|
-
)
|
|
316
|
-
}
|
|
314
|
+
Object.assign(options._resolutionCapture, {
|
|
315
|
+
resolvedBy: resolved.resolvedBy,
|
|
316
|
+
resolvedTarget: resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options)
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
319
|
return resolved;
|