@ansonlai/docx-redline-js 0.4.0 → 0.5.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 +646 -288
- package/ARCHITECTURE.md +215 -9
- package/CHANGELOG.md +319 -0
- package/README.md +604 -360
- package/adapters/config.js +45 -43
- package/bin/docx-redline.js +3 -0
- package/core/list-targeting.js +101 -110
- package/core/paragraph-targeting.js +501 -61
- package/core/paragraph-text.js +209 -0
- package/core/redline-validation.js +11 -5
- package/core/revision-cloning.js +38 -0
- package/core/types.js +64 -10
- package/core/word-xml.js +43 -15
- package/dist/docx-redline-js.esm.js +3145 -505
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +88 -76
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +342 -23
- package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +505 -0
- package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
- package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
- package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
- package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
- package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
- package/docs/schemas/document-operations.schema.json +109 -0
- package/docs/test-comparison-dashboard.html +4250 -7
- package/engine/formatting-removal.js +11 -2
- package/engine/oxml-engine.js +508 -336
- package/engine/reconstruction-mode.js +15 -14
- package/engine/reconstruction-writer.js +247 -142
- package/engine/route-selection.js +35 -0
- package/engine/rpr-helpers.js +334 -35
- package/engine/run-builders.js +239 -196
- package/engine/surgical-diff-application.js +407 -50
- package/engine/surgical-mode.js +142 -6
- package/engine/surgical-run-splitting.js +103 -0
- package/engine/surgical-spans.js +52 -1
- package/engine/table-cell-context.js +3 -6
- package/engine/table-mode.js +1 -1
- package/index.d.ts +234 -6
- package/index.js +24 -1
- package/node/cli.js +322 -0
- package/node/docx-document.js +302 -0
- package/node/index.d.ts +31 -0
- package/node/index.js +2 -0
- package/node/zip-archive.js +52 -0
- package/orchestration/list-markdown.js +10 -16
- package/orchestration/list-parsing.js +7 -12
- package/orchestration/list-structural-fallback.js +21 -10
- package/package.json +123 -102
- package/pipeline/content-analysis.js +12 -17
- package/pipeline/ingestion-export.js +3 -31
- package/pipeline/ingestion-paragraph.js +10 -5
- package/pipeline/list-generation.js +150 -55
- package/pipeline/list-markers.js +70 -3
- package/pipeline/serialization.js +4 -2
- package/pipeline/structured-content.js +160 -0
- package/scripts/apply_changes.mjs +27 -0
- package/scripts/benchmark-operation-session.mjs +137 -0
- package/scripts/benchmark-targeting-browser.html +74 -0
- package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
- package/scripts/benchmark-test-runner.mjs +59 -0
- package/scripts/build-test-dashboard.mjs +23 -0
- package/scripts/export-lane1-fixtures.mjs +380 -0
- package/scripts/export-reredline-stress-fixtures.mjs +317 -0
- package/scripts/export-validation-fixtures.mjs +1 -1
- package/scripts/extract_text.mjs +7 -0
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +256 -0
- package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
- package/scripts/generate-test-dashboard.mjs +362 -11
- package/scripts/lib/word-coverage-catalogue.mjs +6 -2
- package/scripts/profile-route-selection.mjs +19 -0
- package/scripts/render-agenda-multilevel.mjs +0 -5
- package/scripts/render-multilevel-cases.mjs +0 -1
- package/scripts/run-tests.mjs +107 -35
- package/scripts/word-com-corpus-suite.ps1 +3 -0
- package/scripts/word-com-differential.ps1 +64 -4
- package/scripts/word-com-suite.ps1 +3 -0
- package/services/batch-operation-orchestrator.js +513 -0
- package/services/capture-engine.js +226 -0
- package/services/comment-builders.js +23 -6
- package/services/comment-engine.js +108 -47
- package/services/comment-locator.js +187 -82
- package/services/comment-replies.js +95 -0
- package/services/document-inspection.js +258 -0
- package/services/document-operation-applier.js +372 -0
- package/services/document-operation-contract.js +345 -0
- package/services/document-operation-mutations.js +1749 -0
- package/services/document-operation-session.js +258 -0
- package/services/numbering-service.js +14 -5
- package/services/operation-heuristics.js +173 -0
- package/services/operation-preflight.js +390 -0
- package/services/receipt-collector.js +288 -0
- package/services/revision-comment-management.js +77 -5
- package/services/revision-token.js +290 -0
- package/services/standalone-docx-plumbing.js +123 -8
- package/services/standalone-operation-runner.d.ts +296 -0
- package/services/standalone-operation-runner.js +10 -1455
- package/services/table-reconciliation.js +15 -6
- package/docs/VALIDATION.md +0 -183
- package/docs/WORD-MANUAL-REVIEW.md +0 -138
- package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
- /package/docs/plans/{2026-08-30-reliability-testing-improvements.md → completed/2026-08-30-reliability-testing-improvements.md} +0 -0
package/scripts/run-tests.mjs
CHANGED
|
@@ -1,43 +1,115 @@
|
|
|
1
|
-
import { readdirSync } from 'fs';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
.sort();
|
|
11
|
-
|
|
12
|
-
let passed = 0;
|
|
13
|
-
let failed = 0;
|
|
14
|
-
const failures = [];
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import { join, dirname, resolve } from 'node:path';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
8
|
+
const scriptDir = dirname(scriptPath);
|
|
9
|
+
const defaultTestDir = join(scriptDir, '..', 'tests');
|
|
15
10
|
const failOutputPattern = /(?:❌\s*(?:FAIL|FAILED|FAILURE|TEST FAILED)|\bTEST FAILED\b)/i;
|
|
16
11
|
|
|
17
|
-
|
|
12
|
+
export function discoverTestFiles(testDir = defaultTestDir) {
|
|
13
|
+
return readdirSync(testDir)
|
|
14
|
+
.filter(file => file.endsWith('.mjs') && file !== 'setup-xml-provider.mjs')
|
|
15
|
+
.sort();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveTestConcurrency(value = process.env.DOCX_TEST_CONCURRENCY) {
|
|
19
|
+
if (value == null || value === '') {
|
|
20
|
+
const available = os.availableParallelism?.() ?? os.cpus().length;
|
|
21
|
+
return Math.max(1, Math.min(4, available));
|
|
22
|
+
}
|
|
23
|
+
if (!/^\d+$/.test(String(value)) || Number(value) < 1) {
|
|
24
|
+
throw new Error('DOCX_TEST_CONCURRENCY must be a positive integer.');
|
|
25
|
+
}
|
|
26
|
+
return Number(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function runTestFile(file, options = {}) {
|
|
30
|
+
const testDir = options.testDir || defaultTestDir;
|
|
31
|
+
const timeout = options.timeout ?? 30000;
|
|
18
32
|
const filePath = join(testDir, file);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
|
|
34
|
+
return new Promise(resolveResult => {
|
|
35
|
+
execFile(process.execPath, [filePath], {
|
|
36
|
+
cwd: options.cwd || join(scriptDir, '..'),
|
|
37
|
+
env: options.env || process.env,
|
|
38
|
+
timeout,
|
|
39
|
+
maxBuffer: options.maxBuffer ?? 1024 * 1024,
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
windowsHide: true
|
|
42
|
+
}, (error, stdout = '', stderr = '') => {
|
|
43
|
+
let executionError = error;
|
|
44
|
+
if (!executionError && failOutputPattern.test(stdout)) {
|
|
45
|
+
executionError = new Error(`Test printed a failure marker while exiting successfully:\n${stdout}`);
|
|
46
|
+
}
|
|
47
|
+
resolveResult({
|
|
48
|
+
file,
|
|
49
|
+
passed: !executionError,
|
|
50
|
+
stdout: String(stdout),
|
|
51
|
+
stderr: String(stderr),
|
|
52
|
+
error: executionError || null
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runTestFiles(files, options = {}) {
|
|
59
|
+
const orderedFiles = [...files].sort();
|
|
60
|
+
const concurrency = Math.min(resolveTestConcurrency(options.concurrency), Math.max(orderedFiles.length, 1));
|
|
61
|
+
const execute = options.runFile || runTestFile;
|
|
62
|
+
const results = new Array(orderedFiles.length);
|
|
63
|
+
let nextIndex = 0;
|
|
64
|
+
|
|
65
|
+
async function worker() {
|
|
66
|
+
while (true) {
|
|
67
|
+
const index = nextIndex++;
|
|
68
|
+
if (index >= orderedFiles.length) return;
|
|
69
|
+
results[index] = await execute(orderedFiles[index], options);
|
|
25
70
|
}
|
|
26
|
-
console.log('PASS');
|
|
27
|
-
passed++;
|
|
28
|
-
} catch (err) {
|
|
29
|
-
console.log('FAIL');
|
|
30
|
-
const stdout = err.stdout?.toString() || '';
|
|
31
|
-
const stderr = err.stderr?.toString() || '';
|
|
32
|
-
failures.push({ file, stderr: [stdout, stderr, err.message].filter(Boolean).join('\n') });
|
|
33
|
-
failed++;
|
|
34
71
|
}
|
|
72
|
+
|
|
73
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatTestRun(results) {
|
|
78
|
+
const lines = results.map(result => ` ${result.file} ... ${result.passed ? 'PASS' : 'FAIL'}`);
|
|
79
|
+
const failures = results.filter(result => !result.passed);
|
|
80
|
+
const passed = results.length - failures.length;
|
|
81
|
+
lines.push('', `${passed} passed, ${failures.length} failed out of ${results.length} tests`);
|
|
82
|
+
const failureLines = [];
|
|
83
|
+
for (const failure of failures) {
|
|
84
|
+
const detail = [failure.stdout, failure.stderr, failure.error?.message].filter(Boolean).join('\n');
|
|
85
|
+
failureLines.push('', `--- ${failure.file} ---`, detail);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
output: `${lines.join('\n')}\n`,
|
|
89
|
+
errorOutput: failureLines.length > 0 ? `${failureLines.join('\n')}\n` : '',
|
|
90
|
+
passed,
|
|
91
|
+
failed: failures.length
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function main(options = {}) {
|
|
96
|
+
const testDir = options.testDir || defaultTestDir;
|
|
97
|
+
const files = options.files || discoverTestFiles(testDir);
|
|
98
|
+
const results = await runTestFiles(files, { ...options, testDir });
|
|
99
|
+
const summary = formatTestRun(results);
|
|
100
|
+
const write = options.write || (text => process.stdout.write(text));
|
|
101
|
+
const writeError = options.writeError || (text => process.stderr.write(text));
|
|
102
|
+
write(summary.output);
|
|
103
|
+
if (summary.errorOutput) writeError(summary.errorOutput);
|
|
104
|
+
return summary;
|
|
35
105
|
}
|
|
36
106
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
107
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(scriptPath)) {
|
|
108
|
+
try {
|
|
109
|
+
const summary = await main();
|
|
110
|
+
if (summary.failed > 0) process.exitCode = 1;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(error?.stack || error?.message || String(error));
|
|
113
|
+
process.exitCode = 1;
|
|
41
114
|
}
|
|
42
|
-
|
|
43
|
-
}
|
|
115
|
+
}
|
|
@@ -37,6 +37,9 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
|
37
37
|
-SourcesDir $sourcesPath
|
|
38
38
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
39
39
|
|
|
40
|
+
# Clean up any orphaned background Word processes from prior aborted runs
|
|
41
|
+
Get-Process WINWORD -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -eq 0 } | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
42
|
+
|
|
40
43
|
& (Join-Path $repoRoot 'scripts\word-com-differential.ps1') `
|
|
41
44
|
-FixturesDir $fixturesPath `
|
|
42
45
|
-SourcesDir $sourcesPath
|
|
@@ -141,6 +141,62 @@ function Get-ScopeRevisionCount($document, [string]$scope) {
|
|
|
141
141
|
return $count
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
function Close-WordDocumentSafely($doc) {
|
|
145
|
+
if ($null -eq $doc) { return }
|
|
146
|
+
for ($attempt = 1; $attempt -le 5; $attempt++) {
|
|
147
|
+
try {
|
|
148
|
+
$doc.Close(0) | Out-Null
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
catch [System.Runtime.InteropServices.COMException] {
|
|
152
|
+
$hr = $_.Exception.HResult
|
|
153
|
+
# 0x80010001 = RPC_E_CALL_REJECTED (-2147418111)
|
|
154
|
+
if ($hr -eq -2147418111 -and $attempt -lt 5) {
|
|
155
|
+
Start-Sleep -Milliseconds 200
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($doc) | Out-Null
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function Stop-WordSafely($application) {
|
|
170
|
+
if ($null -eq $application) { return }
|
|
171
|
+
for ($attempt = 1; $attempt -le 5; $attempt++) {
|
|
172
|
+
try {
|
|
173
|
+
$application.Quit(0) | Out-Null
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
catch [System.Runtime.InteropServices.COMException] {
|
|
177
|
+
$hr = $_.Exception.HResult
|
|
178
|
+
# 0x80010001 = RPC_E_CALL_REJECTED (-2147418111)
|
|
179
|
+
# 0x80010108 = RPC_E_DISCONNECTED (-2147417848)
|
|
180
|
+
if ($hr -eq -2147417848) {
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
if ($hr -eq -2147418111 -and $attempt -lt 5) {
|
|
184
|
+
Start-Sleep -Milliseconds 300
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($application) | Out-Null
|
|
195
|
+
} catch {}
|
|
196
|
+
[System.GC]::Collect()
|
|
197
|
+
[System.GC]::WaitForPendingFinalizers()
|
|
198
|
+
}
|
|
199
|
+
|
|
144
200
|
try {
|
|
145
201
|
$word = New-Object -ComObject Word.Application
|
|
146
202
|
$word.Visible = $false
|
|
@@ -173,7 +229,8 @@ try {
|
|
|
173
229
|
$expectedRejected = Get-ScopeText $sourceDocument $assertionScope $fidelity
|
|
174
230
|
}
|
|
175
231
|
finally {
|
|
176
|
-
|
|
232
|
+
Close-WordDocumentSafely $sourceDocument
|
|
233
|
+
$sourceDocument = $null
|
|
177
234
|
}
|
|
178
235
|
$replacements = if ($expected.replacements) { @($expected.replacements) } else { @(@{
|
|
179
236
|
originalTarget = $expected.originalTarget
|
|
@@ -226,7 +283,8 @@ try {
|
|
|
226
283
|
$caseFailed = $true
|
|
227
284
|
}
|
|
228
285
|
finally {
|
|
229
|
-
|
|
286
|
+
Close-WordDocumentSafely $document
|
|
287
|
+
$document = $null
|
|
230
288
|
}
|
|
231
289
|
|
|
232
290
|
# Phase 2: fresh open, reject-all restores the original text.
|
|
@@ -252,7 +310,8 @@ try {
|
|
|
252
310
|
$caseFailed = $true
|
|
253
311
|
}
|
|
254
312
|
finally {
|
|
255
|
-
|
|
313
|
+
Close-WordDocumentSafely $document
|
|
314
|
+
$document = $null
|
|
256
315
|
}
|
|
257
316
|
}
|
|
258
317
|
|
|
@@ -267,7 +326,8 @@ try {
|
|
|
267
326
|
}
|
|
268
327
|
}
|
|
269
328
|
finally {
|
|
270
|
-
|
|
329
|
+
Stop-WordSafely $word
|
|
330
|
+
$word = $null
|
|
271
331
|
}
|
|
272
332
|
|
|
273
333
|
Write-Output ""
|
|
@@ -8,6 +8,9 @@ $exportScript = Join-Path $PSScriptRoot 'export-validation-fixtures.mjs'
|
|
|
8
8
|
$differentialScript = Join-Path $PSScriptRoot 'word-com-differential.ps1'
|
|
9
9
|
Push-Location $repoRoot
|
|
10
10
|
try {
|
|
11
|
+
# Clean up any orphaned background Word processes from prior aborted runs
|
|
12
|
+
Get-Process WINWORD -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -eq 0 } | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
13
|
+
|
|
11
14
|
& node $exportScript --output-dir $OutputDir
|
|
12
15
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
13
16
|
|