@ansonlai/docx-redline-js 0.8.4 → 0.10.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/ARCHITECTURE.md +679 -460
- package/CHANGELOG.md +208 -1
- package/README.md +152 -16
- package/core/current-properties.js +136 -0
- package/core/paragraph-text.js +211 -209
- package/core/types.js +9 -3
- package/core/word-revision-semantics.js +256 -0
- package/core/word-xml.js +160 -118
- package/dist/docx-redline-js.esm.js +4338 -796
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +111 -108
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4463 -921
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4463 -921
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +49 -7
- package/docs/SKILL_AUTHORING.md +169 -150
- package/docs/TESTING.md +1146 -1059
- package/docs/schemas/document-operations.schema.json +55 -2
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +21 -22
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-writer.js +67 -10
- package/engine/rpr-helpers.js +42 -0
- package/engine/run-builders.js +0 -73
- package/engine/surgical-diff-application.js +604 -605
- package/engine/table-mode.js +63 -63
- package/index.d.ts +53 -2
- package/index.js +13 -2
- package/node/cli-help.js +1 -0
- package/node/cli.js +21 -3
- package/orchestration/list-structural-fallback.js +67 -9
- package/package.json +1 -1
- package/pipeline/diff-engine.js +130 -1
- package/pipeline/ingestion-export.js +227 -243
- package/pipeline/ingestion-paragraph.js +93 -98
- package/pipeline/list-generation.js +415 -419
- package/pipeline/markdown-processor.js +5 -2
- package/pipeline/pipeline.js +206 -204
- package/scripts/export-validation-fixtures.mjs +19 -6
- package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
- package/scripts/lib/word-coverage-catalogue.mjs +7 -2
- package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
- package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
- package/scripts/word-com-differential.ps1 +443 -335
- package/scripts/word-com-visual-suite.ps1 +2 -2
- package/scripts/word-revision-semantics-matrix.ps1 +108 -0
- package/services/batch-operation-orchestrator.js +23 -3
- package/services/capture-engine.js +228 -226
- package/services/comment-replies.js +1 -1
- package/services/document-operation-applier.js +214 -0
- package/services/document-operation-contract.js +76 -0
- package/services/document-operation-mutations.js +688 -51
- package/services/error-recovery.js +17 -0
- package/services/inspection-comparison.js +82 -0
- package/services/list-conversion-diagnostics.js +296 -0
- package/services/list-format-change.js +586 -0
- package/services/list-format-mutation.js +416 -0
- package/services/list-format.js +305 -0
- package/services/numbering-allocation.js +642 -0
- package/services/numbering-helpers.js +354 -524
- package/services/operation-batch-compiler.js +183 -11
- package/services/operation-preflight.js +43 -33
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +38 -5
- package/services/revision-fidelity-verifier.js +484 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +182 -5
- package/services/table-reconciliation.js +59 -7
package/engine/table-mode.js
CHANGED
|
@@ -2,30 +2,30 @@
|
|
|
2
2
|
* Table-specific reconciliation and transformation flows.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
NS_W,
|
|
7
|
-
createRevisionMetadata,
|
|
8
|
-
getRevisionIdAllocatorForDocument,
|
|
9
|
-
setRevisionIdAllocatorForDocument
|
|
10
|
-
} from '../core/types.js';
|
|
11
|
-
import {
|
|
12
|
-
getElementsByTagNS,
|
|
13
|
-
getElementsByTagNSOrTag,
|
|
14
|
-
getFirstElementByTagNS,
|
|
15
|
-
getFirstElementByTagNSOrTag,
|
|
16
|
-
getXmlParseError
|
|
17
|
-
} from '../core/xml-query.js';
|
|
18
|
-
import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
19
|
-
import { log, error } from '../adapters/logger.js';
|
|
20
|
-
import { diffTablesWithVirtualGrid, serializeVirtualGridToOoxml, generateTableOoxml } from '../services/table-reconciliation.js';
|
|
21
|
-
import { parseTable } from '../pipeline/pipeline.js';
|
|
22
|
-
import { ingestTableToVirtualGrid } from '../pipeline/ingestion.js';
|
|
23
|
-
import { createWordElement, withOoxmlSourceType } from '../core/word-xml.js';
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
function noChanges(serializer, xmlDoc) {
|
|
27
|
-
return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
|
|
28
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
NS_W,
|
|
7
|
+
createRevisionMetadata,
|
|
8
|
+
getRevisionIdAllocatorForDocument,
|
|
9
|
+
setRevisionIdAllocatorForDocument
|
|
10
|
+
} from '../core/types.js';
|
|
11
|
+
import {
|
|
12
|
+
getElementsByTagNS,
|
|
13
|
+
getElementsByTagNSOrTag,
|
|
14
|
+
getFirstElementByTagNS,
|
|
15
|
+
getFirstElementByTagNSOrTag,
|
|
16
|
+
getXmlParseError
|
|
17
|
+
} from '../core/xml-query.js';
|
|
18
|
+
import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
19
|
+
import { log, error } from '../adapters/logger.js';
|
|
20
|
+
import { diffTablesWithVirtualGrid, serializeVirtualGridToOoxml, generateTableOoxml } from '../services/table-reconciliation.js';
|
|
21
|
+
import { parseTable } from '../pipeline/pipeline.js';
|
|
22
|
+
import { ingestTableToVirtualGrid } from '../pipeline/ingestion.js';
|
|
23
|
+
import { createWordElement, withOoxmlSourceType } from '../core/word-xml.js';
|
|
24
|
+
import { markParagraphDeleted } from './paragraph-mark-revisions.js';
|
|
25
|
+
|
|
26
|
+
function noChanges(serializer, xmlDoc) {
|
|
27
|
+
return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
|
|
28
|
+
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Applies structural reconciliation to tables using Virtual Grid.
|
|
@@ -38,8 +38,8 @@ function noChanges(serializer, xmlDoc) {
|
|
|
38
38
|
* @param {boolean} [generateRedlines=true] - Track change toggle
|
|
39
39
|
* @returns {{ oxml: string, hasChanges: boolean }}
|
|
40
40
|
*/
|
|
41
|
-
export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
|
|
42
|
-
const tableNodes = getElementsByTagNSOrTag(xmlDoc, NS_W, 'tbl');
|
|
41
|
+
export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
|
|
42
|
+
const tableNodes = getElementsByTagNSOrTag(xmlDoc, NS_W, 'tbl');
|
|
43
43
|
const newTableData = parseTable(modifiedText);
|
|
44
44
|
const hasNewContent = newTableData.rows.length > 0 || newTableData.headers.length > 0;
|
|
45
45
|
|
|
@@ -55,15 +55,15 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
|
|
|
55
55
|
return noChanges(serializer, xmlDoc);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const options = {
|
|
59
|
-
generateRedlines,
|
|
60
|
-
author,
|
|
61
|
-
revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
|
|
62
|
-
};
|
|
58
|
+
const options = {
|
|
59
|
+
generateRedlines,
|
|
60
|
+
author,
|
|
61
|
+
revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
|
|
62
|
+
};
|
|
63
63
|
const reconciledOxml = serializeVirtualGridToOoxml(oldGrid, operations, options);
|
|
64
64
|
const wrappedOxml = `<root xmlns:w="${NS_W}">${reconciledOxml}</root>`;
|
|
65
|
-
const reconciledDoc = parseOoxmlSafe(wrappedOxml, 'application/xml').doc;
|
|
66
|
-
if (!reconciledDoc) return noChanges(serializer, xmlDoc);
|
|
65
|
+
const reconciledDoc = parseOoxmlSafe(wrappedOxml, 'application/xml').doc;
|
|
66
|
+
if (!reconciledDoc) return noChanges(serializer, xmlDoc);
|
|
67
67
|
|
|
68
68
|
const parseError = getXmlParseError(reconciledDoc);
|
|
69
69
|
if (parseError) {
|
|
@@ -71,7 +71,7 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
|
|
|
71
71
|
return noChanges(serializer, xmlDoc);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
const newTableNode = getFirstElementByTagNSOrTag(reconciledDoc, NS_W, 'tbl');
|
|
74
|
+
const newTableNode = getFirstElementByTagNSOrTag(reconciledDoc, NS_W, 'tbl');
|
|
75
75
|
if (!newTableNode) {
|
|
76
76
|
error('[OxmlEngine] No table found in reconciled OOXML');
|
|
77
77
|
return noChanges(serializer, xmlDoc);
|
|
@@ -80,7 +80,7 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
|
|
|
80
80
|
const importedTable = xmlDoc.importNode(newTableNode, true);
|
|
81
81
|
targetTable.parentNode.replaceChild(importedTable, targetTable);
|
|
82
82
|
|
|
83
|
-
return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
|
|
83
|
+
return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -94,17 +94,17 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
|
|
|
94
94
|
* @param {boolean} generateRedlines - Track change toggle
|
|
95
95
|
* @returns {{ oxml: string, hasChanges: boolean }}
|
|
96
96
|
*/
|
|
97
|
-
export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
|
|
98
|
-
const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
|
|
97
|
+
export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
|
|
98
|
+
const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
|
|
99
99
|
const tableData = parseTable(modifiedText);
|
|
100
100
|
if (!tableData || (tableData.rows.length === 0 && tableData.headers.length === 0)) {
|
|
101
101
|
log('[OxmlEngine] Failed to parse table data from Markdown');
|
|
102
102
|
return noChanges(serializer, xmlDoc);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator });
|
|
106
|
-
const tableDoc = parseOoxmlSafe(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, 'application/xml').doc;
|
|
107
|
-
if (!tableDoc) return noChanges(serializer, xmlDoc);
|
|
105
|
+
const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator, trackAsBlock: true });
|
|
106
|
+
const tableDoc = parseOoxmlSafe(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, 'application/xml').doc;
|
|
107
|
+
if (!tableDoc) return noChanges(serializer, xmlDoc);
|
|
108
108
|
|
|
109
109
|
const tableParseError = getXmlParseError(tableDoc);
|
|
110
110
|
if (tableParseError) {
|
|
@@ -132,16 +132,16 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
|
|
|
132
132
|
let parent = firstParagraph.parentNode;
|
|
133
133
|
|
|
134
134
|
if (parent && parent.nodeType === 9) {
|
|
135
|
-
const wrappedDoc = parseOoxmlSafe(
|
|
136
|
-
`<w:document xmlns:w="${NS_W}"><w:body/></w:document>`,
|
|
137
|
-
'application/xml'
|
|
138
|
-
).doc;
|
|
139
|
-
if (!wrappedDoc) return noChanges(serializer, workingDoc);
|
|
135
|
+
const wrappedDoc = parseOoxmlSafe(
|
|
136
|
+
`<w:document xmlns:w="${NS_W}"><w:body/></w:document>`,
|
|
137
|
+
'application/xml'
|
|
138
|
+
).doc;
|
|
139
|
+
if (!wrappedDoc) return noChanges(serializer, workingDoc);
|
|
140
140
|
const wrappedBody = getFirstElementByTagNS(wrappedDoc, NS_W, 'body');
|
|
141
141
|
paragraphs.forEach(p => wrappedBody.appendChild(wrappedDoc.importNode(p, true)));
|
|
142
142
|
|
|
143
|
-
workingDoc = wrappedDoc;
|
|
144
|
-
setRevisionIdAllocatorForDocument(workingDoc, revisionIdAllocator);
|
|
143
|
+
workingDoc = wrappedDoc;
|
|
144
|
+
setRevisionIdAllocatorForDocument(workingDoc, revisionIdAllocator);
|
|
145
145
|
paragraphs = getElementsByTagNS(workingDoc, NS_W, 'p');
|
|
146
146
|
firstParagraph = paragraphs[0];
|
|
147
147
|
parent = firstParagraph.parentNode;
|
|
@@ -149,26 +149,26 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
|
|
|
149
149
|
|
|
150
150
|
const importedTable = workingDoc.importNode(newTableElement, true);
|
|
151
151
|
|
|
152
|
-
if (generateRedlines) {
|
|
153
|
-
paragraphs.forEach(p => {
|
|
154
|
-
|
|
155
|
-
const runs = getElementsByTagNS(p, NS_W, 'r');
|
|
156
|
-
runs.forEach(run => {
|
|
152
|
+
if (generateRedlines) {
|
|
153
|
+
paragraphs.forEach(p => {
|
|
154
|
+
markParagraphDeleted(p, { author, revisionIdAllocator: workingDoc });
|
|
155
|
+
const runs = getElementsByTagNS(p, NS_W, 'r');
|
|
156
|
+
runs.forEach(run => {
|
|
157
157
|
const textNodes = getElementsByTagNS(run, NS_W, 't');
|
|
158
158
|
textNodes.forEach(t => {
|
|
159
159
|
const text = t.textContent || '';
|
|
160
160
|
if (text.trim()) {
|
|
161
|
-
const delText = createWordElement(workingDoc, 'w:delText');
|
|
162
|
-
delText.textContent = text;
|
|
163
|
-
t.parentNode.replaceChild(delText, t);
|
|
161
|
+
const delText = createWordElement(workingDoc, 'w:delText');
|
|
162
|
+
delText.textContent = text;
|
|
163
|
+
t.parentNode.replaceChild(delText, t);
|
|
164
164
|
}
|
|
165
165
|
});
|
|
166
|
-
|
|
167
|
-
const del = createWordElement(workingDoc, 'w:del');
|
|
168
|
-
const metadata = createRevisionMetadata(author, workingDoc, 'del');
|
|
169
|
-
del.setAttribute('w:id', String(metadata.id));
|
|
170
|
-
del.setAttribute('w:author', metadata.author);
|
|
171
|
-
del.setAttribute('w:date', metadata.date);
|
|
166
|
+
|
|
167
|
+
const del = createWordElement(workingDoc, 'w:del');
|
|
168
|
+
const metadata = createRevisionMetadata(author, workingDoc, 'del');
|
|
169
|
+
del.setAttribute('w:id', String(metadata.id));
|
|
170
|
+
del.setAttribute('w:author', metadata.author);
|
|
171
|
+
del.setAttribute('w:date', metadata.date);
|
|
172
172
|
run.parentNode.insertBefore(del, run);
|
|
173
173
|
del.appendChild(run);
|
|
174
174
|
});
|
|
@@ -183,5 +183,5 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
log('[OxmlEngine] Text-to-table transformation complete');
|
|
186
|
-
return withOoxmlSourceType({ oxml: serializer.serializeToString(workingDoc), hasChanges: true });
|
|
187
|
-
}
|
|
186
|
+
return withOoxmlSourceType({ oxml: serializer.serializeToString(workingDoc), hasChanges: true });
|
|
187
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type OoxmlSourceType = 'package' | 'document' | 'fragment';
|
|
2
2
|
export type RedlineStatus = 'ok' | 'no-op' | 'partial' | 'error';
|
|
3
|
-
export type ExistingRevisionsPolicy = 'merge-same-author' | 'slice-cross-author' | 'reject-input' | 'accept-all-first' | 'accept-all-first-keep-normalized';
|
|
3
|
+
export type ExistingRevisionsPolicy = 'auto' | '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
|
|
|
6
6
|
export interface RevisionTextSegment {
|
|
@@ -30,6 +30,16 @@ export type {
|
|
|
30
30
|
DocumentOperationBatchResult,
|
|
31
31
|
HighlightDocumentOperation,
|
|
32
32
|
InsertionAffinity,
|
|
33
|
+
ListConversionDiagnostic,
|
|
34
|
+
ListFormatDocumentOperation,
|
|
35
|
+
ListFormatRequest,
|
|
36
|
+
ListFormatConvertPlainRequest,
|
|
37
|
+
ListFormatChangeFormatRequest,
|
|
38
|
+
ListFormatJoinRequest,
|
|
39
|
+
ListFormatResult,
|
|
40
|
+
ListFormatConvertPlainResult,
|
|
41
|
+
ListFormatRebindResult,
|
|
42
|
+
ListNumberFormat,
|
|
33
43
|
LocalizedReplacement,
|
|
34
44
|
LocalizedReplacementChange,
|
|
35
45
|
OperationConflict,
|
|
@@ -52,6 +62,13 @@ export interface RedlineError {
|
|
|
52
62
|
| 'DIFF_TOKEN_LIMIT'
|
|
53
63
|
| 'UNSUPPORTED_REVISION_VIEW_MUTATION'
|
|
54
64
|
| 'UNSUPPORTED_INSERTION_AFFINITY'
|
|
65
|
+
| 'UNSUPPORTED_LIST_CONVERSION'
|
|
66
|
+
| 'UNSUPPORTED_LIST_FORMAT_CHANGE'
|
|
67
|
+
| 'LIST_MARKER_NOT_FOUND'
|
|
68
|
+
| 'LIST_MARKER_AMBIGUOUS'
|
|
69
|
+
| 'LIST_MARKER_UNSUPPORTED'
|
|
70
|
+
| 'LIST_FORMAT_TARGET_IS_LIST'
|
|
71
|
+
| 'LIST_FORMAT_STYLE_NUMBERED'
|
|
55
72
|
| 'UNSAFE_PARAGRAPH_BOUNDARY'
|
|
56
73
|
| 'FOREIGN_PARAGRAPH_MARK_DELETION'
|
|
57
74
|
| 'RESTORATION_STATE_REQUIRED'
|
|
@@ -80,6 +97,11 @@ export interface RedlineError {
|
|
|
80
97
|
recoveryVersion?: number;
|
|
81
98
|
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;
|
|
82
99
|
context?: Record<string, unknown>;
|
|
100
|
+
/** List-conversion errors: the literal marker, the requested list kind/format and the kept source definition. */
|
|
101
|
+
listMarker?: string;
|
|
102
|
+
requestedList?: { kind: 'bullet' | 'numbered'; format: string };
|
|
103
|
+
sourceList?: { numId: string; level: number; kind: 'bullet' | 'numbered'; format: string };
|
|
104
|
+
targetExcerpt?: string;
|
|
83
105
|
recovery?: {
|
|
84
106
|
action: string;
|
|
85
107
|
sameArgumentsSafe: boolean;
|
|
@@ -87,6 +109,8 @@ export interface RedlineError {
|
|
|
87
109
|
requiresUserAuthorization: boolean;
|
|
88
110
|
field?: string;
|
|
89
111
|
recommendedValue?: unknown;
|
|
112
|
+
allowedValue?: unknown;
|
|
113
|
+
hint?: string;
|
|
90
114
|
};
|
|
91
115
|
expectedRevision?: RevisionToken;
|
|
92
116
|
currentRevision?: RevisionToken;
|
|
@@ -348,6 +372,11 @@ export function inspectDocumentParts(parts: { documentXml: string; commentsXml?:
|
|
|
348
372
|
export function buildRevisionTokenFraming(options: { scope: string; entries?: unknown }): { framing: Uint8Array; scope: string; version: number; coveredParts: string[] };
|
|
349
373
|
export function computeRevisionToken(options: { scope: string; entries?: unknown; digestFn?: (bytes: Uint8Array) => Promise<string> }): Promise<RevisionToken>;
|
|
350
374
|
export function computeRevisionTokenSync(options: { scope: string; entries?: unknown; digestFn: (bytes: Uint8Array) => string }): RevisionToken;
|
|
375
|
+
/** Compare two inspections by visible text, table position, list label, review segments and comments (ids, dates and run boundaries ignored). */
|
|
376
|
+
export function compareDocumentInspections(expected: { paragraphs: unknown[]; comments?: unknown[] }, actual: { paragraphs: unknown[]; comments?: unknown[] }): { matches: boolean; differences: Array<{ path: string; expected: unknown; actual: unknown }> };
|
|
377
|
+
export function inspectionReviewShape(inspection: { paragraphs: unknown[]; comments?: unknown[] }): { paragraphs: unknown[]; comments: string[] };
|
|
378
|
+
/** Resolve `existingRevisions: 'auto'` for one target node; other policies are returned unchanged. */
|
|
379
|
+
export function resolveExistingRevisionsPolicy(policy: ExistingRevisionsPolicy | null | undefined, node: unknown, author: string): ExistingRevisionsPolicy | null | undefined;
|
|
351
380
|
export function computeDocumentPartsRevisionToken(parts: unknown, options?: { digestFn?: (bytes: Uint8Array) => Promise<string> }): Promise<RevisionToken>;
|
|
352
381
|
export function validateRevisionToken(token: unknown): { valid: boolean; error?: { code: string; message: string } };
|
|
353
382
|
export function areRevisionTokensEqual(a: string, b: string): boolean;
|
|
@@ -437,7 +466,29 @@ export function overwriteParagraphNumIds(oxml: string, numId: string | number):
|
|
|
437
466
|
export function extractFirstParagraphNumId(oxml: string): string | null;
|
|
438
467
|
export function buildExplicitDecimalMultilevelNumberingXml(options?: Record<string, unknown>): string;
|
|
439
468
|
export function remapNumberingPayloadForDocument(numberingXml: string, state: unknown): unknown;
|
|
440
|
-
export
|
|
469
|
+
export interface NumberingIdConflict { kind: 'abstractNum' | 'num'; id: number }
|
|
470
|
+
export interface NumberingMergeOptions { onConflict?: (conflict: NumberingIdConflict) => void }
|
|
471
|
+
export interface NumberingIntegrityIssue { code: string; severity: 'error'; message: string; numId?: number; abstractNumId?: number; ilvl?: number }
|
|
472
|
+
export function mergeNumberingXmlBySchemaOrder(baseXml: string, incomingXml: string, options?: NumberingMergeOptions): string;
|
|
473
|
+
export function mergeNumberingParts(existingXml: string | null | undefined, incomingParts: string | Array<string | null | undefined> | null | undefined, options?: NumberingMergeOptions): string;
|
|
474
|
+
export function allocateNumberingDefinition(params: {
|
|
475
|
+
numberingIdState: unknown;
|
|
476
|
+
sourceNumberingXml?: string | null;
|
|
477
|
+
sourceNumId?: number | string | null;
|
|
478
|
+
levels?: Array<{ ilvl: number; numFmt?: string; lvlText?: string; start?: number }>;
|
|
479
|
+
startOverrides?: Record<number | string, number>;
|
|
480
|
+
}): { numId: number; abstractNumId: number; numberingXml: string } | null;
|
|
481
|
+
export function allocateNumberingForReplacement(params: {
|
|
482
|
+
numberingXml: string | null;
|
|
483
|
+
replacementNodes: unknown[];
|
|
484
|
+
sourceParagraphs?: unknown[];
|
|
485
|
+
numberingIdState?: unknown;
|
|
486
|
+
allowContinueSource?: boolean;
|
|
487
|
+
}): { policy: 'continue-source' | 'allocate' | 'passthrough'; numberingXml: string | null; replacementNodes: unknown[]; numId?: number };
|
|
488
|
+
export function applyStartOverridesToNum(numElement: unknown, startOverrides: Record<number | string, number>): void;
|
|
489
|
+
export function copyLevelGeometry(sourceLvl: unknown, targetLvl: unknown): void;
|
|
490
|
+
export function canonicalNumberingDefinition(numbering: unknown, numId: string | number): string | null;
|
|
491
|
+
export function validateNumberingIntegrity(documentXml: unknown, numberingXml?: unknown): NumberingIntegrityIssue[];
|
|
441
492
|
|
|
442
493
|
export function buildListMarkdown(items: unknown[], options?: Record<string, unknown>): string;
|
|
443
494
|
export function inferNumberingStyleFromMarker(marker: string): string;
|
package/index.js
CHANGED
|
@@ -260,9 +260,18 @@ export {
|
|
|
260
260
|
overwriteParagraphNumIds,
|
|
261
261
|
extractFirstParagraphNumId,
|
|
262
262
|
buildExplicitDecimalMultilevelNumberingXml,
|
|
263
|
-
remapNumberingPayloadForDocument
|
|
264
|
-
mergeNumberingXmlBySchemaOrder
|
|
263
|
+
remapNumberingPayloadForDocument
|
|
265
264
|
} from './services/numbering-helpers.js';
|
|
265
|
+
export {
|
|
266
|
+
allocateNumberingDefinition,
|
|
267
|
+
allocateNumberingForReplacement,
|
|
268
|
+
applyStartOverridesToNum,
|
|
269
|
+
canonicalNumberingDefinition,
|
|
270
|
+
copyLevelGeometry,
|
|
271
|
+
mergeNumberingParts,
|
|
272
|
+
mergeNumberingXmlBySchemaOrder,
|
|
273
|
+
validateNumberingIntegrity
|
|
274
|
+
} from './services/numbering-allocation.js';
|
|
266
275
|
|
|
267
276
|
// Pipeline components
|
|
268
277
|
export { ReconciliationPipeline } from './pipeline/pipeline.js';
|
|
@@ -275,6 +284,8 @@ export {
|
|
|
275
284
|
} from './pipeline/ingestion-export.js';
|
|
276
285
|
export { extractCanonicalParagraphText, readCanonicalRunText, isNodeVisibleInRevisionView, extractParagraphRevisionSegments } from './core/paragraph-text.js';
|
|
277
286
|
export { inspectDocumentParts } from './services/document-inspection.js';
|
|
287
|
+
export { compareDocumentInspections, inspectionReviewShape } from './services/inspection-comparison.js';
|
|
288
|
+
export { resolveExistingRevisionsPolicy } from './core/word-xml.js';
|
|
278
289
|
export {
|
|
279
290
|
buildRevisionTokenFraming,
|
|
280
291
|
computeRevisionToken,
|
package/node/cli-help.js
CHANGED
|
@@ -144,6 +144,7 @@ export const CLI_COMMAND_HELP = Object.freeze({
|
|
|
144
144
|
option('textToComment', '--text-to-comment <text>', 'Anchor an inline comment to an exact subspan.'),
|
|
145
145
|
option('targetRef', '--target-ref <N>', 'Disambiguate an inline target with a 1-based machine index.'),
|
|
146
146
|
option('existingRevisions', '--existing-revisions <policy>', 'Select revision handling; cross-author slicing must be deliberate.'),
|
|
147
|
+
option('revisionFidelity', '--revision-fidelity <enforce|warn|off>', 'Reject All verification of each tracked operation (default enforce: refuse with REVISION_FIDELITY_VIOLATION); the agent profile rejects off.'),
|
|
147
148
|
option('atomic', '--atomic[=true|false]', 'Choose all-or-nothing or progressive batch execution.'),
|
|
148
149
|
option('generateRedlines', '--generate-redlines[=true|false]', 'Control tracked-change generation.'),
|
|
149
150
|
option('noRedlines', '--no-redlines', 'Apply clean text without tracked-change markup.'),
|
package/node/cli.js
CHANGED
|
@@ -7,11 +7,12 @@ import { validateRedlineOoxml } from '../core/redline-validation.js';
|
|
|
7
7
|
import { configureLogger } from '../adapters/logger.js';
|
|
8
8
|
import { isExistingRevisionsPolicy } from '../services/document-operation-contract.js';
|
|
9
9
|
import { normalizeErrorWithRecovery } from '../services/error-recovery.js';
|
|
10
|
+
import { REVISION_FIDELITY_MODES, resolveRevisionFidelityMode } from '../services/revision-fidelity-verifier.js';
|
|
10
11
|
import { localizedReplacementOffsets } from '../services/localized-replacement-compiler.js';
|
|
11
12
|
import { buildCliHelp, CLI_COMMANDS, commandOptionKeys } from './cli-help.js';
|
|
12
13
|
|
|
13
14
|
const suffixes = { apply: 'redlined', accept: 'accepted', reject: 'rejected', 'delete-comments': 'comments-removed' };
|
|
14
|
-
const CLI_CONTRACT_VERSION =
|
|
15
|
+
const CLI_CONTRACT_VERSION = 9;
|
|
15
16
|
const DEFAULT_INSPECTION_LIMIT = 20;
|
|
16
17
|
const INSPECTION_SOFT_BYTE_LIMIT = 48 * 1024;
|
|
17
18
|
const CLI_CAPABILITIES = [
|
|
@@ -39,7 +40,13 @@ const CLI_CAPABILITIES = [
|
|
|
39
40
|
'comment-resolve-operation-v1',
|
|
40
41
|
'delete-comments-by-id-v1',
|
|
41
42
|
'header-footer-parts-v1',
|
|
42
|
-
'format-text-occurrence-v1'
|
|
43
|
+
'format-text-occurrence-v1',
|
|
44
|
+
'revision-fidelity-v1',
|
|
45
|
+
'list-conversion-diagnostics-v1',
|
|
46
|
+
'paragraph-numbering-binding-v1',
|
|
47
|
+
'list-format-v1',
|
|
48
|
+
'list-format-change-v1',
|
|
49
|
+
'existing-revisions-auto-v1'
|
|
43
50
|
];
|
|
44
51
|
const commandOptions = Object.fromEntries(CLI_COMMANDS.map(command => [command, new Set(commandOptionKeys(command))]));
|
|
45
52
|
|
|
@@ -597,7 +604,9 @@ function compactError(error) {
|
|
|
597
604
|
'code', 'stage', 'mismatchOffset', 'expectedExcerpt', 'actualExcerpt',
|
|
598
605
|
'expectedCodePoint', 'actualCodePoint', 'ownerAuthor', 'commentIds',
|
|
599
606
|
'recoveryVersion', 'category', 'field', 'captureRef', 'operationIndexes',
|
|
600
|
-
'consumedByOperation', 'expectedScope', 'actualScope'
|
|
607
|
+
'consumedByOperation', 'expectedScope', 'actualScope',
|
|
608
|
+
'invariants', 'violations', 'verificationMode', 'baselineView',
|
|
609
|
+
'listMarker', 'requestedList', 'sourceList', 'targetExcerpt'
|
|
601
610
|
];
|
|
602
611
|
const compact = {};
|
|
603
612
|
for (const field of fields) {
|
|
@@ -823,6 +832,12 @@ export async function executeCli(argv, io = process) {
|
|
|
823
832
|
if (profile && profile !== 'agent') {
|
|
824
833
|
return cliError('INVALID_PROFILE', `Unknown execution profile: "${profile}". Supported profiles: agent.`);
|
|
825
834
|
}
|
|
835
|
+
if (flags.revisionFidelity != null && !REVISION_FIDELITY_MODES.includes(String(flags.revisionFidelity))) {
|
|
836
|
+
return cliError('INVALID_OPERATION', `Unsupported revision-fidelity mode: "${String(flags.revisionFidelity)}". Use enforce, warn or off.`);
|
|
837
|
+
}
|
|
838
|
+
if (profile === 'agent' && String(flags.revisionFidelity) === 'off') {
|
|
839
|
+
return cliError('INVALID_PROFILE', 'The agent profile requires Reject All verification; --revision-fidelity off is only for diagnosis outside the agent profile.');
|
|
840
|
+
}
|
|
826
841
|
let inspectOptions = null;
|
|
827
842
|
if (command === 'inspect' || command === 'extract') {
|
|
828
843
|
try { inspectOptions = inspectionOptions(flags); }
|
|
@@ -925,6 +940,7 @@ export async function executeCli(argv, io = process) {
|
|
|
925
940
|
validate: true,
|
|
926
941
|
generateRedlines,
|
|
927
942
|
existingRevisions: flags.existingRevisions || 'merge-same-author',
|
|
943
|
+
verifyRevisionFidelity: resolveRevisionFidelityMode(flags.revisionFidelity == null ? undefined : String(flags.revisionFidelity)),
|
|
928
944
|
requireComplete
|
|
929
945
|
};
|
|
930
946
|
const result = await document.applyOperations(operations, {
|
|
@@ -932,8 +948,10 @@ export async function executeCli(argv, io = process) {
|
|
|
932
948
|
atomic,
|
|
933
949
|
validate: true,
|
|
934
950
|
strictTargets: true,
|
|
951
|
+
strictListConversion: agentProfile || atomic,
|
|
935
952
|
generateRedlines,
|
|
936
953
|
...(flags.existingRevisions ? { existingRevisions: flags.existingRevisions } : {}),
|
|
954
|
+
verifyRevisionFidelity: effectiveOptions.verifyRevisionFidelity,
|
|
937
955
|
...(expectedRevision ? { expectedRevision } : {})
|
|
938
956
|
});
|
|
939
957
|
const localizedVerificationFailed = (result.results || []).some(item => item?.change && (
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { createSerializer, parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
7
7
|
import { getXmlParseError } from '../core/xml-query.js';
|
|
8
|
-
import { NS_W } from '../core/types.js';
|
|
8
|
+
import { NS_W, createRevisionMetadata } from '../core/types.js';
|
|
9
|
+
import { clonePropertiesWithoutRevisionHistory } from '../core/revision-cloning.js';
|
|
10
|
+
import { getCurrentNumIdElements } from '../core/current-properties.js';
|
|
9
11
|
import { createWordElement } from '../core/word-xml.js';
|
|
10
12
|
import {
|
|
11
13
|
getDocumentParagraphNodes,
|
|
@@ -62,12 +64,47 @@ export function stripSingleLineListMarkerPrefix(text) {
|
|
|
62
64
|
return String(candidate.contentText || '').trim();
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
const ROMAN_VALUES = { i: 1, v: 5, x: 10, l: 50, c: 100, d: 500, m: 1000 };
|
|
68
|
+
|
|
69
|
+
function parseRomanNumeral(text) {
|
|
70
|
+
const letters = String(text || '').toLowerCase();
|
|
71
|
+
if (!/^[ivxlcdm]+$/.test(letters)) return null;
|
|
72
|
+
let total = 0;
|
|
73
|
+
for (let i = 0; i < letters.length; i++) {
|
|
74
|
+
const value = ROMAN_VALUES[letters[i]];
|
|
75
|
+
const next = ROMAN_VALUES[letters[i + 1]] || 0;
|
|
76
|
+
total += value < next ? -value : value;
|
|
77
|
+
}
|
|
78
|
+
return total;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Derives the numeric start a literal marker implies: `7.` -> 7, `C.` -> 3, `iv.` -> 4.
|
|
83
|
+
* Decimal markers keep every positive value (including 1). Letter and roman markers report only
|
|
84
|
+
* values above 1, because the default start (1) needs no override and keeps the shared-list behavior.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} marker
|
|
87
|
+
* @param {string} numberingStyle
|
|
88
|
+
* @returns {number|null}
|
|
89
|
+
*/
|
|
90
|
+
export function parseMarkerStart(marker, numberingStyle) {
|
|
91
|
+
const text = String(marker || '').trim();
|
|
92
|
+
let value = null;
|
|
93
|
+
if (numberingStyle === 'decimal') {
|
|
94
|
+
const match = text.match(/^(\d+)\.?$/);
|
|
95
|
+
value = match ? Number.parseInt(match[1], 10) : null;
|
|
96
|
+
} else if (numberingStyle === 'lowerRoman' || numberingStyle === 'upperRoman') {
|
|
97
|
+
const match = text.match(/^([A-Za-z]+)[.)]?$/);
|
|
98
|
+
value = match ? parseRomanNumeral(match[1]) : null;
|
|
99
|
+
} else if (numberingStyle === 'lowerAlpha' || numberingStyle === 'upperAlpha') {
|
|
100
|
+
const match = text.match(/^([A-Za-z])[.)]?$/);
|
|
101
|
+
value = match ? match[1].toLowerCase().charCodeAt(0) - 96 : null;
|
|
102
|
+
} else {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (!Number.isFinite(value) || value < 1) return null;
|
|
106
|
+
if (numberingStyle !== 'decimal' && value === 1) return null;
|
|
107
|
+
return value;
|
|
71
108
|
}
|
|
72
109
|
|
|
73
110
|
/**
|
|
@@ -199,7 +236,8 @@ function getDirectWordChild(element, localName) {
|
|
|
199
236
|
* numId: string|number,
|
|
200
237
|
* ilvl?: number,
|
|
201
238
|
* clearParagraphPropertyChanges?: boolean,
|
|
202
|
-
* removeListPropertyNode?: boolean
|
|
239
|
+
* removeListPropertyNode?: boolean,
|
|
240
|
+
* trackPropertyChange?: { author?: string, revisionIdAllocator?: object|null }|null
|
|
203
241
|
* }} options - Binding options
|
|
204
242
|
* @returns {number} Number of paragraph nodes updated
|
|
205
243
|
*/
|
|
@@ -209,6 +247,7 @@ export function enforceListBindingOnParagraphNodes(nodes, options = {}) {
|
|
|
209
247
|
const ilvl = Number.isInteger(options?.ilvl) ? Math.max(0, options.ilvl) : 0;
|
|
210
248
|
const clearParagraphPropertyChanges = options?.clearParagraphPropertyChanges !== false;
|
|
211
249
|
const removeListPropertyNode = options?.removeListPropertyNode !== false;
|
|
250
|
+
const trackPropertyChange = options?.trackPropertyChange || null;
|
|
212
251
|
|
|
213
252
|
const paragraphs = (Array.isArray(nodes) ? nodes : [])
|
|
214
253
|
.filter(node => node && node.nodeType === 1 && node.localName === 'p');
|
|
@@ -224,6 +263,9 @@ export function enforceListBindingOnParagraphNodes(nodes, options = {}) {
|
|
|
224
263
|
paragraph.insertBefore(pPr, paragraph.firstChild);
|
|
225
264
|
}
|
|
226
265
|
|
|
266
|
+
// Snapshot the pre-binding pPr so Reject All restores the unnumbered paragraph.
|
|
267
|
+
const priorPPr = trackPropertyChange ? clonePropertiesWithoutRevisionHistory(pPr) : null;
|
|
268
|
+
|
|
227
269
|
if (clearParagraphPropertyChanges) {
|
|
228
270
|
const pPrChange = getDirectWordChild(pPr, 'pPrChange');
|
|
229
271
|
if (pPrChange) pPr.removeChild(pPrChange);
|
|
@@ -253,6 +295,20 @@ export function enforceListBindingOnParagraphNodes(nodes, options = {}) {
|
|
|
253
295
|
numPr.appendChild(numIdEl);
|
|
254
296
|
}
|
|
255
297
|
numIdEl.setAttribute('w:val', String(numId));
|
|
298
|
+
|
|
299
|
+
if (trackPropertyChange && !getDirectWordChild(pPr, 'pPrChange')) {
|
|
300
|
+
const metadata = createRevisionMetadata(
|
|
301
|
+
trackPropertyChange.author,
|
|
302
|
+
trackPropertyChange.revisionIdAllocator || ownerDoc,
|
|
303
|
+
'pPrChange'
|
|
304
|
+
);
|
|
305
|
+
const pPrChange = createWordElement(ownerDoc, 'w:pPrChange');
|
|
306
|
+
pPrChange.setAttribute('w:id', String(metadata.id));
|
|
307
|
+
pPrChange.setAttribute('w:author', metadata.author);
|
|
308
|
+
pPrChange.setAttribute('w:date', metadata.date);
|
|
309
|
+
pPrChange.appendChild(priorPPr || createWordElement(ownerDoc, 'w:pPr'));
|
|
310
|
+
pPr.appendChild(pPrChange);
|
|
311
|
+
}
|
|
256
312
|
updated++;
|
|
257
313
|
}
|
|
258
314
|
|
|
@@ -311,7 +367,8 @@ function extractFirstParagraphNumIdFromOxml(oxml) {
|
|
|
311
367
|
// Tracked-change output leads with a deleted-original paragraph that carries
|
|
312
368
|
// no numPr, so the generated list binding is the first paragraph that has one.
|
|
313
369
|
for (const paragraph of paragraphs) {
|
|
314
|
-
|
|
370
|
+
// Current numIds only; a pPrChange snapshot must not supply the generated list binding.
|
|
371
|
+
const numIdNodes = getCurrentNumIdElements(paragraph);
|
|
315
372
|
for (const numIdNode of numIdNodes) {
|
|
316
373
|
const numId = getElementId(numIdNode, ['w:val', 'val']);
|
|
317
374
|
if (numId != null) return String(numId);
|
|
@@ -433,6 +490,7 @@ export function buildSingleLineListStructuralFallbackPlan(options = {}) {
|
|
|
433
490
|
numberingKey: `${modifiedCandidate.type}:${modifiedCandidate.numberingStyle}:single`,
|
|
434
491
|
originalText,
|
|
435
492
|
wasListParagraph: !!existingListInfo,
|
|
493
|
+
numberingStyle: modifiedCandidate.numberingStyle,
|
|
436
494
|
startAt: modifiedCandidate.startAt
|
|
437
495
|
};
|
|
438
496
|
}
|