@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.
Files changed (100) hide show
  1. package/AGENTS.md +589 -287
  2. package/ARCHITECTURE.md +215 -9
  3. package/CHANGELOG.md +319 -0
  4. package/README.md +604 -360
  5. package/adapters/config.js +45 -43
  6. package/bin/docx-redline.js +3 -0
  7. package/core/list-targeting.js +101 -110
  8. package/core/paragraph-targeting.js +501 -61
  9. package/core/paragraph-text.js +209 -0
  10. package/core/revision-cloning.js +38 -0
  11. package/core/types.js +64 -10
  12. package/core/word-xml.js +43 -15
  13. package/dist/docx-redline-js.esm.js +2849 -466
  14. package/dist/docx-redline-js.esm.js.map +4 -4
  15. package/dist/docx-redline-js.esm.min.js +87 -76
  16. package/dist/docx-redline-js.esm.min.js.map +4 -4
  17. package/docs/TESTING.md +342 -23
  18. package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
  19. package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
  20. package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
  21. package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
  22. package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
  23. package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
  24. package/docs/schemas/document-operations.schema.json +109 -0
  25. package/docs/test-comparison-dashboard.html +4250 -7
  26. package/engine/formatting-removal.js +11 -2
  27. package/engine/oxml-engine.js +491 -336
  28. package/engine/reconstruction-mode.js +15 -14
  29. package/engine/reconstruction-writer.js +247 -142
  30. package/engine/route-selection.js +35 -0
  31. package/engine/rpr-helpers.js +334 -35
  32. package/engine/run-builders.js +239 -196
  33. package/engine/surgical-diff-application.js +222 -37
  34. package/engine/surgical-mode.js +134 -6
  35. package/engine/surgical-spans.js +52 -1
  36. package/engine/table-cell-context.js +3 -6
  37. package/engine/table-mode.js +1 -1
  38. package/index.d.ts +234 -6
  39. package/index.js +24 -1
  40. package/node/cli.js +317 -0
  41. package/node/docx-document.js +302 -0
  42. package/node/index.d.ts +31 -0
  43. package/node/index.js +2 -0
  44. package/node/zip-archive.js +52 -0
  45. package/orchestration/list-markdown.js +10 -16
  46. package/orchestration/list-parsing.js +7 -12
  47. package/orchestration/list-structural-fallback.js +21 -10
  48. package/package.json +24 -3
  49. package/pipeline/content-analysis.js +12 -17
  50. package/pipeline/ingestion-export.js +3 -31
  51. package/pipeline/ingestion-paragraph.js +10 -5
  52. package/pipeline/list-generation.js +150 -55
  53. package/pipeline/list-markers.js +70 -3
  54. package/pipeline/serialization.js +4 -2
  55. package/pipeline/structured-content.js +160 -0
  56. package/scripts/apply_changes.mjs +27 -0
  57. package/scripts/benchmark-operation-session.mjs +137 -0
  58. package/scripts/benchmark-targeting-browser.html +74 -0
  59. package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
  60. package/scripts/benchmark-test-runner.mjs +59 -0
  61. package/scripts/build-test-dashboard.mjs +23 -0
  62. package/scripts/export-lane1-fixtures.mjs +380 -0
  63. package/scripts/export-reredline-stress-fixtures.mjs +317 -0
  64. package/scripts/export-validation-fixtures.mjs +1 -1
  65. package/scripts/extract_text.mjs +7 -0
  66. package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
  67. package/scripts/generate-test-dashboard.mjs +362 -11
  68. package/scripts/lib/word-coverage-catalogue.mjs +6 -2
  69. package/scripts/profile-route-selection.mjs +19 -0
  70. package/scripts/render-agenda-multilevel.mjs +0 -5
  71. package/scripts/render-multilevel-cases.mjs +0 -1
  72. package/scripts/run-tests.mjs +107 -35
  73. package/scripts/word-com-corpus-suite.ps1 +3 -0
  74. package/scripts/word-com-differential.ps1 +64 -4
  75. package/scripts/word-com-suite.ps1 +3 -0
  76. package/services/batch-operation-orchestrator.js +494 -0
  77. package/services/capture-engine.js +226 -0
  78. package/services/comment-builders.js +23 -6
  79. package/services/comment-engine.js +108 -47
  80. package/services/comment-locator.js +187 -82
  81. package/services/comment-replies.js +95 -0
  82. package/services/document-inspection.js +258 -0
  83. package/services/document-operation-applier.js +372 -0
  84. package/services/document-operation-contract.js +323 -0
  85. package/services/document-operation-mutations.js +1733 -0
  86. package/services/document-operation-session.js +258 -0
  87. package/services/numbering-service.js +14 -5
  88. package/services/operation-heuristics.js +173 -0
  89. package/services/operation-preflight.js +366 -0
  90. package/services/receipt-collector.js +288 -0
  91. package/services/revision-comment-management.js +37 -5
  92. package/services/revision-token.js +290 -0
  93. package/services/standalone-docx-plumbing.js +123 -8
  94. package/services/standalone-operation-runner.d.ts +296 -0
  95. package/services/standalone-operation-runner.js +10 -1455
  96. package/services/table-reconciliation.js +15 -6
  97. package/docs/VALIDATION.md +0 -183
  98. package/docs/WORD-MANUAL-REVIEW.md +0 -138
  99. package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
  100. /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:#166534;text-decoration:none}.docx-view del{background:#fee2e2;color:#991b1b}.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}}
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,'&amp;').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 renderDocxMeta(item){const r=item.revisions;$('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>':'');$('expected-before').textContent=item.expectations?.rejected||item.expectations?.source||'';$('expected-after').textContent=item.expectations?.accepted||''}
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: &ldquo;'+escapeHtml(c.anchor)+'&rdquo;</div><div style="margin:5px 0;font-weight:500;">'+escapeHtml(c.content)+'</div><div class="lane1-comment-author">By '+escapeHtml(c.author)+' &bull; 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(join(process.cwd(), 'node_modules', 'docx-preview', 'dist', 'docx-preview.min.js'), 'utf8')
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
- table: () => elementCount(xml, 'tbl') > 0,
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 = `
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs';
2
2
  import { spawnSync } from 'child_process';
3
- import { resolve } from 'path';
4
3
 
5
4
  const cases = [
6
5
  '37-administrative-administrative-list-change-board-agenda-child',