@ansonlai/docx-redline-js 0.4.0 → 0.5.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 +589 -287
- 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/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 +2849 -466
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +87 -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/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 +491 -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 +222 -37
- package/engine/surgical-mode.js +134 -6
- 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 +317 -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 +24 -3
- 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-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 +494 -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 +323 -0
- package/services/document-operation-mutations.js +1733 -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 +366 -0
- package/services/receipt-collector.js +288 -0
- package/services/revision-comment-management.js +37 -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
|
@@ -17,6 +17,8 @@ function parseArgs(argv) {
|
|
|
17
17
|
if (fixturesIndex >= 0 && !argv[fixturesIndex + 1]) throw new Error('--fixtures-dir requires a path');
|
|
18
18
|
const corpusIndex = argv.indexOf('--corpus-fixtures-dir');
|
|
19
19
|
if (corpusIndex >= 0 && !argv[corpusIndex + 1]) throw new Error('--corpus-fixtures-dir requires a path');
|
|
20
|
+
const lane1Index = argv.indexOf('--lane1-fixtures-dir');
|
|
21
|
+
if (lane1Index >= 0 && !argv[lane1Index + 1]) throw new Error('--lane1-fixtures-dir requires a path');
|
|
20
22
|
return {
|
|
21
23
|
outputPath: outputIndex >= 0
|
|
22
24
|
? resolve(process.cwd(), argv[outputIndex + 1])
|
|
@@ -26,11 +28,14 @@ function parseArgs(argv) {
|
|
|
26
28
|
: join(process.cwd(), 'tmp', 'dashboard-docx'),
|
|
27
29
|
corpusFixturesDir: corpusIndex >= 0
|
|
28
30
|
? resolve(process.cwd(), argv[corpusIndex + 1])
|
|
29
|
-
: null
|
|
31
|
+
: null,
|
|
32
|
+
lane1FixturesDir: lane1Index >= 0
|
|
33
|
+
? resolve(process.cwd(), argv[lane1Index + 1])
|
|
34
|
+
: (existsSync(join(process.cwd(), 'tmp', 'lane1-docx', 'manifest.json')) ? join(process.cwd(), 'tmp', 'lane1-docx') : null)
|
|
30
35
|
};
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
export function buildDashboardData(fixturesDir = null, corpusFixturesDir = null) {
|
|
38
|
+
export function buildDashboardData(fixturesDir = null, corpusFixturesDir = null, lane1FixturesDir = null) {
|
|
34
39
|
const { cases, priorities } = loadCoverageCatalogue();
|
|
35
40
|
const corpusSuitePath = corpusFixturesDir ? join(corpusFixturesDir, 'suite.json') : null;
|
|
36
41
|
const corpusSuite = corpusSuitePath && existsSync(corpusSuitePath)
|
|
@@ -40,6 +45,28 @@ export function buildDashboardData(fixturesDir = null, corpusFixturesDir = null)
|
|
|
40
45
|
const visualEligible = new Set(
|
|
41
46
|
selectVisualReviewCases().map(testCase => `synthetic:${testCase.name}`)
|
|
42
47
|
);
|
|
48
|
+
|
|
49
|
+
const resolvedLane1Dir = lane1FixturesDir || (existsSync(join(process.cwd(), 'tmp', 'lane1-docx', 'manifest.json')) ? join(process.cwd(), 'tmp', 'lane1-docx') : null);
|
|
50
|
+
let lane1Cases = [];
|
|
51
|
+
if (resolvedLane1Dir && existsSync(join(resolvedLane1Dir, 'manifest.json'))) {
|
|
52
|
+
const lane1Manifest = JSON.parse(readFileSync(join(resolvedLane1Dir, 'manifest.json'), 'utf8'));
|
|
53
|
+
lane1Cases = (lane1Manifest.cases || []).map(item => {
|
|
54
|
+
const readBase64 = suffix => {
|
|
55
|
+
const path = join(resolvedLane1Dir, `${item.name}${suffix}.docx`);
|
|
56
|
+
return existsSync(path) ? readFileSync(path).toString('base64') : null;
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
...item,
|
|
60
|
+
fixtureGeneratedAt: lane1Manifest.generatedAt || null,
|
|
61
|
+
docxVariants: {
|
|
62
|
+
source: readBase64('.source'),
|
|
63
|
+
tracked: readBase64(''),
|
|
64
|
+
accepted: readBase64('.accepted'),
|
|
65
|
+
rejected: readBase64('.rejected')
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
43
70
|
return {
|
|
44
71
|
generatedAt: new Date().toISOString(),
|
|
45
72
|
tasks: COVERAGE_TASKS,
|
|
@@ -96,18 +123,127 @@ export function buildDashboardData(fixturesDir = null, corpusFixturesDir = null)
|
|
|
96
123
|
formatting: (documentXml.match(/<w:(?:rPrChange|pPrChange)\b/g) || []).length
|
|
97
124
|
}
|
|
98
125
|
};
|
|
99
|
-
})
|
|
126
|
+
}),
|
|
127
|
+
lane1Cases
|
|
100
128
|
};
|
|
101
129
|
}
|
|
102
130
|
|
|
103
131
|
function escapeJsonForHtml(value) {
|
|
104
132
|
return JSON.stringify(value).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
|
|
133
|
+
}export function patchDocxPreview(rawSource) {
|
|
134
|
+
let s = rawSource;
|
|
135
|
+
// 1. Hyperlink element parsing: support ins, del, moveFrom, moveTo
|
|
136
|
+
s = s.replace(
|
|
137
|
+
/parseHyperlink\(node,\s*parent\)\s*\{[\s\S]*?for\s*\(const\s+c\s+of\s+globalXmlParser\.elements\(node\)\)\s*\{[\s\S]*?switch\s*\(c\.localName\)\s*\{[\s\S]*?case\s*"r":[\s\S]*?break;\s*\}\s*\}\s*return\s+result;\s*\}/,
|
|
138
|
+
`parseHyperlink(node, parent) {
|
|
139
|
+
var result = { type: DomType.Hyperlink, parent: parent, children: [] };
|
|
140
|
+
result.anchor = globalXmlParser.attr(node, "anchor");
|
|
141
|
+
result.id = globalXmlParser.attr(node, "id");
|
|
142
|
+
for (const c of globalXmlParser.elements(node)) {
|
|
143
|
+
switch (c.localName) {
|
|
144
|
+
case "r":
|
|
145
|
+
result.children.push(this.parseRun(c, result));
|
|
146
|
+
break;
|
|
147
|
+
case "moveTo":
|
|
148
|
+
case "ins":
|
|
149
|
+
result.children.push(this.parseChange(DomType.Inserted, c, e => this.parseParagraph(e)));
|
|
150
|
+
break;
|
|
151
|
+
case "moveFrom":
|
|
152
|
+
case "del":
|
|
153
|
+
result.children.push(this.parseChange(DomType.Deleted, c, e => this.parseParagraph(e)));
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}`
|
|
159
|
+
);
|
|
160
|
+
// 2. SmartTag element parsing: support ins, del, moveFrom, moveTo
|
|
161
|
+
s = s.replace(
|
|
162
|
+
/parseSmartTag\(node,\s*parent\)\s*\{[\s\S]*?for\s*\(const\s+c\s+of\s+globalXmlParser\.elements\(node\)\)\s*\{[\s\S]*?case\s*"r":[\s\S]*?case\s*"smartTag":[\s\S]*?break;\s*\}\s*\}\s*return\s+result;\s*\}/,
|
|
163
|
+
`parseSmartTag(node, parent) {
|
|
164
|
+
var result = { type: DomType.SmartTag, parent, children: [] };
|
|
165
|
+
var uri = globalXmlParser.attr(node, "uri");
|
|
166
|
+
var element = globalXmlParser.attr(node, "element");
|
|
167
|
+
if (uri) result.uri = uri;
|
|
168
|
+
if (element) result.element = element;
|
|
169
|
+
for (const c of globalXmlParser.elements(node)) {
|
|
170
|
+
switch (c.localName) {
|
|
171
|
+
case "r":
|
|
172
|
+
result.children.push(this.parseRun(c, result));
|
|
173
|
+
break;
|
|
174
|
+
case "smartTag":
|
|
175
|
+
result.children.push(this.parseSmartTag(c, result));
|
|
176
|
+
break;
|
|
177
|
+
case "moveTo":
|
|
178
|
+
case "ins":
|
|
179
|
+
result.children.push(this.parseChange(DomType.Inserted, c, e => this.parseParagraph(e)));
|
|
180
|
+
break;
|
|
181
|
+
case "moveFrom":
|
|
182
|
+
case "del":
|
|
183
|
+
result.children.push(this.parseChange(DomType.Deleted, c, e => this.parseParagraph(e)));
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}`
|
|
189
|
+
);
|
|
190
|
+
// 3. Paragraph level: support moveFrom and moveTo as del/ins
|
|
191
|
+
s = s.replace(
|
|
192
|
+
/case "ins":\s*result\.children\.push\(this\.parseChange\(DomType\.Inserted,\s*el,\s*e\s*=>\s*this\.parseParagraph\(e\)\)\);\s*break;\s*case "del":\s*result\.children\.push\(this\.parseChange\(DomType\.Deleted,\s*el,\s*e\s*=>\s*this\.parseParagraph\(e\)\)\);\s*break;/,
|
|
193
|
+
`case "moveTo":
|
|
194
|
+
case "ins":
|
|
195
|
+
result.children.push(this.parseChange(DomType.Inserted, el, e => this.parseParagraph(e)));
|
|
196
|
+
break;
|
|
197
|
+
case "moveFrom":
|
|
198
|
+
case "del":
|
|
199
|
+
result.children.push(this.parseChange(DomType.Deleted, el, e => this.parseParagraph(e)));
|
|
200
|
+
break;`
|
|
201
|
+
);
|
|
202
|
+
// 4. Body element level: support block-level ins and del
|
|
203
|
+
s = s.replace(
|
|
204
|
+
/case "tbl":\s*children\.push\(this\.parseTable\(elem\)\);\s*break;\s*case "sdt":\s*children\.push\(\.\.\.this\.parseSdt\(elem,\s*e\s*=>\s*this\.parseBodyElements\(e\)\)\);\s*break;/,
|
|
205
|
+
`case "tbl":
|
|
206
|
+
children.push(this.parseTable(elem));
|
|
207
|
+
break;
|
|
208
|
+
case "sdt":
|
|
209
|
+
children.push(...this.parseSdt(elem, e => this.parseBodyElements(e)));
|
|
210
|
+
break;
|
|
211
|
+
case "moveTo":
|
|
212
|
+
case "ins":
|
|
213
|
+
children.push(this.parseChange(DomType.Inserted, elem, e => ({ children: this.parseBodyElements(e) })));
|
|
214
|
+
break;
|
|
215
|
+
case "moveFrom":
|
|
216
|
+
case "del":
|
|
217
|
+
children.push(this.parseChange(DomType.Deleted, elem, e => ({ children: this.parseBodyElements(e) })));
|
|
218
|
+
break;`
|
|
219
|
+
);
|
|
220
|
+
// 5. Table rows: support trPr ins and del
|
|
221
|
+
s = s.replace(
|
|
222
|
+
/case "trPr":\s*case "tblPrEx":\s*this\.parseTableRowProperties\(c,\s*result\);\s*break;/,
|
|
223
|
+
`case "trPr":
|
|
224
|
+
case "tblPrEx":
|
|
225
|
+
this.parseTableRowProperties(c, result);
|
|
226
|
+
if (globalXmlParser.element(c, "ins")) result.className = (result.className ? result.className + " " : "") + "docx-ins-row";
|
|
227
|
+
if (globalXmlParser.element(c, "del")) result.className = (result.className ? result.className + " " : "") + "docx-del-row";
|
|
228
|
+
break;`
|
|
229
|
+
);
|
|
230
|
+
// 6. Formatting revisions (rPrChange)
|
|
231
|
+
s = s.replace(
|
|
232
|
+
/case "vertAlign":\s*run\.verticalAlign\s*=\s*values\.valueOfVertAlign\(c,\s*true\);\s*break;/,
|
|
233
|
+
`case "vertAlign":
|
|
234
|
+
run.verticalAlign = values.valueOfVertAlign(c, true);
|
|
235
|
+
break;
|
|
236
|
+
case "rPrChange":
|
|
237
|
+
run.className = (run.className ? run.className + " " : "") + "docx-format-change";
|
|
238
|
+
break;`
|
|
239
|
+
);
|
|
240
|
+
return s;
|
|
105
241
|
}
|
|
106
242
|
|
|
107
243
|
export function renderDashboardHtml(data, libraries = {}) {
|
|
108
244
|
const encoded = escapeJsonForHtml(data);
|
|
109
245
|
const jszipSource = String(libraries.jszipSource || '').replace(/<\/script/gi, '<\\/script');
|
|
110
|
-
const docxPreviewSource = String(libraries.docxPreviewSource || '').replace(/<\/script/gi, '<\\/script');
|
|
246
|
+
const docxPreviewSource = patchDocxPreview(String(libraries.docxPreviewSource || '')).replace(/<\/script/gi, '<\\/script');
|
|
111
247
|
return `<!doctype html>
|
|
112
248
|
<html lang="en">
|
|
113
249
|
<head>
|
|
@@ -117,8 +253,17 @@ export function renderDashboardHtml(data, libraries = {}) {
|
|
|
117
253
|
<style>
|
|
118
254
|
:root{color-scheme:light dark;--bg:#f4f7fb;--surface:#fff;--surface2:#eef3f9;--text:#172033;--muted:#68758a;--line:#d9e1ec;--blue:#2563eb;--blue2:#dbeafe;--green:#138a5b;--green2:#d9f5e9;--amber:#b66a00;--amber2:#fff1cf;--red:#be3b45;--red2:#ffe2e5;--purple:#7656c8;--purple2:#eee8ff;--shadow:0 12px 30px rgba(38,55,80,.09)}
|
|
119
255
|
@media(prefers-color-scheme:dark){:root{--bg:#10141d;--surface:#181e29;--surface2:#222a37;--text:#edf2fb;--muted:#a8b3c5;--line:#323c4d;--blue:#78a7ff;--blue2:#223d68;--green:#58d2a0;--green2:#183f33;--amber:#ffc66d;--amber2:#533d1e;--red:#ff929a;--red2:#55282d;--purple:#b9a1ff;--purple2:#382f59;--shadow:none}}
|
|
120
|
-
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}button,select,input{font:inherit;color:inherit}.shell{max-width:1480px;margin:auto;padding:28px;transition:max-width .18s ease}.top{display:flex;justify-content:space-between;gap:24px;align-items:end;margin-bottom:22px}.eyebrow{text-transform:uppercase;letter-spacing:.12em;color:var(--blue);font-weight:700;font-size:11px}h1{margin:4px 0 2px;font-size:clamp(25px,4vw,40px);line-height:1.12}h2{font-size:18px;margin:0 0 14px}.stamp{color:var(--muted);text-align:right}.controls{display:flex;flex-wrap:wrap;gap:10px;margin:0 0 18px}.controls label{display:flex;align-items:center;gap:7px;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 11px}.controls select,.controls input{border:0;background:transparent;outline:none}.stats{display:grid;grid-template-columns:repeat(6,minmax(120px,1fr));gap:12px;margin-bottom:18px}.stat,.panel{background:var(--surface);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}.stat{padding:16px}.stat strong{display:block;font-size:25px;line-height:1.1}.stat span{color:var(--muted);font-size:12px}.layout{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(300px,.7fr);gap:18px}.sidebar-hidden .shell{max-width:none}.sidebar-hidden .layout{grid-template-columns:minmax(0,1fr)}.sidebar-hidden #dashboard-sidebar{display:none}.panel{padding:18px;margin-bottom:18px}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:12px}.sub{color:var(--muted);font-size:12px;margin-top:-8px;margin-bottom:14px}.matrix-wrap{overflow:auto}.matrix{display:grid;min-width:1040px;gap:4px;align-items:stretch}.matrix .label{font-size:11px;color:var(--muted);padding:8px 5px;display:flex;align-items:end}.matrix .row-label{justify-content:flex-end;text-align:right;align-items:center}.cell{border:0;border-radius:7px;min-height:42px;padding:4px;cursor:pointer;background:var(--surface2);display:flex;align-items:center;justify-content:center;font-weight:700}.cell:hover,.cell:focus{outline:2px solid var(--blue);outline-offset:1px}.cell.tested{background:var(--green2);color:var(--green)}.cell.planned{background:var(--amber2);color:var(--amber)}.cell.missing{background:var(--red2);color:var(--red)}.cell.empty{color:var(--muted);font-weight:400}.cell.selected{box-shadow:inset 0 0 0 3px currentColor}.legend{display:flex;flex-wrap:wrap;gap:15px;margin-top:12px;color:var(--muted);font-size:12px}.legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:5px}.bars{display:grid;gap:9px}.bar-row{display:grid;grid-template-columns:130px minmax(0,1fr) 34px;gap:8px;align-items:center}.track{height:12px;background:var(--surface2);border-radius:99px;overflow:hidden;display:flex}.seg-syn{background:var(--blue)}.seg-real{background:var(--purple)}.bar-value{text-align:right;font-variant-numeric:tabular-nums}.detail{min-height:180px}.detail h3{font-size:16px;margin:0 0 8px}.detail ul{margin:8px 0 0;padding-left:18px;max-height:310px;overflow:auto}.detail code{font-size:11px;overflow-wrap:anywhere}.badge{display:inline-block;border-radius:99px;padding:3px 8px;margin:3px 4px 3px 0;background:var(--surface2);font-size:11px}.badge.syn{background:var(--blue2);color:var(--blue)}.badge.real{background:var(--purple2);color:var(--purple)}.gap{padding:11px 0;border-top:1px solid var(--line)}.gap:first-of-type{border-top:0}.gap strong{display:block}.gap p{margin:4px 0;color:var(--muted);font-size:12px}.oracle{display:grid;grid-template-columns:1fr 44px;gap:8px;align-items:center;margin:9px 0}.oracle .track{height:8px}.search-results{margin-top:8px}.case-row{display:grid;grid-template-columns:minmax(220px,1.4fr) 100px 130px;gap:12px;padding:9px 0;border-top:1px solid var(--line);align-items:center}.case-row:first-child{border-top:0}.case-row code{overflow-wrap:anywhere;font-size:11px}.empty-state{color:var(--muted);padding:18px 0}.docx-toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px}.docx-toolbar select{min-width:300px;max-width:100%;border:1px solid var(--line);background:var(--surface2);border-radius:8px;padding:7px 9px}.docx-status{color:var(--muted);font-size:12px}.docx-compare{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.docx-pane{min-width:0}.docx-pane h3{font-size:13px;margin:0 0 7px}.docx-view{background:var(--surface2);border:1px solid var(--line);border-radius:10px;min-height:330px;max-height:620px;overflow:auto}.docx-view .docx-wrapper{background:var(--surface2)!important;padding:12px!important}.docx-view .docx-wrapper>section.docx{background:#fff!important;color:#111!important;width:100%!important;min-height:380px!important;padding:44px!important;margin:0!important;box-shadow:none!important}.docx-view ins{background:#dcfce7;color:#
|
|
256
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}button,select,input{font:inherit;color:inherit}.shell{max-width:1480px;margin:auto;padding:28px;transition:max-width .18s ease}.top{display:flex;justify-content:space-between;gap:24px;align-items:end;margin-bottom:22px}.eyebrow{text-transform:uppercase;letter-spacing:.12em;color:var(--blue);font-weight:700;font-size:11px}h1{margin:4px 0 2px;font-size:clamp(25px,4vw,40px);line-height:1.12}h2{font-size:18px;margin:0 0 14px}.stamp{color:var(--muted);text-align:right}.controls{display:flex;flex-wrap:wrap;gap:10px;margin:0 0 18px}.controls label{display:flex;align-items:center;gap:7px;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 11px}.controls select,.controls input{border:0;background:transparent;outline:none}.stats{display:grid;grid-template-columns:repeat(6,minmax(120px,1fr));gap:12px;margin-bottom:18px}.stat,.panel{background:var(--surface);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}.stat{padding:16px}.stat strong{display:block;font-size:25px;line-height:1.1}.stat span{color:var(--muted);font-size:12px}.layout{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(300px,.7fr);gap:18px}.sidebar-hidden .shell{max-width:none}.sidebar-hidden .layout{grid-template-columns:minmax(0,1fr)}.sidebar-hidden #dashboard-sidebar{display:none}.panel{padding:18px;margin-bottom:18px}.panel-head{display:flex;align-items:start;justify-content:space-between;gap:12px}.sub{color:var(--muted);font-size:12px;margin-top:-8px;margin-bottom:14px}.matrix-wrap{overflow:auto}.matrix{display:grid;min-width:1040px;gap:4px;align-items:stretch}.matrix .label{font-size:11px;color:var(--muted);padding:8px 5px;display:flex;align-items:end}.matrix .row-label{justify-content:flex-end;text-align:right;align-items:center}.cell{border:0;border-radius:7px;min-height:42px;padding:4px;cursor:pointer;background:var(--surface2);display:flex;align-items:center;justify-content:center;font-weight:700}.cell:hover,.cell:focus{outline:2px solid var(--blue);outline-offset:1px}.cell.tested{background:var(--green2);color:var(--green)}.cell.planned{background:var(--amber2);color:var(--amber)}.cell.missing{background:var(--red2);color:var(--red)}.cell.empty{color:var(--muted);font-weight:400}.cell.selected{box-shadow:inset 0 0 0 3px currentColor}.legend{display:flex;flex-wrap:wrap;gap:15px;margin-top:12px;color:var(--muted);font-size:12px}.legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:5px}.bars{display:grid;gap:9px}.bar-row{display:grid;grid-template-columns:130px minmax(0,1fr) 34px;gap:8px;align-items:center}.track{height:12px;background:var(--surface2);border-radius:99px;overflow:hidden;display:flex}.seg-syn{background:var(--blue)}.seg-real{background:var(--purple)}.bar-value{text-align:right;font-variant-numeric:tabular-nums}.detail{min-height:180px}.detail h3{font-size:16px;margin:0 0 8px}.detail ul{margin:8px 0 0;padding-left:18px;max-height:310px;overflow:auto}.detail code{font-size:11px;overflow-wrap:anywhere}.badge{display:inline-block;border-radius:99px;padding:3px 8px;margin:3px 4px 3px 0;background:var(--surface2);font-size:11px}.badge.syn{background:var(--blue2);color:var(--blue)}.badge.real{background:var(--purple2);color:var(--purple)}.gap{padding:11px 0;border-top:1px solid var(--line)}.gap:first-of-type{border-top:0}.gap strong{display:block}.gap p{margin:4px 0;color:var(--muted);font-size:12px}.oracle{display:grid;grid-template-columns:1fr 44px;gap:8px;align-items:center;margin:9px 0}.oracle .track{height:8px}.search-results{margin-top:8px}.case-row{display:grid;grid-template-columns:minmax(220px,1.4fr) 100px 130px;gap:12px;padding:9px 0;border-top:1px solid var(--line);align-items:center}.case-row:first-child{border-top:0}.case-row code{overflow-wrap:anywhere;font-size:11px}.empty-state{color:var(--muted);padding:18px 0}.docx-toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px}.docx-toolbar select{min-width:300px;max-width:100%;border:1px solid var(--line);background:var(--surface2);border-radius:8px;padding:7px 9px}.docx-status{color:var(--muted);font-size:12px}.docx-compare{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.docx-pane{min-width:0}.docx-pane h3{font-size:13px;margin:0 0 7px}.docx-view{background:var(--surface2);border:1px solid var(--line);border-radius:10px;min-height:330px;max-height:620px;overflow:auto}.docx-view .docx-wrapper{background:var(--surface2)!important;padding:12px!important}.docx-view .docx-wrapper>section.docx{background:#fff!important;color:#111!important;width:100%!important;min-height:380px!important;padding:44px!important;margin:0!important;box-shadow:none!important}.docx-view ins,.docx-view ins *{background:#dcfce7!important;color:#15803d!important;text-decoration:underline!important;text-decoration-color:#15803d!important;text-decoration-thickness:2px!important}.docx-view del,.docx-view del *{background:#fee2e2!important;color:#b91c1c!important;text-decoration:line-through!important;text-decoration-color:#b91c1c!important;text-decoration-thickness:2px!important}.docx-view ins:has(p,table,div),.docx-view del:has(p,table,div){display:block!important}.docx-view tr.docx-ins-row td,.docx-view tr.docx-ins-row td *{background:#dcfce7!important;color:#15803d!important}.docx-view tr.docx-del-row td,.docx-view tr.docx-del-row td *{background:#fee2e2!important;color:#b91c1c!important;text-decoration:line-through!important}.docx-view .docx-format-change{background:#fef08a!important;outline:1px dashed #ca8a04!important}.diff-del{background:#fee2e2;color:#b91c1c;text-decoration:line-through;padding:1px 3px;border-radius:3px;font-weight:600}.diff-ins{background:#dcfce7;color:#15803d;text-decoration:underline;padding:1px 3px;border-radius:3px;font-weight:600}.screen-reader{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:980px){.stats{grid-template-columns:repeat(3,1fr)}.layout{grid-template-columns:1fr}.stamp{text-align:left}.top{align-items:start;flex-direction:column}}@media(max-width:760px){.docx-compare{grid-template-columns:1fr}}@media(max-width:620px){.shell{padding:16px}.stats{grid-template-columns:repeat(2,1fr)}.bar-row{grid-template-columns:100px minmax(0,1fr) 30px}.case-row{grid-template-columns:1fr}.controls label{width:100%;justify-content:space-between}.docx-toolbar select{min-width:0;width:100%}.docx-view .docx-wrapper>section.docx{padding:24px!important}}
|
|
121
257
|
.action{border:1px solid var(--line);background:var(--surface2);border-radius:8px;padding:7px 10px;cursor:pointer}.action:hover,.action:focus{border-color:var(--blue)}.action.primary{background:var(--blue);border-color:var(--blue);color:#fff}.preset-row{display:flex;gap:7px;flex-wrap:wrap;margin:0 0 14px}.docx-meta{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 14px}.pane-head{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:7px}.pane-head h3{margin:0}.pane-controls{display:flex;gap:7px}.pane-controls select{border:1px solid var(--line);background:var(--surface2);border-radius:7px;padding:5px}.expectations{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:14px}.expectation{background:var(--surface2);border-radius:9px;padding:11px;white-space:pre-wrap;overflow-wrap:anywhere}.expectation strong{display:block;margin-bottom:5px}.sync-control{display:inline-flex;align-items:center;gap:5px}.case-row{grid-template-columns:minmax(220px,1.4fr) 100px 120px auto}@media(max-width:760px){.expectations{grid-template-columns:1fr}}@media(max-width:620px){.case-row{grid-template-columns:1fr}.pane-head{align-items:flex-start;flex-direction:column}.pane-controls{width:100%}.pane-controls select{flex:1}}
|
|
258
|
+
.lane1-card{background:var(--surface2);border:1px solid var(--line);border-radius:10px;padding:14px;margin-bottom:12px}
|
|
259
|
+
.lane1-comments-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px;margin-top:10px}
|
|
260
|
+
.lane1-comment-box{background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--blue);border-radius:8px;padding:10px 12px;font-size:13px}
|
|
261
|
+
.lane1-comment-anchor{color:var(--amber);font-weight:600;font-family:ui-monospace,monospace;font-size:12px}
|
|
262
|
+
.lane1-comment-author{color:var(--muted);font-size:11px;margin-top:4px}
|
|
263
|
+
.lane1-ops-table{width:100%;border-collapse:collapse;margin-top:10px;font-size:12px}
|
|
264
|
+
.lane1-ops-table th,.lane1-ops-table td{border:1px solid var(--line);padding:6px 10px;text-align:left;vertical-align:top}
|
|
265
|
+
.lane1-ops-table th{background:var(--surface2)}
|
|
266
|
+
.badge.lane1{background:var(--purple2);color:var(--purple);font-weight:600}
|
|
122
267
|
</style>
|
|
123
268
|
</head>
|
|
124
269
|
<body>
|
|
@@ -128,6 +273,8 @@ export function renderDashboardHtml(data, libraries = {}) {
|
|
|
128
273
|
<label>Lane <select id="lane"><option value="all">All cases</option><option value="synthetic">Synthetic Word</option><option value="superdoc">Real documents</option></select></label>
|
|
129
274
|
<label>Category <select id="category"><option value="all">All categories</option><option value="legal">Legal</option><option value="administrative">Administrative</option></select></label>
|
|
130
275
|
<label>Find case <input id="search" type="search" placeholder="Name or structure"></label>
|
|
276
|
+
<a href="#docx-comparison" class="action" style="text-decoration:none;display:inline-flex;align-items:center;">Word COM Workbench</a>
|
|
277
|
+
<a href="#lane1-workbench" class="action primary" style="text-decoration:none;display:inline-flex;align-items:center;">Lane 1 Visual Showcase</a>
|
|
131
278
|
<button class="action" id="sidebar-toggle" type="button" aria-controls="dashboard-sidebar" aria-expanded="true">Hide sidebar</button>
|
|
132
279
|
</div>
|
|
133
280
|
<section class="stats" id="stats" aria-label="Coverage summary"></section>
|
|
@@ -135,6 +282,78 @@ export function renderDashboardHtml(data, libraries = {}) {
|
|
|
135
282
|
<div>
|
|
136
283
|
<section class="panel"><div class="panel-head"><div><h2>Task × structure matrix</h2><div class="sub">Select a cell to inspect its cases. Planned cells are high-priority gaps with recorded dependencies.</div></div></div><div class="matrix-wrap"><div class="matrix" id="matrix"></div></div><div class="legend"><span><i style="background:var(--green2)"></i>Tested</span><span><i style="background:var(--amber2)"></i>Planned high priority</span><span><i style="background:var(--red2)"></i>Unplanned high priority</span><span><i style="background:var(--surface2)"></i>Not prioritized</span></div></section>
|
|
137
284
|
<section class="panel" id="docx-comparison"><h2>DOCX comparison workbench</h2><div class="sub">Compare source, tracked, accepted, and rejected states from synthetic fixtures and reviewed real legal/administrative documents. Tracked markup uses docxjs experimental revision rendering.</div><div class="docx-toolbar"><label for="docx-case">Document</label><select id="docx-case"></select><label class="sync-control"><input id="sync-scroll" type="checkbox" checked> Sync scroll</label><span class="docx-status" id="docx-status"></span></div><div class="preset-row"><button class="action primary" type="button" data-preset="source,tracked">Source ↔ tracked</button><button class="action" type="button" data-preset="source,accepted">Source ↔ accepted</button><button class="action" type="button" data-preset="accepted,rejected">Accepted ↔ rejected</button></div><div class="docx-meta" id="docx-meta"></div><div class="docx-compare"><div class="docx-pane"><div class="pane-head"><h3>Left document</h3><div class="pane-controls"><select id="left-view" aria-label="Left document state"></select><button class="action" id="left-download" type="button">Download</button></div></div><div class="docx-view" id="docx-left"></div></div><div class="docx-pane"><div class="pane-head"><h3>Right document</h3><div class="pane-controls"><select id="right-view" aria-label="Right document state"></select><button class="action" id="right-download" type="button">Download</button></div></div><div class="docx-view" id="docx-right"></div></div></div><div class="expectations"><div class="expectation"><strong>Expected rejected/source text</strong><span id="expected-before"></span></div><div class="expectation"><strong>Expected accepted text</strong><span id="expected-after"></span></div></div></section>
|
|
285
|
+
<section class="panel" id="lane1-workbench">
|
|
286
|
+
<div class="panel-head">
|
|
287
|
+
<div>
|
|
288
|
+
<div class="eyebrow">Lane 1 Automated Regression Showcase</div>
|
|
289
|
+
<h2>Lane 1 Visual Inspection & Compound Batch Workflows</h2>
|
|
290
|
+
<div class="sub">Side-by-side visual comparison of high-complexity compound operations, real-world legal markup, multi-paragraph revisions, tables, lists, and package-level comment threads from Lane 1 automated test suites.</div>
|
|
291
|
+
</div>
|
|
292
|
+
<div id="lane1-count-badge"></div>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="docx-toolbar">
|
|
295
|
+
<label for="lane1-case">Lane 1 Scenario</label>
|
|
296
|
+
<select id="lane1-case"></select>
|
|
297
|
+
<label class="sync-control"><input id="lane1-sync-scroll" type="checkbox" checked> Sync scroll</label>
|
|
298
|
+
<span class="docx-status" id="lane1-status"></span>
|
|
299
|
+
</div>
|
|
300
|
+
<div class="preset-row">
|
|
301
|
+
<button class="action primary" type="button" data-lane1-preset="source,tracked">Source ↔ Tracked (with comments)</button>
|
|
302
|
+
<button class="action" type="button" data-lane1-preset="source,accepted">Source ↔ Accepted</button>
|
|
303
|
+
<button class="action" type="button" data-lane1-preset="accepted,rejected">Accepted ↔ Rejected</button>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="docx-meta" id="lane1-meta"></div>
|
|
306
|
+
<div class="docx-compare">
|
|
307
|
+
<div class="docx-pane">
|
|
308
|
+
<div class="pane-head">
|
|
309
|
+
<h3>Left document</h3>
|
|
310
|
+
<div class="pane-controls">
|
|
311
|
+
<select id="lane1-left-view" aria-label="Left document state"></select>
|
|
312
|
+
<button class="action" id="lane1-left-download" type="button">Download</button>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="docx-view" id="lane1-docx-left"></div>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="docx-pane">
|
|
318
|
+
<div class="pane-head">
|
|
319
|
+
<h3>Right document</h3>
|
|
320
|
+
<div class="pane-controls">
|
|
321
|
+
<select id="lane1-right-view" aria-label="Right document state"></select>
|
|
322
|
+
<button class="action" id="lane1-right-download" type="button">Download</button>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
<div class="docx-view" id="lane1-docx-right"></div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
<div style="margin-top:18px;display:grid;gap:12px;">
|
|
329
|
+
<details class="lane1-card" open id="lane1-comments-details">
|
|
330
|
+
<summary style="cursor:pointer;font-weight:700;font-size:14px;">
|
|
331
|
+
Counsel Comments & Review Notes (<span id="lane1-comments-count">0</span>)
|
|
332
|
+
</summary>
|
|
333
|
+
<div class="lane1-comments-grid" id="lane1-comments-list"></div>
|
|
334
|
+
</details>
|
|
335
|
+
<details class="lane1-card" id="lane1-ops-details">
|
|
336
|
+
<summary style="cursor:pointer;font-weight:700;font-size:14px;">
|
|
337
|
+
Operations Audit Ledger (<span id="lane1-ops-count">0</span> operations applied)
|
|
338
|
+
</summary>
|
|
339
|
+
<div style="overflow-x:auto;">
|
|
340
|
+
<table class="lane1-ops-table">
|
|
341
|
+
<thead>
|
|
342
|
+
<tr>
|
|
343
|
+
<th style="width:40px;">#</th>
|
|
344
|
+
<th style="width:85px;">Type</th>
|
|
345
|
+
<th style="width:165px;">Author</th>
|
|
346
|
+
<th>Clause / Target</th>
|
|
347
|
+
<th>Change / Detail</th>
|
|
348
|
+
<th style="width:80px;">Status</th>
|
|
349
|
+
</tr>
|
|
350
|
+
</thead>
|
|
351
|
+
<tbody id="lane1-ops-body"></tbody>
|
|
352
|
+
</table>
|
|
353
|
+
</div>
|
|
354
|
+
</details>
|
|
355
|
+
</div>
|
|
356
|
+
</section>
|
|
138
357
|
<section class="panel"><h2>Coverage by structure</h2><div class="sub">Unique case count, split between synthetic packages and reviewed real documents.</div><div class="bars" id="structure-bars"></div></section>
|
|
139
358
|
<section class="panel"><h2>Matching cases</h2><div class="sub" id="case-caption"></div><div class="search-results" id="case-list"></div></section>
|
|
140
359
|
</div>
|
|
@@ -169,17 +388,148 @@ const escapeHtml=value=>String(value??'').replace(/&/g,'&').replace(/</g,'&l
|
|
|
169
388
|
function decodeDocx(value){const raw=atob(value),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return bytes}
|
|
170
389
|
function activeDocx(){return DATA.cases.find(c=>c.identity===$('docx-case').value)}
|
|
171
390
|
function viewOptions(selected){return Object.entries(VIEW_LABELS).map(([value,text])=>'<option value="'+value+'"'+(value===selected?' selected':'')+'>'+text+'</option>').join('')}
|
|
172
|
-
function
|
|
391
|
+
function diffWords(before, after) {
|
|
392
|
+
if (!before || !after) return null;
|
|
393
|
+
const tokenize = s => s.match(/\\s+|[^\\s\\w]+|\\w+/g) || [s];
|
|
394
|
+
const a = tokenize(before);
|
|
395
|
+
const b = tokenize(after);
|
|
396
|
+
const m = a.length, n = b.length;
|
|
397
|
+
if (m * n > 250000) {
|
|
398
|
+
return [{ type: 'del', text: before }, { type: 'ins', text: after }];
|
|
399
|
+
}
|
|
400
|
+
const dp = Array.from({ length: m + 1 }, () => new Uint16Array(n + 1));
|
|
401
|
+
for (let i = 1; i <= m; i++) {
|
|
402
|
+
for (let j = 1; j <= n; j++) {
|
|
403
|
+
if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
404
|
+
else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
let i = m, j = n;
|
|
408
|
+
const diff = [];
|
|
409
|
+
while (i > 0 || j > 0) {
|
|
410
|
+
if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
|
|
411
|
+
diff.unshift({ type: 'same', text: a[i - 1] });
|
|
412
|
+
i--; j--;
|
|
413
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
414
|
+
diff.unshift({ type: 'ins', text: b[j - 1] });
|
|
415
|
+
j--;
|
|
416
|
+
} else {
|
|
417
|
+
diff.unshift({ type: 'del', text: a[i - 1] });
|
|
418
|
+
i--;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return diff;
|
|
422
|
+
}
|
|
423
|
+
function renderDocxMeta(item){
|
|
424
|
+
const r=item.revisions;
|
|
425
|
+
$('docx-meta').innerHTML='<span class="badge '+(item.lane==='superdoc'?'real':'syn')+'">'+(item.lane==='superdoc'?'Reviewed real document':'Synthetic fixture')+'</span><span class="badge">'+label(item.category)+'</span><span class="badge">'+label(item.task)+'</span>'+item.structures.map(s=>'<span class="badge">'+label(s)+'</span>').join('')+'<span class="badge">'+r.insertions+' insertion revision'+(r.insertions===1?'':'s')+'</span><span class="badge">'+r.deletions+' deletion revision'+(r.deletions===1?'':'s')+'</span>'+(r.formatting?'<span class="badge">'+r.formatting+' formatting revision'+(r.formatting===1?'':'s')+'</span>':'');
|
|
426
|
+
const beforeText = item.expectations?.rejected || item.expectations?.source || '';
|
|
427
|
+
const afterText = item.expectations?.accepted || '';
|
|
428
|
+
const diff = diffWords(beforeText, afterText);
|
|
429
|
+
if (diff && beforeText !== afterText) {
|
|
430
|
+
$('expected-before').innerHTML = diff.filter(x => x.type !== 'ins').map(x => x.type === 'del' ? '<del class="diff-del">' + escapeHtml(x.text) + '</del>' : escapeHtml(x.text)).join('');
|
|
431
|
+
$('expected-after').innerHTML = diff.filter(x => x.type !== 'del').map(x => x.type === 'ins' ? '<ins class="diff-ins">' + escapeHtml(x.text) + '</ins>' : escapeHtml(x.text)).join('');
|
|
432
|
+
} else {
|
|
433
|
+
$('expected-before').textContent = beforeText;
|
|
434
|
+
$('expected-after').textContent = afterText;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
173
437
|
async function renderPane(side,item){const state=$(side+'-view').value,target=$('docx-'+side),payload=item.docxVariants?.[state];target.innerHTML='';if(!payload){target.innerHTML='<div class="empty-state">This document state is unavailable.</div>';return}const base={experimental:true,renderHeaders:true,renderFooters:true,renderFootnotes:true,renderEndnotes:true,renderComments:true,ignoreWidth:true,ignoreHeight:true,breakPages:false,useBase64URL:true,renderChanges:state==='tracked'};await window.docx.renderAsync(decodeDocx(payload),target,null,base)}
|
|
174
|
-
async function renderDocxComparison(){const item=activeDocx();if(!item?.docxVariants?.tracked){$('docx-status').textContent='No embedded DOCX is available.';return}if(!window.docx?.renderAsync){$('docx-status').textContent='docxjs failed to load.';return}$('docx-status').textContent='Rendering both documents…';renderDocxMeta(item);try{await Promise.all([renderPane('left',item),renderPane('right',item)]);$('docx-status').textContent='Ready · docx-preview 0.4.0';}catch(error){$('docx-status').textContent='Render error: '+error.message}}
|
|
438
|
+
async function renderDocxComparison(){const item=activeDocx();if(!item?.docxVariants?.tracked){$('docx-status').textContent='No embedded DOCX is available.';return}if(!window.docx?.renderAsync){$('docx-status').textContent='docxjs failed to load.';return}$('docx-status').textContent='Rendering both documents…';renderDocxMeta(item);try{await Promise.all([renderPane('left',item),renderPane('right',item)]);$('docx-status').textContent='Ready · docx-preview 0.4.0 (revisions enhanced)';}catch(error){$('docx-status').textContent='Render error: '+error.message}}
|
|
175
439
|
function setComparison(left,right){$('left-view').value=left;$('right-view').value=right;renderDocxComparison()}
|
|
176
440
|
function selectDocx(identity){if(!$('docx-case').querySelector('option[value="'+identity+'"]'))return;$('docx-case').value=identity;renderDocxComparison()}
|
|
177
441
|
function downloadView(side){const item=activeDocx(),state=$(side+'-view').value,payload=item?.docxVariants?.[state];if(!payload)return;const blob=new Blob([decodeDocx(payload)],{type:'application/vnd.openxmlformats-officedocument.wordprocessingml.document'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=item.identity.replace(/^(synthetic|superdoc):/,'')+'.'+state+'.docx';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}
|
|
178
442
|
function initDocx(){const available=DATA.cases.filter(c=>c.docxVariants?.tracked),options=lane=>available.filter(c=>c.lane===lane).map(c=>'<option value="'+c.identity+'">'+escapeHtml(c.displayName||c.identity)+'</option>').join('');$('docx-case').innerHTML='<optgroup label="Reviewed real documents">'+options('superdoc')+'</optgroup><optgroup label="Synthetic fixtures">'+options('synthetic')+'</optgroup>';$('left-view').innerHTML=viewOptions('source');$('right-view').innerHTML=viewOptions('tracked');const preferred=available.find(c=>c.lane==='superdoc')||available.find(c=>c.identity==='synthetic:administrative-tab-aligned-status')||available[0];if(preferred)$('docx-case').value=preferred.identity;$('docx-case').addEventListener('change',renderDocxComparison);$('left-view').addEventListener('change',renderDocxComparison);$('right-view').addEventListener('change',renderDocxComparison);document.querySelectorAll('[data-preset]').forEach(button=>button.addEventListener('click',()=>setComparison(...button.dataset.preset.split(','))));$('left-download').addEventListener('click',()=>downloadView('left'));$('right-download').addEventListener('click',()=>downloadView('right'));let syncing=false;for(const [from,to] of [[$('docx-left'),$('docx-right')],[$('docx-right'),$('docx-left')]])from.addEventListener('scroll',()=>{if(!$('sync-scroll').checked||syncing)return;syncing=true;const maxFrom=from.scrollHeight-from.clientHeight,maxTo=to.scrollHeight-to.clientHeight;to.scrollTop=maxFrom>0?from.scrollTop/maxFrom*maxTo:0;to.scrollLeft=from.scrollLeft;requestAnimationFrame(()=>{syncing=false})});renderDocxComparison()}
|
|
179
443
|
function render(){const cases=filtered();renderStats(cases);renderMatrix(cases);renderBars(cases);renderOracles(cases);renderCases(cases)}
|
|
180
444
|
function setSidebarHidden(hidden){document.body.classList.toggle('sidebar-hidden',hidden);const toggle=$('sidebar-toggle');toggle.textContent=hidden?'Show sidebar':'Hide sidebar';toggle.setAttribute('aria-expanded',String(!hidden));try{localStorage.setItem('docx-dashboard-sidebar-hidden',hidden?'1':'0')}catch{}}
|
|
445
|
+
const LANE1_CASES=DATA.lane1Cases||[];
|
|
446
|
+
function activeLane1Docx(){return LANE1_CASES.find(c=>c.identity===$('lane1-case').value)||LANE1_CASES[0]}
|
|
447
|
+
function renderLane1Meta(item){
|
|
448
|
+
if(!item)return;const r=item.revisions||{},b=item.breakdown||{};
|
|
449
|
+
let meta='<span class="badge lane1">Lane 1 Automated Test</span>';
|
|
450
|
+
if(item.category)meta+='<span class="badge">'+label(item.category)+'</span>';
|
|
451
|
+
if(item.sourceWords)meta+='<span class="badge">'+item.sourceWords.toLocaleString()+' words</span>';
|
|
452
|
+
if(item.sourceParagraphs)meta+='<span class="badge">'+item.sourceParagraphs+' paragraphs</span>';
|
|
453
|
+
if(item.operationsCount)meta+='<span class="badge">'+item.operationsCount+' operations ('+(b.redlines||0)+' redlines, '+(b.comments||0)+' comments, '+(b.formatting||0)+' formatting)</span>';
|
|
454
|
+
if(r.insertions)meta+='<span class="badge">'+r.insertions+' insertions</span>';
|
|
455
|
+
if(r.deletions)meta+='<span class="badge">'+r.deletions+' deletions</span>';
|
|
456
|
+
if(item.authors && item.authors.length > 1){
|
|
457
|
+
meta+='<span class="badge" style="background:var(--purple2);color:var(--purple);font-weight:600;">Dual-Author Negotiation</span>';
|
|
458
|
+
meta+='<span class="badge" style="border:1px solid var(--blue);color:var(--blue);">Lead: '+escapeHtml(item.authors[0])+'</span>';
|
|
459
|
+
meta+='<span class="badge" style="border:1px solid var(--amber);color:var(--amber);">Counterparty: '+escapeHtml(item.authors[1])+'</span>';
|
|
460
|
+
} else if(item.author){
|
|
461
|
+
meta+='<span class="badge">Author: '+escapeHtml(item.author)+'</span>';
|
|
462
|
+
}
|
|
463
|
+
$('lane1-meta').innerHTML=meta;
|
|
464
|
+
|
|
465
|
+
const comments=item.comments||[];
|
|
466
|
+
$('lane1-comments-count').textContent=comments.length;
|
|
467
|
+
if(comments.length===0){$('lane1-comments-list').innerHTML='<div class="empty-state">No comments in this scenario.</div>'}
|
|
468
|
+
else{$('lane1-comments-list').innerHTML=comments.map(c=>{
|
|
469
|
+
const isCounter = item.authors && c.author === item.authors[1];
|
|
470
|
+
const borderStyle = isCounter ? 'border-left:3px solid var(--amber);' : 'border-left:3px solid var(--blue);';
|
|
471
|
+
const tag = isCounter ? '<span class="badge" style="background:var(--amber2);color:var(--amber);margin-right:6px;font-size:10px;">Counterparty</span>' : (item.authors?.length > 1 ? '<span class="badge" style="background:var(--blue2);color:var(--blue);margin-right:6px;font-size:10px;">Lead Agency</span>' : '');
|
|
472
|
+
return '<div class="lane1-comment-box" style="'+borderStyle+'"><div class="lane1-comment-anchor">'+tag+'Anchor: “'+escapeHtml(c.anchor)+'”</div><div style="margin:5px 0;font-weight:500;">'+escapeHtml(c.content)+'</div><div class="lane1-comment-author">By '+escapeHtml(c.author)+' • Clause: '+escapeHtml(c.target).slice(0,75)+(c.target.length>75?'...':'')+'</div></div>';
|
|
473
|
+
}).join('')}
|
|
474
|
+
|
|
475
|
+
const ops=item.operations||[];
|
|
476
|
+
$('lane1-ops-count').textContent=ops.length;
|
|
477
|
+
if(ops.length===0){$('lane1-ops-body').innerHTML='<tr><td colspan="6" class="empty-state">No operations logged.</td></tr>'}
|
|
478
|
+
else{$('lane1-ops-body').innerHTML=ops.map(op=>{
|
|
479
|
+
const isCounter = item.authors && op.author === item.authors[1];
|
|
480
|
+
const authorBadge = op.author ? '<span class="badge" style="font-size:11px;'+(isCounter?'background:var(--amber2);color:var(--amber);':'background:var(--blue2);color:var(--blue);')+'">'+escapeHtml(op.author)+'</span>' : '';
|
|
481
|
+
return '<tr><td>'+op.index+'</td><td><span class="badge">'+escapeHtml(op.type)+'</span></td><td>'+authorBadge+'</td><td><code>'+escapeHtml(op.target).slice(0,95)+(op.target.length>95?'...':'')+'</code></td><td>'+escapeHtml(op.change).slice(0,130)+(op.change.length>130?'...':'')+'</td><td><span class="badge" style="background:var(--green2);color:var(--green);">'+escapeHtml(op.status)+'</span></td></tr>';
|
|
482
|
+
}).join('')}
|
|
483
|
+
}
|
|
484
|
+
async function renderLane1Pane(side,item){
|
|
485
|
+
const state=$('lane1-'+side+'-view').value,target=$('lane1-docx-'+side),payload=item.docxVariants?.[state];
|
|
486
|
+
target.innerHTML='';if(!payload){target.innerHTML='<div class="empty-state">This document state is unavailable.</div>';return}
|
|
487
|
+
const base={experimental:true,renderHeaders:true,renderFooters:true,renderFootnotes:true,renderEndnotes:true,renderComments:true,ignoreWidth:true,ignoreHeight:true,breakPages:false,useBase64URL:true,renderChanges:state==='tracked'};
|
|
488
|
+
await window.docx.renderAsync(decodeDocx(payload),target,null,base);
|
|
489
|
+
}
|
|
490
|
+
async function renderLane1Comparison(){
|
|
491
|
+
const item=activeLane1Docx();
|
|
492
|
+
if(!item?.docxVariants?.tracked){$('lane1-status').textContent='No Lane 1 DOCX is available.';return}
|
|
493
|
+
if(!window.docx?.renderAsync){$('lane1-status').textContent='docxjs failed to load.';return}
|
|
494
|
+
$('lane1-status').textContent='Rendering Lane 1 documents…';
|
|
495
|
+
renderLane1Meta(item);
|
|
496
|
+
try{await Promise.all([renderLane1Pane('left',item),renderLane1Pane('right',item)]);$('lane1-status').textContent='Ready · docx-preview (Lane 1 legal markup)'}
|
|
497
|
+
catch(error){$('lane1-status').textContent='Render error: '+error.message}
|
|
498
|
+
}
|
|
499
|
+
function setLane1Comparison(left,right){$('lane1-left-view').value=left;$('lane1-right-view').value=right;renderLane1Comparison()}
|
|
500
|
+
function downloadLane1View(side){
|
|
501
|
+
const item=activeLane1Docx(),state=$('lane1-'+side+'-view').value,payload=item?.docxVariants?.[state];
|
|
502
|
+
if(!payload)return;const blob=new Blob([decodeDocx(payload)],{type:'application/vnd.openxmlformats-officedocument.wordprocessingml.document'}),url=URL.createObjectURL(blob),a=document.createElement('a');
|
|
503
|
+
a.href=url;a.download=state==='tracked'?item.name+'-STRUCTURED-FIX.docx':item.name+'.'+state+'.docx';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
|
|
504
|
+
}
|
|
505
|
+
function initLane1Docx(){
|
|
506
|
+
if(!LANE1_CASES.length){if($('lane1-workbench'))$('lane1-workbench').style.display='none';return}
|
|
507
|
+
$('lane1-count-badge').innerHTML='<span class="badge syn">'+LANE1_CASES.length+' compound scenario'+(LANE1_CASES.length===1?'':'s')+'</span>';
|
|
508
|
+
$('lane1-case').innerHTML=LANE1_CASES.map(c=>'<option value="'+c.identity+'">'+escapeHtml(c.title||c.name)+'</option>').join('');
|
|
509
|
+
$('lane1-left-view').innerHTML=viewOptions('source');
|
|
510
|
+
$('lane1-right-view').innerHTML=viewOptions('tracked');
|
|
511
|
+
$('lane1-case').value=LANE1_CASES[0].identity;
|
|
512
|
+
|
|
513
|
+
$('lane1-case').addEventListener('change',renderLane1Comparison);
|
|
514
|
+
$('lane1-left-view').addEventListener('change',renderLane1Comparison);
|
|
515
|
+
$('lane1-right-view').addEventListener('change',renderLane1Comparison);
|
|
516
|
+
document.querySelectorAll('[data-lane1-preset]').forEach(b=>b.addEventListener('click',()=>setLane1Comparison(...b.dataset.lane1Preset.split(','))));
|
|
517
|
+
$('lane1-left-download').addEventListener('click',()=>downloadLane1View('left'));
|
|
518
|
+
$('lane1-right-download').addEventListener('click',()=>downloadLane1View('right'));
|
|
519
|
+
|
|
520
|
+
let syncing=false;
|
|
521
|
+
for(const [from,to] of [[$('lane1-docx-left'),$('lane1-docx-right')],[$('lane1-docx-right'),$('lane1-docx-left')]]){
|
|
522
|
+
from.addEventListener('scroll',()=>{
|
|
523
|
+
if(!$('lane1-sync-scroll').checked||syncing)return;
|
|
524
|
+
syncing=true;const maxFrom=from.scrollHeight-from.clientHeight,maxTo=to.scrollHeight-to.clientHeight;
|
|
525
|
+
to.scrollTop=maxFrom>0?from.scrollTop/maxFrom*maxTo:0;to.scrollLeft=from.scrollLeft;
|
|
526
|
+
requestAnimationFrame(()=>{syncing=false});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
renderLane1Comparison();
|
|
530
|
+
}
|
|
181
531
|
function initSidebar(){let hidden=false;try{hidden=localStorage.getItem('docx-dashboard-sidebar-hidden')==='1'}catch{}setSidebarHidden(hidden);$('sidebar-toggle').addEventListener('click',()=>setSidebarHidden(!document.body.classList.contains('sidebar-hidden')))}
|
|
182
|
-
$('lane').addEventListener('change',render);$('category').addEventListener('change',render);$('search').addEventListener('input',render);$('stamp').textContent='Generated '+new Date(DATA.generatedAt).toLocaleString();renderGaps();render();initSidebar();initDocx();
|
|
532
|
+
$('lane').addEventListener('change',render);$('category').addEventListener('change',render);$('search').addEventListener('input',render);$('stamp').textContent='Generated '+new Date(DATA.generatedAt).toLocaleString();renderGaps();render();initSidebar();initDocx();initLane1Docx();
|
|
183
533
|
</script>
|
|
184
534
|
</body>
|
|
185
535
|
</html>\n`;
|
|
@@ -187,12 +537,13 @@ $('lane').addEventListener('change',render);$('category').addEventListener('chan
|
|
|
187
537
|
|
|
188
538
|
const isCli = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
189
539
|
if (isCli) {
|
|
190
|
-
const { outputPath, fixturesDir, corpusFixturesDir } = parseArgs(process.argv.slice(2));
|
|
540
|
+
const { outputPath, fixturesDir, corpusFixturesDir, lane1FixturesDir } = parseArgs(process.argv.slice(2));
|
|
541
|
+
const unminifiedDocxPreviewPath = join(process.cwd(), 'node_modules', 'docx-preview', 'dist', 'docx-preview.js');
|
|
191
542
|
const libraries = {
|
|
192
543
|
jszipSource: readFileSync(join(process.cwd(), 'node_modules', 'jszip', 'dist', 'jszip.min.js'), 'utf8'),
|
|
193
|
-
docxPreviewSource: readFileSync(
|
|
544
|
+
docxPreviewSource: readFileSync(unminifiedDocxPreviewPath, 'utf8')
|
|
194
545
|
};
|
|
195
|
-
const html = renderDashboardHtml(buildDashboardData(fixturesDir, corpusFixturesDir), libraries);
|
|
546
|
+
const html = renderDashboardHtml(buildDashboardData(fixturesDir, corpusFixturesDir, lane1FixturesDir), libraries);
|
|
196
547
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
197
548
|
writeFileSync(outputPath, html, 'utf8');
|
|
198
549
|
console.log(`Wrote test comparison dashboard: ${outputPath}`);
|
|
@@ -49,8 +49,12 @@ function assertSyntheticClaim(testCase, structure) {
|
|
|
49
49
|
'multi-paragraph': () => elementCount(xml, 'p') > 1 || testCase.sourceText?.includes('\n')
|
|
50
50
|
|| testCase.original.includes('\n') || testCase.modified.includes('\n'),
|
|
51
51
|
'formatted-runs': () => /<(?:w:)?(?:b|i|u)(?:\s|\/|>)/.test(xml) || /(?:\*\*|\*|\+\+)/.test(testCase.modified),
|
|
52
|
-
list: () => elementCount(xml, 'numPr') > 0
|
|
53
|
-
|
|
52
|
+
list: () => elementCount(xml, 'numPr') > 0
|
|
53
|
+
|| (testCase.operationOptions?.structuredContent === true
|
|
54
|
+
&& /^\s*(?:[-+*]|\d+[.)])\s+/m.test(testCase.modified)),
|
|
55
|
+
table: () => elementCount(xml, 'tbl') > 0
|
|
56
|
+
|| (testCase.operationOptions?.structuredContent === true
|
|
57
|
+
&& /^\s*\|.*\|\s*\r?\n\s*\|\s*:?-{3,}/m.test(testCase.modified)),
|
|
54
58
|
bookmark: () => elementCount(xml, 'bookmarkStart') > 0 && elementCount(xml, 'bookmarkEnd') > 0,
|
|
55
59
|
hyperlink: () => elementCount(xml, 'hyperlink') > 0,
|
|
56
60
|
'content-control': () => elementCount(xml, 'sdt') > 0,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
|
|
2
|
+
import { configureXmlProvider } from '../adapters/xml-adapter.js';
|
|
3
|
+
import { applyRedlineToOxml } from '../engine/oxml-engine.js';
|
|
4
|
+
import { createRouteFrequencyCollector } from '../engine/route-selection.js';
|
|
5
|
+
|
|
6
|
+
configureXmlProvider({ DOMParser, XMLSerializer });
|
|
7
|
+
const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
8
|
+
const paragraph = text => `<w:p xmlns:w="${W}"><w:r><w:t>${text}</w:t></w:r></w:p>`;
|
|
9
|
+
const cases = [
|
|
10
|
+
[paragraph('Alpha'), 'Alpha', 'Beta'],
|
|
11
|
+
[paragraph('Alpha'), 'Alpha', '1. One\n2. Two'],
|
|
12
|
+
[paragraph('Alpha'), 'Alpha', '| A |\n| --- |\n| B |'],
|
|
13
|
+
[`<w:document xmlns:w="${W}"><w:body>${paragraph('1. One')}${paragraph('2. Two')}</w:body></w:document>`, '1. One\n2. Two', '1. One changed\n2. Two']
|
|
14
|
+
];
|
|
15
|
+
const collector = createRouteFrequencyCollector();
|
|
16
|
+
for (const [oxml, original, modified] of cases) {
|
|
17
|
+
await applyRedlineToOxml(oxml, original, modified, { author:'Route profiler', _routeInstrumentation: collector });
|
|
18
|
+
}
|
|
19
|
+
process.stdout.write(`${JSON.stringify({ cases: cases.length, routes: collector.snapshot() }, null, 2)}\n`);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import { spawnSync } from 'child_process';
|
|
3
2
|
|
|
4
3
|
const baseName = '48-administrative-administrative-list-change-dash-agenda-child';
|
|
@@ -9,10 +8,6 @@ const docxPath = `${fixtureDir}/${baseName}.docx`;
|
|
|
9
8
|
const acceptedDocx = `${fixtureDir}/${baseName}.accepted.docx`;
|
|
10
9
|
const rejectedDocx = `${fixtureDir}/${baseName}.rejected.docx`;
|
|
11
10
|
|
|
12
|
-
const allMarkupPdf = `${visualDir}/${baseName}--allMarkup.pdf`;
|
|
13
|
-
const acceptAllPdf = `${visualDir}/${baseName}--acceptAll.pdf`;
|
|
14
|
-
const rejectAllPdf = `${visualDir}/${baseName}--rejectAll.pdf`;
|
|
15
|
-
|
|
16
11
|
console.log('Rendering Word COM for', baseName);
|
|
17
12
|
|
|
18
13
|
const psScript = `
|