@ansonlai/docx-redline-js 0.2.0 → 0.4.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 +36 -10
- package/README.md +83 -6
- package/adapters/xml-adapter.js +73 -10
- package/core/list-targeting.js +3 -0
- package/core/paragraph-targeting.js +33 -7
- package/core/redline-validation.js +22 -0
- package/core/types.js +122 -27
- package/core/xml-query.js +3 -1
- package/dist/docx-redline-js.esm.js +1148 -572
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +79 -78
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +687 -0
- package/docs/VALIDATION.md +81 -2
- package/docs/WORD-MANUAL-REVIEW.md +138 -0
- package/docs/plans/2026-08-30-reliability-testing-improvements.md +488 -0
- package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +210 -0
- package/docs/plans/{2026-03-01-release-0.1.4-design.md → completed/2026-03-01-release-0.1.4-design.md} +2 -0
- package/docs/plans/{2026-03-01-release-0.1.4.md → completed/2026-03-01-release-0.1.4.md} +5 -3
- package/docs/plans/{2026-05-31-architectural changes.md → completed/2026-05-31-architectural changes.md } +2 -0
- package/docs/plans/completed/2026-08-02-reliability-improvements.md +1155 -0
- package/docs/test-comparison-dashboard.html +95 -0
- package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +22 -0
- package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +24 -0
- package/docs/validation-reports/2026-08-30-phase-3-coverage.md +73 -0
- package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +82 -0
- package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +114 -0
- package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +79 -0
- package/engine/format-extraction.js +1 -1
- package/engine/formatting-removal.js +95 -104
- package/engine/oxml-engine.js +176 -83
- package/engine/reconstruction-mapper.js +276 -79
- package/engine/reconstruction-mode.js +20 -6
- package/engine/reconstruction-writer.js +117 -72
- package/engine/run-builders.js +17 -13
- package/engine/surgical-diff-application.js +7 -21
- package/engine/surgical-mode.js +3 -2
- package/engine/table-mode.js +27 -16
- package/index.d.ts +95 -3
- package/index.js +14 -13
- package/orchestration/list-structural-fallback.js +16 -39
- package/package.json +23 -5
- package/pipeline/diff-engine.js +174 -55
- package/pipeline/ingestion-export.js +39 -24
- package/pipeline/ingestion-paragraph.js +7 -5
- package/pipeline/list-generation.js +27 -18
- package/pipeline/patching.js +2 -3
- package/pipeline/pipeline.js +65 -36
- package/pipeline/serialization.js +13 -5
- package/scripts/build-test-dashboard.mjs +43 -0
- package/scripts/check-types.mjs +16 -24
- package/scripts/export-validation-fixtures.mjs +191 -45
- package/scripts/fetch-superdoc-corpus.mjs +61 -0
- package/scripts/generate-test-dashboard.mjs +199 -0
- package/scripts/inspect-visual-evidence.mjs +271 -0
- package/scripts/lib/minimal-zip.mjs +199 -18
- package/scripts/lib/word-coverage-catalogue.mjs +207 -0
- package/scripts/lib/word-coverage-metadata.mjs +93 -0
- package/scripts/lib/zip-reader.mjs +64 -0
- package/scripts/package-superdoc-word-fixtures.ps1 +64 -0
- package/scripts/prepare-corpus-word-visual-review.mjs +84 -0
- package/scripts/prepare-superdoc-word-corpus.mjs +284 -0
- package/scripts/prepare-word-review.mjs +77 -0
- package/scripts/prepare-word-visual-review.mjs +90 -0
- package/scripts/render-agenda-multilevel.mjs +70 -0
- package/scripts/render-case22.mjs +73 -0
- package/scripts/render-case40.ps1 +35 -0
- package/scripts/render-multilevel-bullet-images.py +58 -0
- package/scripts/render-multilevel-bullet-visual.ps1 +32 -0
- package/scripts/render-multilevel-cases.mjs +80 -0
- package/scripts/report-coverage-gaps.mjs +103 -0
- package/scripts/report-word-coverage.mjs +71 -0
- package/scripts/sample-multimodal-visual-check.mjs +221 -0
- package/scripts/test-multilevel-bullet-visual.mjs +187 -0
- package/scripts/word-com-corpus-suite.ps1 +43 -0
- package/scripts/word-com-corpus-visual-suite.ps1 +116 -0
- package/scripts/word-com-differential.ps1 +158 -16
- package/scripts/word-com-suite.ps1 +19 -0
- package/scripts/word-com-visual-suite.ps1 +132 -0
- package/services/comment-engine.js +51 -46
- package/services/comment-locator.js +0 -1
- package/services/comment-package.js +11 -10
- package/services/numbering-service.js +1 -1
- package/services/revision-comment-management.js +31 -10
- package/services/standalone-docx-plumbing.js +45 -34
- package/services/standalone-operation-runner.js +315 -75
- package/services/table-reconciliation.js +23 -11
|
@@ -30,9 +30,10 @@ export async function executeListGeneration(options) {
|
|
|
30
30
|
originalRunModel = [],
|
|
31
31
|
originalText = '',
|
|
32
32
|
generateRedlines = true,
|
|
33
|
-
author = 'AI',
|
|
34
|
-
font = null,
|
|
35
|
-
|
|
33
|
+
author = 'AI',
|
|
34
|
+
font = null,
|
|
35
|
+
revisionIdAllocator = null,
|
|
36
|
+
numberingService
|
|
36
37
|
} = options;
|
|
37
38
|
|
|
38
39
|
const normalizedListText = normalizeCompositeListMarkers(cleanText);
|
|
@@ -72,13 +73,18 @@ export async function executeListGeneration(options) {
|
|
|
72
73
|
if (tableData.headers.length > 0 || tableData.rows.length > 0) {
|
|
73
74
|
if (generateRedlines && results.length === 0 && deletionRuns.length > 0) {
|
|
74
75
|
results.push(serializeToOoxml(deletionRuns, null, [], {
|
|
75
|
-
author,
|
|
76
|
-
generateRedlines,
|
|
77
|
-
font
|
|
78
|
-
|
|
76
|
+
author,
|
|
77
|
+
generateRedlines,
|
|
78
|
+
font,
|
|
79
|
+
revisionIdAllocator
|
|
80
|
+
}));
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
results.push(generateTableOoxml(tableData, {
|
|
83
|
+
results.push(generateTableOoxml(tableData, {
|
|
84
|
+
generateRedlines,
|
|
85
|
+
author,
|
|
86
|
+
revisionIdAllocator
|
|
87
|
+
}));
|
|
82
88
|
i = tableBlock.endIndex;
|
|
83
89
|
continue;
|
|
84
90
|
}
|
|
@@ -92,9 +98,10 @@ export async function executeListGeneration(options) {
|
|
|
92
98
|
numberingContext,
|
|
93
99
|
numberingService,
|
|
94
100
|
generateRedlines,
|
|
95
|
-
author,
|
|
96
|
-
font,
|
|
97
|
-
|
|
101
|
+
author,
|
|
102
|
+
font,
|
|
103
|
+
revisionIdAllocator,
|
|
104
|
+
deletionRuns
|
|
98
105
|
);
|
|
99
106
|
results.push(entry.ooxml);
|
|
100
107
|
}
|
|
@@ -226,9 +233,10 @@ function buildListEntry(
|
|
|
226
233
|
numberingContext,
|
|
227
234
|
numberingService,
|
|
228
235
|
generateRedlines,
|
|
229
|
-
author,
|
|
230
|
-
font,
|
|
231
|
-
|
|
236
|
+
author,
|
|
237
|
+
font,
|
|
238
|
+
revisionIdAllocator,
|
|
239
|
+
deletionRuns
|
|
232
240
|
) {
|
|
233
241
|
let pPrXml = '';
|
|
234
242
|
let segmentText = '';
|
|
@@ -272,9 +280,10 @@ function buildListEntry(
|
|
|
272
280
|
|
|
273
281
|
return {
|
|
274
282
|
ooxml: serializeToOoxml(runModel, pPrXml, formatHints, {
|
|
275
|
-
author,
|
|
276
|
-
generateRedlines,
|
|
277
|
-
font
|
|
278
|
-
|
|
283
|
+
author,
|
|
284
|
+
generateRedlines,
|
|
285
|
+
font,
|
|
286
|
+
revisionIdAllocator
|
|
287
|
+
})
|
|
279
288
|
};
|
|
280
289
|
}
|
package/pipeline/patching.js
CHANGED
|
@@ -182,9 +182,8 @@ export function applyPatches(splitModel, diffOps, options) {
|
|
|
182
182
|
|
|
183
183
|
function processInsertionOperation(context) {
|
|
184
184
|
const {
|
|
185
|
-
insertOp,
|
|
186
|
-
|
|
187
|
-
styleLookup,
|
|
185
|
+
insertOp,
|
|
186
|
+
styleLookup,
|
|
188
187
|
patchedModel,
|
|
189
188
|
state,
|
|
190
189
|
options,
|
package/pipeline/pipeline.js
CHANGED
|
@@ -16,7 +16,7 @@ import { detectNumberingContext } from './ingestion.js';
|
|
|
16
16
|
import { generateTableOoxml } from '../services/table-reconciliation.js';
|
|
17
17
|
import { executeListGeneration, detectIndentationStep } from './list-generation.js';
|
|
18
18
|
import { detectContentType, parseListItems, parseTable } from './content-analysis.js';
|
|
19
|
-
import {
|
|
19
|
+
import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
20
20
|
import { log, error as logError } from '../adapters/logger.js';
|
|
21
21
|
import { getFirstElementByTagNS, getXmlParseError } from '../core/xml-query.js';
|
|
22
22
|
import { getPlatform } from '../adapters/config.js';
|
|
@@ -54,6 +54,7 @@ export class ReconciliationPipeline {
|
|
|
54
54
|
this.validationMode = options.validationMode ?? 'auto';
|
|
55
55
|
this.numberingService = options.numberingService || new NumberingService();
|
|
56
56
|
this.font = options.font || null;
|
|
57
|
+
this.revisionIdAllocator = options.revisionIdAllocator || null;
|
|
57
58
|
this.platform = options.platform ?? getPlatform();
|
|
58
59
|
this.isWebPlatform = options.isWebPlatform ?? isWebPlatform(this.platform);
|
|
59
60
|
this.enableEventLoopYielding = options.enableEventLoopYielding ?? this.isWebPlatform;
|
|
@@ -75,10 +76,20 @@ export class ReconciliationPipeline {
|
|
|
75
76
|
|
|
76
77
|
try {
|
|
77
78
|
// Stage 1: Ingest OOXML
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const parsed = options.xmlDoc
|
|
80
|
+
? { doc: options.xmlDoc, error: null, warnings: [] }
|
|
81
|
+
: parseOoxmlSafe(originalOoxml, 'application/xml');
|
|
82
|
+
if (parsed.error || !parsed.doc) {
|
|
83
|
+
return {
|
|
84
|
+
ooxml: originalOoxml,
|
|
85
|
+
isValid: false,
|
|
86
|
+
status: 'error',
|
|
87
|
+
error: parsed.error,
|
|
88
|
+
warnings: parsed.warnings || []
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
warnings.push(...(parsed.warnings || []));
|
|
92
|
+
const doc = parsed.doc;
|
|
82
93
|
const pElement = getFirstElementByTagNS(doc, '*', 'p');
|
|
83
94
|
|
|
84
95
|
const { runModel, acceptedText, pPr } = ingestOoxml(originalOoxml, { xmlDoc: doc });
|
|
@@ -122,19 +133,28 @@ export class ReconciliationPipeline {
|
|
|
122
133
|
}
|
|
123
134
|
await this.maybeYield(runModel.length, Math.max(acceptedText.length, cleanText.length));
|
|
124
135
|
|
|
125
|
-
// Count actual paragraph elements ingested
|
|
126
|
-
const paragraphCount = runModel.filter(r => r.kind === RunKind.PARAGRAPH_START).length;
|
|
127
|
-
|
|
136
|
+
// Count actual paragraph elements ingested
|
|
137
|
+
const paragraphCount = runModel.filter(r => r.kind === RunKind.PARAGRAPH_START).length;
|
|
138
|
+
const sourceIsMarkedMultilineList = isListTargetStrict(acceptedText) || isListTargetLoose(acceptedText);
|
|
139
|
+
const canPatchExistingMarkedList = isTargetList
|
|
140
|
+
&& sourceIsMarkedMultilineList
|
|
141
|
+
&& paragraphCount > 1
|
|
142
|
+
&& acceptedText !== cleanText;
|
|
143
|
+
|
|
128
144
|
log(`[Reconcile] isTargetList: ${isTargetList}, paragraphCount: ${paragraphCount}`);
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
log(
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
|
|
146
|
+
// Preserve already-marked multi-paragraph lists during ordinary text edits.
|
|
147
|
+
// Rebuilding the whole block would discard unchanged run formatting and
|
|
148
|
+
// place all source deletion runs in the first generated paragraph, which
|
|
149
|
+
// makes rejection merge the original paragraph boundaries.
|
|
150
|
+
if (canPatchExistingMarkedList) {
|
|
151
|
+
log('[Reconcile] Existing marked list edit detected; using run-aware patching to preserve formatting and paragraph boundaries.');
|
|
152
|
+
} else if (isTargetList) {
|
|
153
|
+
log('[Reconcile] 🎯 ENTERING LIST GENERATION PATH');
|
|
154
|
+
log(`[Reconcile] cleanText preview: ${cleanText.substring(0, 100)}...`);
|
|
155
|
+
log(`[Reconcile] acceptedText preview: ${acceptedText.substring(0, 100)}...`);
|
|
156
|
+
return this.executeListGeneration(cleanText, numberingContext, runModel);
|
|
157
|
+
}
|
|
138
158
|
|
|
139
159
|
log(`[Reconcile] Computed ${diffOps.length} diff operations`);
|
|
140
160
|
|
|
@@ -153,9 +173,10 @@ export class ReconciliationPipeline {
|
|
|
153
173
|
await this.maybeYield(patchedModel.length, Math.max(acceptedText.length, cleanText.length));
|
|
154
174
|
|
|
155
175
|
// Stage 6: Serialize to OOXML
|
|
156
|
-
const resultOoxml = serializeToOoxml(patchedModel, pPr, formatHints, {
|
|
157
|
-
author: this.author,
|
|
158
|
-
generateRedlines: this.generateRedlines
|
|
176
|
+
const resultOoxml = serializeToOoxml(patchedModel, pPr, formatHints, {
|
|
177
|
+
author: this.author,
|
|
178
|
+
generateRedlines: this.generateRedlines,
|
|
179
|
+
revisionIdAllocator: this.revisionIdAllocator
|
|
159
180
|
});
|
|
160
181
|
|
|
161
182
|
// Stage 7: Basic validation
|
|
@@ -172,14 +193,15 @@ export class ReconciliationPipeline {
|
|
|
172
193
|
warnings
|
|
173
194
|
};
|
|
174
195
|
|
|
175
|
-
} catch (error) {
|
|
176
|
-
logError('[Reconcile] Pipeline error:', error);
|
|
177
|
-
return {
|
|
178
|
-
ooxml: originalOoxml,
|
|
179
|
-
isValid: false,
|
|
180
|
-
warnings: [`Pipeline error: ${error.message}`]
|
|
181
|
-
|
|
182
|
-
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logError('[Reconcile] Pipeline error:', error);
|
|
198
|
+
return {
|
|
199
|
+
ooxml: originalOoxml,
|
|
200
|
+
isValid: false,
|
|
201
|
+
warnings: [`Pipeline error: ${error.message}`],
|
|
202
|
+
error: error?.code ? { code: error.code, message: error.message } : undefined
|
|
203
|
+
};
|
|
204
|
+
}
|
|
183
205
|
}
|
|
184
206
|
|
|
185
207
|
/**
|
|
@@ -194,8 +216,13 @@ export class ReconciliationPipeline {
|
|
|
194
216
|
try {
|
|
195
217
|
// Check for well-formed XML by wrapping in namespace container
|
|
196
218
|
const wrappedXml = `<root xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${ooxml}</root>`;
|
|
197
|
-
const
|
|
198
|
-
const doc =
|
|
219
|
+
const parsed = parseOoxmlSafe(wrappedXml, 'application/xml');
|
|
220
|
+
const doc = parsed.doc;
|
|
221
|
+
|
|
222
|
+
if (parsed.error || !doc) {
|
|
223
|
+
errors.push('Generated OOXML is not well-formed XML: ' + (parsed.error?.message || 'parse error'));
|
|
224
|
+
return { isValid: false, errors };
|
|
225
|
+
}
|
|
199
226
|
|
|
200
227
|
const parseError = getXmlParseError(doc);
|
|
201
228
|
if (parseError) {
|
|
@@ -276,9 +303,10 @@ export class ReconciliationPipeline {
|
|
|
276
303
|
originalRunModel,
|
|
277
304
|
originalText,
|
|
278
305
|
generateRedlines: this.generateRedlines,
|
|
279
|
-
author: this.author,
|
|
280
|
-
font: this.font,
|
|
281
|
-
|
|
306
|
+
author: this.author,
|
|
307
|
+
font: this.font,
|
|
308
|
+
revisionIdAllocator: this.revisionIdAllocator,
|
|
309
|
+
numberingService: this.numberingService
|
|
282
310
|
});
|
|
283
311
|
}
|
|
284
312
|
|
|
@@ -309,9 +337,10 @@ export class ReconciliationPipeline {
|
|
|
309
337
|
};
|
|
310
338
|
}
|
|
311
339
|
|
|
312
|
-
const tableOoxml = generateTableOoxml(tableData, {
|
|
313
|
-
generateRedlines: this.generateRedlines,
|
|
314
|
-
author: this.author
|
|
340
|
+
const tableOoxml = generateTableOoxml(tableData, {
|
|
341
|
+
generateRedlines: this.generateRedlines,
|
|
342
|
+
author: this.author,
|
|
343
|
+
revisionIdAllocator: this.revisionIdAllocator
|
|
315
344
|
});
|
|
316
345
|
|
|
317
346
|
return {
|
|
@@ -24,7 +24,7 @@ const XMLNS_ATTR_REGEX = /\s+xmlns:[^=]+="[^"]*"/g;
|
|
|
24
24
|
*/
|
|
25
25
|
export function serializeToOoxml(patchedModel, pPr, formatHints = [], options = {}) {
|
|
26
26
|
const serializationOptions = normalizeSerializationOptions(options);
|
|
27
|
-
const {
|
|
27
|
+
const { generateRedlines } = serializationOptions;
|
|
28
28
|
const paragraphs = [];
|
|
29
29
|
let currentPPrXml = '';
|
|
30
30
|
let currentPPrElement = null;
|
|
@@ -140,7 +140,8 @@ function normalizeSerializationOptions(options) {
|
|
|
140
140
|
return {
|
|
141
141
|
author: getDefaultAuthor(),
|
|
142
142
|
generateRedlines: true,
|
|
143
|
-
font: options
|
|
143
|
+
font: options,
|
|
144
|
+
revisionIdAllocator: null
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
|
|
@@ -152,7 +153,8 @@ function normalizeSerializationOptions(options) {
|
|
|
152
153
|
return {
|
|
153
154
|
author: resolvedAuthor,
|
|
154
155
|
generateRedlines: normalized.generateRedlines ?? true,
|
|
155
|
-
font: normalized.font ?? null
|
|
156
|
+
font: normalized.font ?? null,
|
|
157
|
+
revisionIdAllocator: normalized.revisionIdAllocator ?? null
|
|
156
158
|
};
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -228,7 +230,10 @@ function buildSimpleRun(text, rPrXml) {
|
|
|
228
230
|
* @returns {string}
|
|
229
231
|
*/
|
|
230
232
|
function buildDeletionXml(item, options = {}) {
|
|
231
|
-
const metadata = createRevisionMetadata(
|
|
233
|
+
const metadata = createRevisionMetadata(
|
|
234
|
+
options.author ?? getDefaultAuthor(),
|
|
235
|
+
options.revisionIdAllocator
|
|
236
|
+
);
|
|
232
237
|
const font = options.font ?? null;
|
|
233
238
|
let rPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : '';
|
|
234
239
|
|
|
@@ -250,7 +255,10 @@ function buildDeletionXml(item, options = {}) {
|
|
|
250
255
|
* @returns {string}
|
|
251
256
|
*/
|
|
252
257
|
function buildInsertionXml(item, formatHints, options = {}) {
|
|
253
|
-
const metadata = createRevisionMetadata(
|
|
258
|
+
const metadata = createRevisionMetadata(
|
|
259
|
+
options.author ?? getDefaultAuthor(),
|
|
260
|
+
options.revisionIdAllocator
|
|
261
|
+
);
|
|
254
262
|
const font = options.font ?? null;
|
|
255
263
|
|
|
256
264
|
// Build the inner run content with format hints
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
const repoRoot = process.cwd();
|
|
6
|
+
const syntheticDir = join(repoRoot, 'tmp', 'dashboard-docx');
|
|
7
|
+
const corpusSourceDir = join(repoRoot, 'tmp', 'superdoc-corpus');
|
|
8
|
+
const corpusFixturesArgIndex = process.argv.indexOf('--corpus-fixtures-dir');
|
|
9
|
+
if (corpusFixturesArgIndex >= 0 && !process.argv[corpusFixturesArgIndex + 1]) {
|
|
10
|
+
throw new Error('--corpus-fixtures-dir requires a path');
|
|
11
|
+
}
|
|
12
|
+
const suppliedCorpusFixturesDir = corpusFixturesArgIndex >= 0;
|
|
13
|
+
const corpusFixturesDir = suppliedCorpusFixturesDir
|
|
14
|
+
? resolve(repoRoot, process.argv[corpusFixturesArgIndex + 1])
|
|
15
|
+
: join(repoRoot, 'tmp', 'superdoc-word-fixtures');
|
|
16
|
+
const manifest = JSON.parse(readFileSync(
|
|
17
|
+
join(repoRoot, 'tests', 'corpus', 'superdoc-english-legal-administrative.json'),
|
|
18
|
+
'utf8'
|
|
19
|
+
));
|
|
20
|
+
const run = (script, args = []) => execFileSync(process.execPath, [script, ...args], {
|
|
21
|
+
cwd: repoRoot,
|
|
22
|
+
stdio: 'inherit'
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
run('scripts/export-validation-fixtures.mjs', ['--output-dir', syntheticDir]);
|
|
26
|
+
|
|
27
|
+
const corpusReady = manifest.documents.every(item =>
|
|
28
|
+
existsSync(join(corpusSourceDir, `${item.id}.docx`))
|
|
29
|
+
);
|
|
30
|
+
if (corpusReady && suppliedCorpusFixturesDir) {
|
|
31
|
+
if (!existsSync(join(corpusFixturesDir, 'suite.json'))) {
|
|
32
|
+
throw new Error(`Supplied corpus fixture directory has no suite.json: ${corpusFixturesDir}`);
|
|
33
|
+
}
|
|
34
|
+
} else if (corpusReady) {
|
|
35
|
+
run('scripts/prepare-superdoc-word-corpus.mjs');
|
|
36
|
+
} else {
|
|
37
|
+
console.warn('Real-document corpus is not downloaded; embedding synthetic DOCX previews only.');
|
|
38
|
+
console.warn('Run npm run test:corpus:word once to fetch and validate the pinned corpus.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const args = ['scripts/generate-test-dashboard.mjs', '--fixtures-dir', syntheticDir];
|
|
42
|
+
if (corpusReady) args.push('--corpus-fixtures-dir', corpusFixturesDir);
|
|
43
|
+
run(args[0], args.slice(1));
|
package/scripts/check-types.mjs
CHANGED
|
@@ -1,29 +1,21 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
'export function acceptTrackedChangesInOoxml',
|
|
10
|
-
'export function rejectTrackedChangesInOoxml',
|
|
11
|
-
'export function deleteCommentsByAuthorInOoxml',
|
|
12
|
-
'export function validateRedlineOoxml'
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
for (const snippet of requiredSnippets) {
|
|
16
|
-
if (!dts.includes(snippet)) {
|
|
17
|
-
throw new Error(`Missing declaration snippet: ${snippet}`);
|
|
18
|
-
}
|
|
3
|
+
const declarationPath = new URL('../index.d.ts', import.meta.url);
|
|
4
|
+
const declarationText = readFileSync(declarationPath, 'utf8');
|
|
5
|
+
const declaredRuntimeNames = new Set();
|
|
6
|
+
const declarationPattern = /^export\s+(?:declare\s+)?(?:function|class|const|let|var|enum)\s+([A-Za-z_$][\w$]*)/gm;
|
|
7
|
+
for (const match of declarationText.matchAll(declarationPattern)) {
|
|
8
|
+
declaredRuntimeNames.add(match[1]);
|
|
19
9
|
}
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
11
|
+
const runtimeModule = await import('../index.js');
|
|
12
|
+
const runtimeNames = Object.keys(runtimeModule).sort();
|
|
13
|
+
const missingDeclarations = runtimeNames.filter(name => !declaredRuntimeNames.has(name));
|
|
14
|
+
|
|
15
|
+
if (missingDeclarations.length > 0) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Runtime exports missing from index.d.ts:\n${missingDeclarations.map(name => `- ${name}`).join('\n')}`
|
|
18
|
+
);
|
|
26
19
|
}
|
|
27
|
-
if (balance !== 0) throw new Error('index.d.ts has unbalanced braces');
|
|
28
20
|
|
|
29
|
-
console.log(
|
|
21
|
+
console.log(`PASS: ${runtimeNames.length} runtime exports have declarations`);
|