@ansonlai/docx-redline-js 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +82 -667
- package/ARCHITECTURE.md +51 -4
- package/CHANGELOG.md +11 -0
- package/README.md +176 -39
- package/core/paragraph-revision-safety.js +10 -8
- package/core/paragraph-targeting.js +14 -2
- package/core/redline-validation.js +7 -4
- package/core/revision-cloning.js +21 -0
- package/core/validation-delta.js +23 -0
- package/dist/docx-redline-js.esm.js +275 -45
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +82 -82
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/AGENT_FAST_START.md +59 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +868 -0
- package/docs/TESTING.md +20 -1
- package/docs/schemas/document-operations.schema.json +16 -2
- package/docs/validation-reports/2026-09-12-agent-protocol-rollout.md +82 -0
- package/engine/oxml-engine.js +80 -13
- package/engine/run-builders.js +5 -15
- package/engine/surgical-mode.js +148 -3
- package/engine/surgical-run-splitting.js +19 -7
- package/engine/surgical-spans.js +2 -1
- package/index.d.ts +17 -1
- package/node/cli.js +235 -36
- package/node/docx-document.js +137 -83
- package/node/index.d.ts +6 -2
- package/package.json +10 -3
- package/pipeline/diff-engine.js +15 -0
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +25 -25
- package/services/batch-operation-orchestrator.js +215 -120
- package/services/document-inspection.js +5 -3
- package/services/document-operation-applier.js +99 -36
- package/services/document-operation-contract.js +50 -6
- package/services/document-operation-mutations.js +404 -41
- package/services/document-operation-session.js +4 -0
- package/services/error-recovery.js +174 -0
- package/services/operation-batch-compiler.js +394 -0
- package/services/operation-preflight.js +91 -72
- package/services/standalone-operation-runner.d.ts +35 -1
- package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +0 -1669
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +0 -856
- package/docs/plans/completed/2026-03-01-release-0.1.4-design.md +0 -33
- package/docs/plans/completed/2026-03-01-release-0.1.4.md +0 -110
- package/docs/plans/completed/2026-05-31-architectural changes.md +0 -593
- package/docs/plans/completed/2026-08-02-reliability-improvements.md +0 -1155
- package/docs/plans/completed/2026-08-30-reliability-testing-improvements.md +0 -488
- package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +0 -669
- package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +0 -427
- package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +0 -519
- package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +0 -69
- package/docs/plans/completed/structural-revision-capability-matrix.md +0 -115
- package/docs/test-comparison-dashboard.html +0 -4338
- package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +0 -22
- package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +0 -24
- package/docs/validation-reports/2026-08-30-phase-3-coverage.md +0 -73
- package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +0 -82
- package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +0 -114
- package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +0 -79
package/docs/TESTING.md
CHANGED
|
@@ -22,7 +22,10 @@ fixtures rarely contain.
|
|
|
22
22
|
| Multimodal LLM visual spot check | On-demand / sampled | Evaluates rendered real-document pages with vision models for layout, table alignment, and typography regressions | Full-corpus automated coverage (intentionally decoupled and sampled due to cost/time) |
|
|
23
23
|
| XSD and LibreOffice | See [Release validation and independent oracles](#release-validation-and-independent-oracles) | Schema conformance and acceptance by a second consumer | Word-specific revision semantics |
|
|
24
24
|
| Agent inspection and package facade | `node tests/document_inspection_tests.mjs`, `node tests/docx_package_facade_tests.mjs` | Canonical text, comment/list resolution, package-scoped IDs, untouched-part preservation, and atomic rollback | Desktop Word rendering |
|
|
25
|
-
| Agent CLI | `node tests/agent_cli_tests.mjs` | JSON contracts, exact-text extraction,
|
|
25
|
+
| Agent CLI | `node tests/agent_cli_tests.mjs`, `node tests/agent_cli_protocol_tests.mjs` | JSON contracts, exact-text extraction, stdin operations, named agent-profile defaults, UTF-8 fidelity, complete-success exits, source safety, all command families, and operation-schema readability | Cross-platform CI beyond the current runner |
|
|
26
|
+
| Example agent session | `node tests/agent_session_example_tests.mjs`, `node tests/localized_edit_example_tests.mjs` | A non-package sample wrapper delegates to the Node facade, binds and refreshes opaque handles across package revisions, returns context windows, compiles exact localized edits, preserves accepted/rejected lifecycle and atomic rollback, and supports review resolution | LLM/provider latency or a production tool-server transport |
|
|
27
|
+
| Batch compilation and recovery | `node tests/batch_source_binding_tests.mjs`, `node tests/error_recovery_contract_tests.mjs`, `node tests/capture_dependency_graph_tests.mjs` | Batch-start source identity survives structural index drift and savepoint restoration; true overlap and capture fan-out fail before mutation; recovery envelopes, retry bases, bounded CLI diagnostics, and complete-success exits remain stable | Whether a model follows the returned recovery action correctly |
|
|
28
|
+
| Agent documentation contract | `node tests/agent_documentation_contract_tests.mjs` | The ordinary fast start stays within its 40–60 line/600-word budget, retains required safety contracts, and the launch card routes to detailed sources while the development wrapper stays outside package files | Whether a particular model reads or follows the instructions |
|
|
26
29
|
| Agent edge cases | `node tests/canonical_paragraph_text_tests.mjs`, `node tests/document_inspection_edge_tests.mjs`, `node tests/docx_package_transaction_edge_tests.mjs`, `node tests/node_zip_archive_tests.mjs`, `node tests/agent_cli_edge_tests.mjs` | Revision-view semantics, cross-paragraph anchors, nested numbering, transaction reuse, multi-author cleanup, malformed ZIP handling, and destructive CLI safeguards | Desktop Word rendering and non-Windows CI |
|
|
27
30
|
| Performance boundary regression | `node tests/performance_phase2_boundary_tests.mjs` | Stable facade re-exports, leaf imports, session rollback, isolated context commit, and comment-first scheduling | The Phase 1 one-parse/one-serialize performance target |
|
|
28
31
|
| Live-session accuracy and instrumentation | `node tests/performance_phase1_session_tests.mjs` | One full parse/serialization, sequential semantic equivalence, exact accepted/rejected text, valid revisions, list/table/comment/highlight preservation, savepoint no-ops, and zero-serialization rollback | Desktop Word rendering |
|
|
@@ -79,6 +82,22 @@ not correctness gates. Per-operation DOM savepoints are retained because
|
|
|
79
82
|
redline accuracy, no-op isolation, and rollback fidelity take precedence over
|
|
80
83
|
the aspirational speed target.
|
|
81
84
|
|
|
85
|
+
Run the observational agent-protocol benchmark with:
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
npm run benchmark:agent
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It writes `tmp/benchmarks/agent-workflow-latest.json` and compares canonical
|
|
92
|
+
stateless Node, legacy file-based CLI, compact stdin/profile CLI, and the
|
|
93
|
+
development-only session example. It records native/in-process CLI time, heap,
|
|
94
|
+
serialized request bytes, protocol calls, instruction words/bytes, exact
|
|
95
|
+
accepted/rejected text, comment preservation, ordering, recovery, and
|
|
96
|
+
cross-author attribution. It does not measure or estimate LLM reasoning, exact
|
|
97
|
+
tokens, tool transport, Claude, or OpenCode wall time; collect those separately
|
|
98
|
+
in the calling harness. The checked WP-07 results are summarized in the
|
|
99
|
+
[agent protocol rollout audit](validation-reports/2026-09-12-agent-protocol-rollout.md).
|
|
100
|
+
|
|
82
101
|
## Cross-author revision slicing test suite
|
|
83
102
|
|
|
84
103
|
The cross-author revision slicing subsystem introduces six complementary test lanes:
|
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
"additionalProperties": false
|
|
29
29
|
},
|
|
30
30
|
"operationArray": { "type": "array", "items": { "$ref": "#/$defs/operation" } },
|
|
31
|
+
"rejectedTextInsertionAnchor": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"required": ["exactText", "offset"],
|
|
34
|
+
"properties": {
|
|
35
|
+
"exactText": { "type": "string", "minLength": 1 },
|
|
36
|
+
"occurrence": { "type": "integer", "minimum": 1 },
|
|
37
|
+
"offset": { "type": "integer", "minimum": 0 }
|
|
38
|
+
},
|
|
39
|
+
"additionalProperties": false
|
|
40
|
+
},
|
|
31
41
|
"target": {
|
|
32
42
|
"oneOf": [
|
|
33
43
|
{ "type": "string", "minLength": 1 },
|
|
@@ -38,7 +48,11 @@
|
|
|
38
48
|
"index": { "type": ["integer", "string"] }, "paragraphIndex": { "type": ["integer", "string"] },
|
|
39
49
|
"paragraphId": { "type": "string" }, "occurrence": { "type": "integer", "minimum": 1 },
|
|
40
50
|
"inTable": { "type": "boolean" }, "fingerprint": { "type": "string" }, "sourceFingerprint": { "type": "string" },
|
|
41
|
-
"revisionView": {
|
|
51
|
+
"revisionView": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"enum": ["accepted", "rejected"],
|
|
54
|
+
"description": "Defaults to rejected for restore operations and accepted for other operation types."
|
|
55
|
+
},
|
|
42
56
|
"captureRef": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
43
57
|
"select": { "type": "string", "minLength": 1 }
|
|
44
58
|
},
|
|
@@ -86,7 +100,7 @@
|
|
|
86
100
|
"additionalProperties": false
|
|
87
101
|
},
|
|
88
102
|
{
|
|
89
|
-
"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"] } } } ]
|
|
103
|
+
"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" } } } ]
|
|
90
104
|
},
|
|
91
105
|
{
|
|
92
106
|
"allOf": [ { "$ref": "#/$defs/base" }, { "type": "object", "required": ["modified"], "properties": { "type": { "const": "restore" }, "modified": { "oneOf": [ { "type": "string", "minLength": 1 }, { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } } ] }, "targetEnd": { "$ref": "#/$defs/target" }, "targetEndRef": { "type": ["integer", "string", "null"] } } } ]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Agent Protocol Rollout Audit
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-12
|
|
4
|
+
**Scope:** WP-06 shell/documentation work and WP-07 repository-level rollout
|
|
5
|
+
**Benchmark:** `npm run benchmark:agent` on Node v24.11.1 / Windows x64,
|
|
6
|
+
7 measured iterations after 2 warmups
|
|
7
|
+
|
|
8
|
+
## Outcome
|
|
9
|
+
|
|
10
|
+
The compact shell path removes the operations-file tool turn, and the
|
|
11
|
+
development session example removes repeated target/policy payload. All measured
|
|
12
|
+
paths produced the requested accepted view, restored the exact source text in
|
|
13
|
+
the rejected view, passed package/revision validation, and preserved comment
|
|
14
|
+
content. These are protocol and native-runtime measurements; no LLM/provider
|
|
15
|
+
latency or token count was estimated.
|
|
16
|
+
|
|
17
|
+
| Measure | Legacy | WP-06/07 result |
|
|
18
|
+
|---|---:|---:|
|
|
19
|
+
| Ordinary agent instructions | 2,023 words | 349 words (82.75% reduction) |
|
|
20
|
+
| Repository launch card | 2,023 words | 527 words |
|
|
21
|
+
| Shell workflow calls | extract + file write + apply (3) | extract + stdin apply (2; 33.33% reduction) |
|
|
22
|
+
| Independent early-split permutations | one previously order-sensitive direction | both directions succeed |
|
|
23
|
+
|
|
24
|
+
## Measured workflow results
|
|
25
|
+
|
|
26
|
+
Median wall time includes the in-process CLI file/stdin transport for CLI rows.
|
|
27
|
+
It does not include process startup, a model, provider tokenization, or tool
|
|
28
|
+
transport.
|
|
29
|
+
|
|
30
|
+
| Case | Legacy CLI | Compact stdin CLI | Session example | Session request bytes vs legacy |
|
|
31
|
+
|---|---:|---:|---:|---:|
|
|
32
|
+
| Terminal punctuation | 49.63 ms | 46.05 ms | 49.50 ms | 125 vs 255 (50.98% fewer) |
|
|
33
|
+
| Term-duration phrase | 55.50 ms | 51.30 ms | 56.05 ms | 128 vs 630 (79.68% fewer) |
|
|
34
|
+
| Simple mutuality | 62.82 ms | 50.22 ms | 58.45 ms | 281 vs 935 (69.95% fewer) |
|
|
35
|
+
| Full-clause rewrite | 50.55 ms | 50.31 ms | 53.81 ms | 223 vs 407 (45.21% fewer) |
|
|
36
|
+
| Comment + redline | 71.94 ms | 62.11 ms | 63.81 ms | 290 vs 724 (59.94% fewer) |
|
|
37
|
+
|
|
38
|
+
Compared with the canonical stateless Node request envelope, localized session
|
|
39
|
+
requests remained 86.11% smaller for punctuation, 89.96% smaller for a duration
|
|
40
|
+
change, and 82.22% smaller for deterministic mutuality. A complete clause
|
|
41
|
+
rewrite is naturally less compressible and is not presented as a localized-edit
|
|
42
|
+
win.
|
|
43
|
+
|
|
44
|
+
## Fidelity and recovery audit
|
|
45
|
+
|
|
46
|
+
- The benchmark validates accepted and rejected paragraph text for every text
|
|
47
|
+
edit and verifies the mixed-batch comment definition remains present.
|
|
48
|
+
- Both ten-edit permutations around an early paragraph split complete without
|
|
49
|
+
target errors.
|
|
50
|
+
- An ambiguous target returns two candidates without auto-selection.
|
|
51
|
+
- A stale session handle returns `STALE_TARGET_HANDLE` with recovery action
|
|
52
|
+
`reinspect`.
|
|
53
|
+
- The default cross-author policy refuses the edit with `EXISTING_REVISIONS`.
|
|
54
|
+
`slice-cross-author` succeeds while retaining both original authors (Anson
|
|
55
|
+
Lai and John Doe), and rejecting the benchmark author's work still retains
|
|
56
|
+
those authors.
|
|
57
|
+
- The CLI protocol regression proves UTF-8 stdin transport, package-revision
|
|
58
|
+
enforcement, source immutability, agent-profile defaults, atomic rollback,
|
|
59
|
+
progressive exit code 3, and rejected-view restoration.
|
|
60
|
+
|
|
61
|
+
## Configuration decision
|
|
62
|
+
|
|
63
|
+
No project or auto-discovered configuration file was added. The CLI already
|
|
64
|
+
defaults author, strict targeting, validation, tracked changes, revision safety,
|
|
65
|
+
and output naming. The explicit `--profile agent` flag captures the remaining
|
|
66
|
+
atomic/complete-success policy and reports `effectiveOptions`. Removing that one
|
|
67
|
+
flag would not justify hidden configuration discovery or precedence reasoning.
|
|
68
|
+
|
|
69
|
+
## Remaining external measurement
|
|
70
|
+
|
|
71
|
+
Claude/OpenCode wall time, prompt tokens, generated tokens, and provider tool
|
|
72
|
+
latency must be captured by those calling harnesses. The repository benchmark
|
|
73
|
+
records bytes, words, calls, native time, failure behavior, and document
|
|
74
|
+
fidelity so those external observations can be compared without conflating the
|
|
75
|
+
library with model reasoning.
|
|
76
|
+
|
|
77
|
+
## Automated verification
|
|
78
|
+
|
|
79
|
+
- `npm test`: 107 passed, 0 failed.
|
|
80
|
+
- `npm run check:types`: 123 runtime exports have declarations.
|
|
81
|
+
- `npm run test:isolation`: core dependency and Word-API isolation passed.
|
|
82
|
+
- Focused ESLint for all WP-06/07 source, benchmark, and test files passed.
|
package/engine/oxml-engine.js
CHANGED
|
@@ -28,7 +28,8 @@ import { applySurgicalMode } from './surgical-mode.js';
|
|
|
28
28
|
import { applyReconstructionMode } from './reconstruction-mode.js';
|
|
29
29
|
import { applyTableReconciliation, applyTextToTableTransformation } from './table-mode.js';
|
|
30
30
|
import { getDefaultAuthor } from '../adapters/config.js';
|
|
31
|
-
import { containsTrackedChanges, getTrackedChangeAuthors, withOoxmlSourceType } from '../core/word-xml.js';
|
|
31
|
+
import { containsTrackedChanges, createWordElement, getTrackedChangeAuthors, withOoxmlSourceType } from '../core/word-xml.js';
|
|
32
|
+
import { clonePropertiesWithoutRevisionHistory } from '../core/revision-cloning.js';
|
|
32
33
|
import {
|
|
33
34
|
NS_W,
|
|
34
35
|
RevisionIdAllocator,
|
|
@@ -52,6 +53,62 @@ function getCommentIdsInOoxml(node) {
|
|
|
52
53
|
}
|
|
53
54
|
return [...ids].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b));
|
|
54
55
|
}
|
|
56
|
+
|
|
57
|
+
function directWordChild(node, localName) {
|
|
58
|
+
return Array.from(node?.childNodes || []).find(child => (
|
|
59
|
+
child?.nodeType === 1
|
|
60
|
+
&& child.namespaceURI === NS_W
|
|
61
|
+
&& child.localName === localName
|
|
62
|
+
)) || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function insertedParagraphMarkMetadata(paragraph, author) {
|
|
66
|
+
const marker = directWordChild(directWordChild(directWordChild(paragraph, 'pPr'), 'rPr'), 'ins');
|
|
67
|
+
if (!marker) return null;
|
|
68
|
+
const markerAuthor = marker.getAttribute('w:author') || marker.getAttributeNS(NS_W, 'author') || '';
|
|
69
|
+
if (markerAuthor.trim().toLowerCase() !== String(author || '').trim().toLowerCase()) return null;
|
|
70
|
+
return {
|
|
71
|
+
id: marker.getAttribute('w:id') || marker.getAttributeNS(NS_W, 'id') || '',
|
|
72
|
+
author: markerAuthor,
|
|
73
|
+
date: marker.getAttribute('w:date') || marker.getAttributeNS(NS_W, 'date') || ''
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function emptyParagraphBaseline(paragraph, serializer) {
|
|
78
|
+
const clone = paragraph.cloneNode(false);
|
|
79
|
+
const pPr = directWordChild(paragraph, 'pPr');
|
|
80
|
+
if (pPr) clone.appendChild(clonePropertiesWithoutRevisionHistory(pPr));
|
|
81
|
+
return serializer.serializeToString(clone);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function restoreInsertedParagraphMark(oxml, metadata) {
|
|
85
|
+
if (!metadata || typeof oxml !== 'string' || !oxml.trim()) return oxml;
|
|
86
|
+
const parsed = parseOoxmlSafe(oxml, 'text/xml');
|
|
87
|
+
if (!parsed.doc || parsed.error) return oxml;
|
|
88
|
+
const paragraph = parsed.doc.documentElement?.localName === 'p'
|
|
89
|
+
? parsed.doc.documentElement
|
|
90
|
+
: getDocumentParagraphs(parsed.doc)[0];
|
|
91
|
+
if (!paragraph) return oxml;
|
|
92
|
+
|
|
93
|
+
let pPr = directWordChild(paragraph, 'pPr');
|
|
94
|
+
if (!pPr) {
|
|
95
|
+
pPr = createWordElement(parsed.doc, 'w:pPr');
|
|
96
|
+
paragraph.insertBefore(pPr, paragraph.firstChild);
|
|
97
|
+
}
|
|
98
|
+
let rPr = directWordChild(pPr, 'rPr');
|
|
99
|
+
if (!rPr) {
|
|
100
|
+
rPr = createWordElement(parsed.doc, 'w:rPr');
|
|
101
|
+
pPr.appendChild(rPr);
|
|
102
|
+
}
|
|
103
|
+
if (!directWordChild(rPr, 'ins')) {
|
|
104
|
+
const marker = createWordElement(parsed.doc, 'w:ins');
|
|
105
|
+
if (metadata.id) marker.setAttribute('w:id', metadata.id);
|
|
106
|
+
marker.setAttribute('w:author', metadata.author);
|
|
107
|
+
if (metadata.date) marker.setAttribute('w:date', metadata.date);
|
|
108
|
+
rPr.appendChild(marker);
|
|
109
|
+
}
|
|
110
|
+
return serializeXml(parsed.doc);
|
|
111
|
+
}
|
|
55
112
|
|
|
56
113
|
/**
|
|
57
114
|
* Applies redline track changes to OOXML by modifying the DOM in-place.
|
|
@@ -77,11 +134,12 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
77
134
|
const serializer = createSerializer();
|
|
78
135
|
let parseWarnings = [];
|
|
79
136
|
const operationWarnings = [];
|
|
80
|
-
let normalizedExistingRevisions = false;
|
|
137
|
+
let normalizedExistingRevisions = false;
|
|
138
|
+
let preservedInsertedParagraphMark = null;
|
|
81
139
|
const existingRevisionsPolicy = options.existingRevisions || 'merge-same-author';
|
|
82
140
|
const keepNormalizedNoOp = existingRevisionsPolicy === 'accept-all-first-keep-normalized';
|
|
83
|
-
const finalize = result => {
|
|
84
|
-
const withStatus = { ...result };
|
|
141
|
+
const finalize = result => {
|
|
142
|
+
const withStatus = { ...result };
|
|
85
143
|
if (normalizedExistingRevisions && withStatus.hasChanges === false && withStatus.status !== 'error') {
|
|
86
144
|
if (existingRevisionsPolicy === 'merge-same-author' || existingRevisionsPolicy === 'slice-cross-author') {
|
|
87
145
|
withStatus.oxml = workingOoxml;
|
|
@@ -105,10 +163,13 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
105
163
|
if (warnings.length > 0) {
|
|
106
164
|
withStatus.warnings = [...new Set(warnings)];
|
|
107
165
|
}
|
|
108
|
-
if (!withStatus.status) {
|
|
109
|
-
withStatus.status = withStatus.hasChanges ? 'ok' : 'no-op';
|
|
110
|
-
}
|
|
111
|
-
|
|
166
|
+
if (!withStatus.status) {
|
|
167
|
+
withStatus.status = withStatus.hasChanges ? 'ok' : 'no-op';
|
|
168
|
+
}
|
|
169
|
+
if (preservedInsertedParagraphMark && withStatus.hasChanges && typeof withStatus.oxml === 'string') {
|
|
170
|
+
withStatus.oxml = restoreInsertedParagraphMark(withStatus.oxml, preservedInsertedParagraphMark);
|
|
171
|
+
}
|
|
172
|
+
return withOoxmlSourceType(withStatus);
|
|
112
173
|
};
|
|
113
174
|
const finalizeUnchanged = () => {
|
|
114
175
|
if (normalizedExistingRevisions && keepNormalizedNoOp) {
|
|
@@ -181,9 +242,13 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
181
242
|
});
|
|
182
243
|
}
|
|
183
244
|
log('[OxmlEngine] Existing revisions from same author detected; rejecting previous changes to merge against baseline');
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
245
|
+
const soleParagraph = inputParagraphs.length === 1 ? inputParagraphs[0] : null;
|
|
246
|
+
preservedInsertedParagraphMark = insertedParagraphMarkMetadata(soleParagraph, author);
|
|
247
|
+
const rejected = rejectTrackedChangesInOoxml(inputOoxml, { author });
|
|
248
|
+
if (rejected.status === 'error') return finalize(rejected);
|
|
249
|
+
workingOoxml = preservedInsertedParagraphMark && !String(rejected.oxml || '').trim()
|
|
250
|
+
? emptyParagraphBaseline(soleParagraph, serializer)
|
|
251
|
+
: rejected.oxml;
|
|
187
252
|
normalizedExistingRevisions = true;
|
|
188
253
|
const rejectedParsed = parseOoxmlSafe(workingOoxml, 'text/xml');
|
|
189
254
|
parseWarnings.push(...rejectedParsed.warnings);
|
|
@@ -230,8 +295,10 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
230
295
|
hasChanges: false,
|
|
231
296
|
status: 'error',
|
|
232
297
|
error: {
|
|
233
|
-
code: 'EXISTING_REVISIONS',
|
|
234
|
-
message: `Input OOXML contains tracked changes from another author (${authors.length ? authors.join(', ') : 'unattributed'}).
|
|
298
|
+
code: 'EXISTING_REVISIONS',
|
|
299
|
+
message: `Input OOXML contains tracked changes from another author (${authors.length ? authors.join(', ') : 'unattributed'}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
|
|
300
|
+
revisionAuthors: authors,
|
|
301
|
+
currentPolicy: existingRevisionsPolicy
|
|
235
302
|
}
|
|
236
303
|
});
|
|
237
304
|
} else {
|
package/engine/run-builders.js
CHANGED
|
@@ -9,6 +9,7 @@ import { extractFormatFromRPr, RPR_SCHEMA_ORDER } from './rpr-helpers.js';
|
|
|
9
9
|
import { createRevisionMetadata } from '../core/types.js';
|
|
10
10
|
import { getFirstElementByTag } from '../core/xml-query.js';
|
|
11
11
|
import { createWordElement } from '../core/word-xml.js';
|
|
12
|
+
import { clonePropertiesWithoutRevisionHistory } from '../core/revision-cloning.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Creates an insertion/deletion wrapper.
|
|
@@ -319,14 +320,9 @@ export function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceN
|
|
|
319
320
|
rPrChange.setAttribute('w:author', metadata.author);
|
|
320
321
|
rPrChange.setAttribute('w:date', dateStr || metadata.date);
|
|
321
322
|
|
|
322
|
-
const previousRPr = createWordElement(xmlDoc, 'w:rPr');
|
|
323
323
|
const source = sourceNode || rPr;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (child.nodeName !== 'w:rPrChange') {
|
|
327
|
-
previousRPr.appendChild(child.cloneNode(true));
|
|
328
|
-
}
|
|
329
|
-
});
|
|
324
|
+
const previousRPr = clonePropertiesWithoutRevisionHistory(source)
|
|
325
|
+
|| createWordElement(xmlDoc, 'w:rPr');
|
|
330
326
|
|
|
331
327
|
rPrChange.appendChild(previousRPr);
|
|
332
328
|
|
|
@@ -369,14 +365,9 @@ export function snapshotAndAttachPPrChange(xmlDoc, pPr, author, dateStr, sourceN
|
|
|
369
365
|
pPrChange.setAttribute('w:author', metadata.author);
|
|
370
366
|
pPrChange.setAttribute('w:date', dateStr || metadata.date);
|
|
371
367
|
|
|
372
|
-
const previousPPr = createWordElement(xmlDoc, 'w:pPr');
|
|
373
368
|
const source = sourceNode || pPr;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (child.nodeType === 1 && child.nodeName !== 'w:pPrChange' && child.localName !== 'pPrChange') {
|
|
377
|
-
previousPPr.appendChild(child.cloneNode(true));
|
|
378
|
-
}
|
|
379
|
-
});
|
|
369
|
+
const previousPPr = clonePropertiesWithoutRevisionHistory(source)
|
|
370
|
+
|| createWordElement(xmlDoc, 'w:pPr');
|
|
380
371
|
|
|
381
372
|
pPrChange.appendChild(previousPPr);
|
|
382
373
|
|
|
@@ -390,4 +381,3 @@ export function snapshotAndAttachPPrChange(xmlDoc, pPr, author, dateStr, sourceN
|
|
|
390
381
|
pPr.appendChild(pPrChange);
|
|
391
382
|
return pPrChange;
|
|
392
383
|
}
|
|
393
|
-
|
package/engine/surgical-mode.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getApplicableFormatHints } from '../pipeline/markdown-processor.js';
|
|
9
|
-
import { computeInsertionOnlyDiffs, computeWordDiffs } from '../pipeline/diff-engine.js';
|
|
9
|
+
import { computeCharacterDiffs, computeInsertionOnlyDiffs, computeWordDiffs } from '../pipeline/diff-engine.js';
|
|
10
10
|
import { getDocumentParagraphs } from './format-extraction.js';
|
|
11
11
|
import { buildSpanIndex, buildSurgicalTextSpans, forEachOverlappingSpan } from './surgical-spans.js';
|
|
12
12
|
import {
|
|
@@ -115,7 +115,10 @@ export function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer
|
|
|
115
115
|
const insertionOnlyDiffs = options.existingRevisions === 'slice-cross-author'
|
|
116
116
|
? computeInsertionOnlyDiffs(fullText, modifiedText)
|
|
117
117
|
: null;
|
|
118
|
-
const diffs = insertionOnlyDiffs ||
|
|
118
|
+
const diffs = insertionOnlyDiffs || refineSpaceEquivalentReplacements(
|
|
119
|
+
computeWordDiffs(fullText, modifiedText, diffOptions),
|
|
120
|
+
diffOptions
|
|
121
|
+
);
|
|
119
122
|
const spanIndex = buildSpanIndex(textSpans);
|
|
120
123
|
const pairReplacements = options.pairReplacements === true;
|
|
121
124
|
const warnings = [];
|
|
@@ -157,6 +160,84 @@ export function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer
|
|
|
157
160
|
}
|
|
158
161
|
if (insertResult === true) hasChanges = true;
|
|
159
162
|
}
|
|
163
|
+
} else if (formatHints.length === 0) {
|
|
164
|
+
const editOperations = collectTextEditOperations(diffs);
|
|
165
|
+
for (const operation of editOperations.reverse()) {
|
|
166
|
+
const liveSpanIndex = buildSpanIndex(buildSurgicalTextSpans(allParagraphs).textSpans);
|
|
167
|
+
if (operation.type === 'insert') {
|
|
168
|
+
const inserted = processInsert(
|
|
169
|
+
xmlDoc,
|
|
170
|
+
liveSpanIndex,
|
|
171
|
+
operation.start,
|
|
172
|
+
operation.text.replace(/\n/g, ' '),
|
|
173
|
+
author,
|
|
174
|
+
formatHints,
|
|
175
|
+
operation.newPos,
|
|
176
|
+
generateRedlines,
|
|
177
|
+
allParagraphs[0] || null,
|
|
178
|
+
null,
|
|
179
|
+
options?.insertionAffinity || null,
|
|
180
|
+
options?.existingRevisions || 'merge-same-author'
|
|
181
|
+
);
|
|
182
|
+
if (inserted && typeof inserted === 'object' && inserted.error) {
|
|
183
|
+
return withOoxmlSourceType({
|
|
184
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
185
|
+
hasChanges: false,
|
|
186
|
+
status: 'error',
|
|
187
|
+
error: inserted.error
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (inserted === true) hasChanges = true;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let delMetadata = null;
|
|
195
|
+
let insMetadata = null;
|
|
196
|
+
if (operation.type === 'replace' && pairReplacements && generateRedlines && operation.text.replace(/\n/g, ' ')) {
|
|
197
|
+
const checkResult = checkSafeAdjacencyForPairing(
|
|
198
|
+
liveSpanIndex,
|
|
199
|
+
operation.start,
|
|
200
|
+
operation.end,
|
|
201
|
+
options?.existingRevisions === 'slice-cross-author'
|
|
202
|
+
);
|
|
203
|
+
if (checkResult.safe) {
|
|
204
|
+
const event = createReplacementRevisionEvent(author, xmlDoc);
|
|
205
|
+
delMetadata = { id: event.deletionId, author: event.author, date: event.date };
|
|
206
|
+
insMetadata = { id: event.insertionId, author: event.author, date: event.date };
|
|
207
|
+
} else if (checkResult.structuralBoundary) {
|
|
208
|
+
warnings.push('PAIRING_SKIPPED_STRUCTURAL_BOUNDARY');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (processDelete(xmlDoc, liveSpanIndex, operation.start, operation.end, author, generateRedlines, delMetadata)) {
|
|
213
|
+
hasChanges = true;
|
|
214
|
+
}
|
|
215
|
+
if (operation.type === 'replace') {
|
|
216
|
+
const inserted = processInsert(
|
|
217
|
+
xmlDoc,
|
|
218
|
+
liveSpanIndex,
|
|
219
|
+
operation.end,
|
|
220
|
+
operation.text.replace(/\n/g, ' '),
|
|
221
|
+
author,
|
|
222
|
+
formatHints,
|
|
223
|
+
operation.newPos,
|
|
224
|
+
generateRedlines,
|
|
225
|
+
allParagraphs[0] || null,
|
|
226
|
+
insMetadata,
|
|
227
|
+
options?.insertionAffinity || null,
|
|
228
|
+
options?.existingRevisions || 'merge-same-author'
|
|
229
|
+
);
|
|
230
|
+
if (inserted && typeof inserted === 'object' && inserted.error) {
|
|
231
|
+
return withOoxmlSourceType({
|
|
232
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
233
|
+
hasChanges: false,
|
|
234
|
+
status: 'error',
|
|
235
|
+
error: inserted.error
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (inserted === true) hasChanges = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
160
241
|
} else {
|
|
161
242
|
|
|
162
243
|
for (let i = 0; i < diffs.length; i++) {
|
|
@@ -267,7 +348,9 @@ export function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer
|
|
|
267
348
|
message: 'Generated OOXML accepted-view text does not match the requested modified text; the mutation was rejected.',
|
|
268
349
|
mismatchOffset,
|
|
269
350
|
expectedExcerpt: excerptAt(expectedText, mismatchOffset),
|
|
270
|
-
actualExcerpt: excerptAt(actualText, mismatchOffset)
|
|
351
|
+
actualExcerpt: excerptAt(actualText, mismatchOffset),
|
|
352
|
+
expectedCodePoint: codePointAtOffset(expectedText, mismatchOffset),
|
|
353
|
+
actualCodePoint: codePointAtOffset(actualText, mismatchOffset)
|
|
271
354
|
},
|
|
272
355
|
...(warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {})
|
|
273
356
|
});
|
|
@@ -294,6 +377,68 @@ function excerptAt(text, offset, radius = 40) {
|
|
|
294
377
|
return text.slice(start, end);
|
|
295
378
|
}
|
|
296
379
|
|
|
380
|
+
function codePointAtOffset(text, offset) {
|
|
381
|
+
if (offset >= text.length) return 'END';
|
|
382
|
+
return `U+${text.codePointAt(offset).toString(16).toUpperCase().padStart(4, '0')}`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function refineSpaceEquivalentReplacements(diffs, diffOptions) {
|
|
386
|
+
const refined = [];
|
|
387
|
+
for (let index = 0; index < diffs.length; index++) {
|
|
388
|
+
const [op, text] = diffs[index];
|
|
389
|
+
const next = diffs[index + 1];
|
|
390
|
+
if (
|
|
391
|
+
op === -1
|
|
392
|
+
&& next?.[0] === 1
|
|
393
|
+
&& text !== next[1]
|
|
394
|
+
&& text.length === next[1].length
|
|
395
|
+
&& text.replace(/\u00a0/g, ' ') === next[1].replace(/\u00a0/g, ' ')
|
|
396
|
+
) {
|
|
397
|
+
refined.push(...computeCharacterDiffs(text, next[1], diffOptions));
|
|
398
|
+
index++;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
refined.push([op, text]);
|
|
402
|
+
}
|
|
403
|
+
return refined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function collectTextEditOperations(diffs) {
|
|
407
|
+
const operations = [];
|
|
408
|
+
let originalPos = 0;
|
|
409
|
+
let newPos = 0;
|
|
410
|
+
for (let index = 0; index < diffs.length; index++) {
|
|
411
|
+
const [op, text] = diffs[index];
|
|
412
|
+
if (op === 0) {
|
|
413
|
+
originalPos += text.length;
|
|
414
|
+
newPos += text.length;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (op === -1) {
|
|
418
|
+
const next = diffs[index + 1];
|
|
419
|
+
if (next?.[0] === 1) {
|
|
420
|
+
operations.push({
|
|
421
|
+
type: 'replace',
|
|
422
|
+
start: originalPos,
|
|
423
|
+
end: originalPos + text.length,
|
|
424
|
+
newPos,
|
|
425
|
+
text: next[1]
|
|
426
|
+
});
|
|
427
|
+
originalPos += text.length;
|
|
428
|
+
newPos += next[1].length;
|
|
429
|
+
index++;
|
|
430
|
+
} else {
|
|
431
|
+
operations.push({ type: 'delete', start: originalPos, end: originalPos + text.length, newPos, text: '' });
|
|
432
|
+
originalPos += text.length;
|
|
433
|
+
}
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
operations.push({ type: 'insert', start: originalPos, end: originalPos, newPos, text });
|
|
437
|
+
newPos += text.length;
|
|
438
|
+
}
|
|
439
|
+
return operations;
|
|
440
|
+
}
|
|
441
|
+
|
|
297
442
|
function collectInsertionOperations(diffs) {
|
|
298
443
|
const operations = [];
|
|
299
444
|
let originalPos = 0;
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { refreshRunPropertyChangeIds } from '../core/revision-cloning.js';
|
|
9
9
|
import { getRunChildText, isTextLikeRunChild } from './surgical-spans.js';
|
|
10
10
|
|
|
11
|
-
const TRACK_CHANGE_CARRIERS = new Set(['ins']);
|
|
11
|
+
const TRACK_CHANGE_CARRIERS = new Set(['ins', 'del']);
|
|
12
12
|
|
|
13
13
|
export function getRunContentPieces(runElement) {
|
|
14
14
|
const pieces = [];
|
|
@@ -72,7 +72,7 @@ export function insertRunPiecesBefore(xmlDoc, parent, referenceNode, pieces, rPr
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Splits a run-level tracked-change carrier at
|
|
75
|
+
* Splits a run-level tracked-change carrier at its visible-view character
|
|
76
76
|
* offset without mutating the source carrier. The original revision ID stays
|
|
77
77
|
* with the leading fragment; an interior trailing fragment receives a fresh,
|
|
78
78
|
* document-scoped ID while all other carrier metadata remains unchanged.
|
|
@@ -86,7 +86,7 @@ export function insertRunPiecesBefore(xmlDoc, parent, referenceNode, pieces, rPr
|
|
|
86
86
|
export function splitTrackChangeCarrier(xmlDoc, carrierElement, splitOffset, allocator = null) {
|
|
87
87
|
const carrierName = getLocalName(carrierElement);
|
|
88
88
|
if (!TRACK_CHANGE_CARRIERS.has(carrierName)) {
|
|
89
|
-
throw new TypeError('splitTrackChangeCarrier requires a w:ins carrier.');
|
|
89
|
+
throw new TypeError('splitTrackChangeCarrier requires a w:ins or w:del carrier.');
|
|
90
90
|
}
|
|
91
91
|
if (!Number.isInteger(splitOffset) || splitOffset < 0) {
|
|
92
92
|
throw new RangeError('splitOffset must be a non-negative integer.');
|
|
@@ -129,17 +129,18 @@ export function splitTrackChangeCarrier(xmlDoc, carrierElement, splitOffset, all
|
|
|
129
129
|
} else {
|
|
130
130
|
const localOffset = splitOffset - offset;
|
|
131
131
|
const rPr = Array.from(child.childNodes || []).find(node => isWordElement(node, 'rPr')) || null;
|
|
132
|
-
const
|
|
133
|
-
const
|
|
132
|
+
const asDeletedText = carrierName === 'del';
|
|
133
|
+
const leftPieces = sliceRunPieces(xmlDoc, pieces, 0, localOffset, asDeletedText);
|
|
134
|
+
const rightPieces = sliceRunPieces(xmlDoc, pieces, localOffset, runLength, asDeletedText);
|
|
134
135
|
leftCarrier.appendChild(createRunFromPieces(xmlDoc, leftPieces, rPr));
|
|
135
136
|
const rightRun = createRunFromPieces(xmlDoc, rightPieces, rPr);
|
|
136
|
-
refreshRunPropertyChangeIds(rightRun, resolveAllocator(xmlDoc, allocator));
|
|
137
137
|
rightCarrier.appendChild(rightRun);
|
|
138
138
|
}
|
|
139
139
|
offset = runEnd;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
const resolvedAllocator = resolveAllocator(xmlDoc, allocator);
|
|
143
|
+
refreshRunPropertyChangeIds(rightCarrier, resolvedAllocator);
|
|
143
144
|
const nextId = resolvedAllocator.next();
|
|
144
145
|
setWordAttribute(rightCarrier, 'id', String(nextId));
|
|
145
146
|
resolvedAllocator._receiptCollector?.recordRevision(nextId, carrierName);
|
|
@@ -166,7 +167,7 @@ function getLocalName(element) {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
function cloneRunPiece(xmlDoc, sourceNode, text, asDeletedText) {
|
|
169
|
-
if (asDeletedText) {
|
|
170
|
+
if (asDeletedText && (isWordElement(sourceNode, 'delText') || isWordElement(sourceNode, 't'))) {
|
|
170
171
|
const delText = createWordElement(xmlDoc, 'w:delText');
|
|
171
172
|
delText.setAttribute('xml:space', 'preserve');
|
|
172
173
|
delText.textContent = text;
|
|
@@ -192,6 +193,17 @@ function cloneRunPiece(xmlDoc, sourceNode, text, asDeletedText) {
|
|
|
192
193
|
return sourceNode.cloneNode(true);
|
|
193
194
|
}
|
|
194
195
|
|
|
196
|
+
if (text === '\u00ad' && isWordElement(sourceNode, 'softHyphen')) {
|
|
197
|
+
return sourceNode.cloneNode(true);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (asDeletedText) {
|
|
201
|
+
const delText = createWordElement(xmlDoc, 'w:delText');
|
|
202
|
+
delText.setAttribute('xml:space', 'preserve');
|
|
203
|
+
delText.textContent = text;
|
|
204
|
+
return delText;
|
|
205
|
+
}
|
|
206
|
+
|
|
195
207
|
const textNode = createWordElement(xmlDoc, 'w:t');
|
|
196
208
|
textNode.setAttribute('xml:space', 'preserve');
|
|
197
209
|
textNode.textContent = text;
|
package/engine/surgical-spans.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getFirstElementByTag } from '../core/xml-query.js';
|
|
|
3
3
|
import { isWordElement } from '../core/word-xml.js';
|
|
4
4
|
|
|
5
5
|
export function getRunChildText(child) {
|
|
6
|
-
if (isWordElement(child, 't')) return child.textContent || '';
|
|
6
|
+
if (isWordElement(child, 't') || isWordElement(child, 'delText')) return child.textContent || '';
|
|
7
7
|
if (isWordElement(child, 'br') || isWordElement(child, 'cr')) return '\n';
|
|
8
8
|
if (isWordElement(child, 'tab')) return '\t';
|
|
9
9
|
if (isWordElement(child, 'noBreakHyphen')) return '\u2011';
|
|
@@ -13,6 +13,7 @@ export function getRunChildText(child) {
|
|
|
13
13
|
|
|
14
14
|
export function isTextLikeRunChild(child) {
|
|
15
15
|
return isWordElement(child, 't')
|
|
16
|
+
|| isWordElement(child, 'delText')
|
|
16
17
|
|| isWordElement(child, 'br')
|
|
17
18
|
|| isWordElement(child, 'cr')
|
|
18
19
|
|| isWordElement(child, 'tab')
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type OoxmlSourceType = 'package' | 'document' | 'fragment';
|
|
2
|
-
export type RedlineStatus = 'ok' | 'no-op' | 'error';
|
|
2
|
+
export type RedlineStatus = 'ok' | 'no-op' | 'partial' | 'error';
|
|
3
3
|
export type ExistingRevisionsPolicy = 'merge-same-author' | 'slice-cross-author' | 'reject-input' | 'accept-all-first' | 'accept-all-first-keep-normalized';
|
|
4
4
|
export type RevisionView = 'accepted' | 'rejected';
|
|
5
5
|
|
|
@@ -68,6 +68,22 @@ export interface RedlineError {
|
|
|
68
68
|
actual?: unknown;
|
|
69
69
|
commentIds?: string[];
|
|
70
70
|
comments?: Array<{ id: string; author: string; text: string }>;
|
|
71
|
+
recoveryVersion?: number;
|
|
72
|
+
category?: 'request_fixable' | 'target_refresh_required' | 'candidate_selection_required' | 'policy_choice_required' | 'user_authorization_required' | 'source_conflict' | 'library_or_builder_failure' | 'manual_document_resolution' | string;
|
|
73
|
+
context?: Record<string, unknown>;
|
|
74
|
+
recovery?: {
|
|
75
|
+
action: string;
|
|
76
|
+
sameArgumentsSafe: boolean;
|
|
77
|
+
requiresReinspection: boolean;
|
|
78
|
+
requiresUserAuthorization: boolean;
|
|
79
|
+
field?: string;
|
|
80
|
+
recommendedValue?: unknown;
|
|
81
|
+
};
|
|
82
|
+
expectedRevision?: RevisionToken;
|
|
83
|
+
currentRevision?: RevisionToken;
|
|
84
|
+
issueSummary?: { total: number; byCode: Array<{ code: string; count: number }> };
|
|
85
|
+
candidates?: unknown[];
|
|
86
|
+
[key: string]: unknown;
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
export interface RedlineOptions {
|