@trohde/earos 1.0.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 (135) hide show
  1. package/README.md +156 -0
  2. package/assets/init/.agents/skills/earos-artifact-gen/SKILL.md +106 -0
  3. package/assets/init/.agents/skills/earos-artifact-gen/references/interview-guide.md +313 -0
  4. package/assets/init/.agents/skills/earos-artifact-gen/references/output-guide.md +367 -0
  5. package/assets/init/.agents/skills/earos-assess/SKILL.md +212 -0
  6. package/assets/init/.agents/skills/earos-assess/references/calibration-benchmarks.md +160 -0
  7. package/assets/init/.agents/skills/earos-assess/references/output-templates.md +311 -0
  8. package/assets/init/.agents/skills/earos-assess/references/scoring-protocol.md +281 -0
  9. package/assets/init/.agents/skills/earos-calibrate/SKILL.md +153 -0
  10. package/assets/init/.agents/skills/earos-calibrate/references/agreement-metrics.md +188 -0
  11. package/assets/init/.agents/skills/earos-calibrate/references/calibration-protocol.md +263 -0
  12. package/assets/init/.agents/skills/earos-create/SKILL.md +257 -0
  13. package/assets/init/.agents/skills/earos-create/references/criterion-writing-guide.md +268 -0
  14. package/assets/init/.agents/skills/earos-create/references/dependency-rules.md +193 -0
  15. package/assets/init/.agents/skills/earos-create/references/rubric-interview-guide.md +123 -0
  16. package/assets/init/.agents/skills/earos-create/references/validation-checklist.md +238 -0
  17. package/assets/init/.agents/skills/earos-profile-author/SKILL.md +251 -0
  18. package/assets/init/.agents/skills/earos-profile-author/references/criterion-writing-guide.md +280 -0
  19. package/assets/init/.agents/skills/earos-profile-author/references/design-methods.md +158 -0
  20. package/assets/init/.agents/skills/earos-profile-author/references/profile-checklist.md +173 -0
  21. package/assets/init/.agents/skills/earos-remediate/SKILL.md +118 -0
  22. package/assets/init/.agents/skills/earos-remediate/references/output-template.md +199 -0
  23. package/assets/init/.agents/skills/earos-remediate/references/remediation-patterns.md +330 -0
  24. package/assets/init/.agents/skills/earos-report/SKILL.md +85 -0
  25. package/assets/init/.agents/skills/earos-report/references/portfolio-template.md +181 -0
  26. package/assets/init/.agents/skills/earos-report/references/single-artifact-template.md +168 -0
  27. package/assets/init/.agents/skills/earos-review/SKILL.md +130 -0
  28. package/assets/init/.agents/skills/earos-review/references/challenge-patterns.md +163 -0
  29. package/assets/init/.agents/skills/earos-review/references/output-template.md +180 -0
  30. package/assets/init/.agents/skills/earos-template-fill/SKILL.md +177 -0
  31. package/assets/init/.agents/skills/earos-template-fill/references/evidence-writing-guide.md +186 -0
  32. package/assets/init/.agents/skills/earos-template-fill/references/section-rubric-mapping.md +200 -0
  33. package/assets/init/.agents/skills/earos-validate/SKILL.md +113 -0
  34. package/assets/init/.agents/skills/earos-validate/references/fix-patterns.md +281 -0
  35. package/assets/init/.agents/skills/earos-validate/references/validation-checks.md +287 -0
  36. package/assets/init/.claude/CLAUDE.md +4 -0
  37. package/assets/init/AGENTS.md +293 -0
  38. package/assets/init/CLAUDE.md +635 -0
  39. package/assets/init/README.md +507 -0
  40. package/assets/init/calibration/gold-set/.gitkeep +0 -0
  41. package/assets/init/calibration/results/.gitkeep +0 -0
  42. package/assets/init/core/core-meta-rubric.yaml +643 -0
  43. package/assets/init/docs/consistency-report.md +325 -0
  44. package/assets/init/docs/getting-started.md +194 -0
  45. package/assets/init/docs/profile-authoring-guide.md +51 -0
  46. package/assets/init/docs/terminology.md +126 -0
  47. package/assets/init/earos.manifest.yaml +104 -0
  48. package/assets/init/evaluations/.gitkeep +0 -0
  49. package/assets/init/examples/aws-event-driven-order-processing/artifact.yaml +2056 -0
  50. package/assets/init/examples/aws-event-driven-order-processing/evaluation.yaml +973 -0
  51. package/assets/init/examples/aws-event-driven-order-processing/report.md +244 -0
  52. package/assets/init/examples/example-solution-architecture.evaluation.yaml +136 -0
  53. package/assets/init/examples/multi-cloud-data-analytics/artifact.yaml +715 -0
  54. package/assets/init/overlays/data-governance.yaml +94 -0
  55. package/assets/init/overlays/regulatory.yaml +154 -0
  56. package/assets/init/overlays/security.yaml +92 -0
  57. package/assets/init/profiles/adr.yaml +225 -0
  58. package/assets/init/profiles/capability-map.yaml +223 -0
  59. package/assets/init/profiles/reference-architecture.yaml +426 -0
  60. package/assets/init/profiles/roadmap.yaml +205 -0
  61. package/assets/init/profiles/solution-architecture.yaml +227 -0
  62. package/assets/init/research/architecture-assessment-rubrics-research.docx +0 -0
  63. package/assets/init/research/architecture-assessment-rubrics-research.md +566 -0
  64. package/assets/init/research/reference-architecture-research.md +751 -0
  65. package/assets/init/standard/EAROS.md +1426 -0
  66. package/assets/init/standard/schemas/artifact.schema.json +1295 -0
  67. package/assets/init/standard/schemas/artifact.uischema.json +65 -0
  68. package/assets/init/standard/schemas/evaluation.schema.json +284 -0
  69. package/assets/init/standard/schemas/rubric.schema.json +383 -0
  70. package/assets/init/templates/evaluation-record.template.yaml +58 -0
  71. package/assets/init/templates/new-profile.template.yaml +65 -0
  72. package/bin.js +188 -0
  73. package/dist/assets/_basePickBy-BVu6YmSW.js +1 -0
  74. package/dist/assets/_baseUniq-CWRzQDz_.js +1 -0
  75. package/dist/assets/arc-CyDBhtDM.js +1 -0
  76. package/dist/assets/architectureDiagram-2XIMDMQ5-BH6O4dvN.js +36 -0
  77. package/dist/assets/blockDiagram-WCTKOSBZ-2xmwdjpg.js +132 -0
  78. package/dist/assets/c4Diagram-IC4MRINW-BNmPRFJF.js +10 -0
  79. package/dist/assets/channel-CiySTNoJ.js +1 -0
  80. package/dist/assets/chunk-4BX2VUAB-DGQTvirp.js +1 -0
  81. package/dist/assets/chunk-55IACEB6-DNMAQAC_.js +1 -0
  82. package/dist/assets/chunk-FMBD7UC4-BJbVTQ5o.js +15 -0
  83. package/dist/assets/chunk-JSJVCQXG-BCxUL74A.js +1 -0
  84. package/dist/assets/chunk-KX2RTZJC-H7wWZOfz.js +1 -0
  85. package/dist/assets/chunk-NQ4KR5QH-BK4RlTQF.js +220 -0
  86. package/dist/assets/chunk-QZHKN3VN-0chxDV5g.js +1 -0
  87. package/dist/assets/chunk-WL4C6EOR-DexfQ-AV.js +189 -0
  88. package/dist/assets/classDiagram-VBA2DB6C-D7luWJQn.js +1 -0
  89. package/dist/assets/classDiagram-v2-RAHNMMFH-D7luWJQn.js +1 -0
  90. package/dist/assets/clone-ylgRbd3D.js +1 -0
  91. package/dist/assets/cose-bilkent-S5V4N54A-DS2IOCfZ.js +1 -0
  92. package/dist/assets/cytoscape.esm-CyJtwmzi.js +331 -0
  93. package/dist/assets/dagre-KLK3FWXG-BbSoTTa3.js +4 -0
  94. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  95. package/dist/assets/diagram-E7M64L7V-C9TvYgv0.js +24 -0
  96. package/dist/assets/diagram-IFDJBPK2-DowUMWrg.js +43 -0
  97. package/dist/assets/diagram-P4PSJMXO-BL6nrnQF.js +24 -0
  98. package/dist/assets/erDiagram-INFDFZHY-rXPRl8VM.js +70 -0
  99. package/dist/assets/flowDiagram-PKNHOUZH-DBRM99-W.js +162 -0
  100. package/dist/assets/ganttDiagram-A5KZAMGK-INcWFsBT.js +292 -0
  101. package/dist/assets/gitGraphDiagram-K3NZZRJ6-DMwpfE91.js +65 -0
  102. package/dist/assets/graph-DLQn37b-.js +1 -0
  103. package/dist/assets/index-BFFITMT8.js +650 -0
  104. package/dist/assets/index-H7f6VTz1.css +1 -0
  105. package/dist/assets/infoDiagram-LFFYTUFH-B0f4TWRM.js +2 -0
  106. package/dist/assets/init-Gi6I4Gst.js +1 -0
  107. package/dist/assets/ishikawaDiagram-PHBUUO56-CsU6XimZ.js +70 -0
  108. package/dist/assets/journeyDiagram-4ABVD52K-CQ7ibNib.js +139 -0
  109. package/dist/assets/kanban-definition-K7BYSVSG-DzEN7THt.js +89 -0
  110. package/dist/assets/katex-B1X10hvy.js +261 -0
  111. package/dist/assets/layout-C0dvb42R.js +1 -0
  112. package/dist/assets/linear-j4a8mGj7.js +1 -0
  113. package/dist/assets/mindmap-definition-YRQLILUH-DP8iEuCf.js +68 -0
  114. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  115. package/dist/assets/pieDiagram-SKSYHLDU-BpIAXgAm.js +30 -0
  116. package/dist/assets/quadrantDiagram-337W2JSQ-DrpXn5Eg.js +7 -0
  117. package/dist/assets/requirementDiagram-Z7DCOOCP-Bg7EwHlG.js +73 -0
  118. package/dist/assets/sankeyDiagram-WA2Y5GQK-BWagRs1F.js +10 -0
  119. package/dist/assets/sequenceDiagram-2WXFIKYE-q5jwhivG.js +145 -0
  120. package/dist/assets/stateDiagram-RAJIS63D-B_J9pE-2.js +1 -0
  121. package/dist/assets/stateDiagram-v2-FVOUBMTO-Q_1GcybB.js +1 -0
  122. package/dist/assets/timeline-definition-YZTLITO2-dv0jgQ0z.js +61 -0
  123. package/dist/assets/treemap-KZPCXAKY-Dt1dkIE7.js +162 -0
  124. package/dist/assets/vennDiagram-LZ73GAT5-BdO5RgRZ.js +34 -0
  125. package/dist/assets/xychartDiagram-JWTSCODW-CpDVe-8v.js +7 -0
  126. package/dist/index.html +23 -0
  127. package/export-docx.js +1583 -0
  128. package/init.js +353 -0
  129. package/manifest-cli.mjs +207 -0
  130. package/package.json +83 -0
  131. package/schemas/artifact.schema.json +1295 -0
  132. package/schemas/artifact.uischema.json +65 -0
  133. package/schemas/evaluation.schema.json +284 -0
  134. package/schemas/rubric.schema.json +383 -0
  135. package/serve.js +238 -0
package/export-docx.js ADDED
@@ -0,0 +1,1583 @@
1
+ /**
2
+ * EaROS — Export artifact data to a Microsoft Word (.docx) document.
3
+ *
4
+ * Mermaid diagrams are rendered in the browser for the editor export flow and
5
+ * passed to the server as PNGs. Kroki remains as a fallback for CLI/server-only
6
+ * exports or any diagram the browser did not pre-render.
7
+ */
8
+ import { Document, Packer, Paragraph, TextRun, HeadingLevel, Table, TableRow, TableCell, ImageRun, AlignmentType, WidthType, BorderStyle, PageBreak, Header, Footer, SectionType, SimpleField, TableOfContents, } from 'docx';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { dirname, extname, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
13
+ function loadArtifactSchema() {
14
+ const candidates = [
15
+ resolve(MODULE_DIR, 'schemas', 'artifact.schema.json'),
16
+ resolve(MODULE_DIR, '../schemas', 'artifact.schema.json'),
17
+ resolve(MODULE_DIR, '../../standard/schemas', 'artifact.schema.json'),
18
+ ];
19
+ for (const candidate of candidates) {
20
+ try {
21
+ return JSON.parse(readFileSync(candidate, 'utf8'));
22
+ }
23
+ catch {
24
+ // Try next candidate path.
25
+ }
26
+ }
27
+ console.warn('[export-docx] Artifact schema not found; falling back to shape-driven rendering');
28
+ return null;
29
+ }
30
+ const ARTIFACT_SCHEMA = loadArtifactSchema();
31
+ // ─── Kroki Mermaid rendering ──────────────────────────────────────────────────
32
+ const KROKI_URL = 'https://kroki.io/mermaid/png';
33
+ const LOCAL_MERMAID_IMAGE_PREFIXES = ['/icons/', '/mermaid-icons/'];
34
+ const LOCAL_MERMAID_IMAGE_DIRS = [
35
+ resolve(process.cwd()),
36
+ resolve(MODULE_DIR, 'public'),
37
+ resolve(MODULE_DIR, 'dist'),
38
+ ];
39
+ const MERMAID_IMAGE_MIME_TYPES = {
40
+ '.gif': 'image/gif',
41
+ '.jpeg': 'image/jpeg',
42
+ '.jpg': 'image/jpeg',
43
+ '.png': 'image/png',
44
+ '.svg': 'image/svg+xml',
45
+ '.webp': 'image/webp',
46
+ };
47
+ const mermaidImageDataUrlCache = new Map();
48
+ function resolveLocalMermaidImage(assetPath) {
49
+ const relativePath = assetPath.replace(/^\/+/, '');
50
+ for (const baseDir of LOCAL_MERMAID_IMAGE_DIRS) {
51
+ const candidate = resolve(baseDir, relativePath);
52
+ if (existsSync(candidate))
53
+ return candidate;
54
+ }
55
+ return null;
56
+ }
57
+ function localImageToDataUrl(filePath) {
58
+ const ext = extname(filePath).toLowerCase();
59
+ const mime = MERMAID_IMAGE_MIME_TYPES[ext] ?? 'application/octet-stream';
60
+ const bytes = readFileSync(filePath);
61
+ return `data:${mime};base64,${bytes.toString('base64')}`;
62
+ }
63
+ function inlineLocalMermaidImages(diagram, label) {
64
+ return diagram.replace(/img:\s*(['"])(\/(?:icons|mermaid-icons)\/[^'"]+)\1/g, (match, quote, assetPath) => {
65
+ const cached = mermaidImageDataUrlCache.get(assetPath);
66
+ if (cached)
67
+ return `img: ${quote}${cached}${quote}`;
68
+ const filePath = resolveLocalMermaidImage(assetPath);
69
+ if (!filePath) {
70
+ console.warn(`[export-docx] Mermaid image asset not found for "${label}": ${assetPath}`);
71
+ return match;
72
+ }
73
+ const dataUrl = localImageToDataUrl(filePath);
74
+ mermaidImageDataUrlCache.set(assetPath, dataUrl);
75
+ return `img: ${quote}${dataUrl}${quote}`;
76
+ });
77
+ }
78
+ async function renderMermaidDiagram(diagram, label) {
79
+ try {
80
+ const preparedDiagram = LOCAL_MERMAID_IMAGE_PREFIXES.some((prefix) => diagram.includes(prefix))
81
+ ? inlineLocalMermaidImages(diagram.trim(), label)
82
+ : diagram.trim();
83
+ const response = await fetch(KROKI_URL, {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'text/plain' },
86
+ body: preparedDiagram,
87
+ signal: AbortSignal.timeout(30_000),
88
+ });
89
+ if (!response.ok) {
90
+ console.warn(`[export-docx] Diagram render failed for "${label}": HTTP ${response.status}`);
91
+ return null;
92
+ }
93
+ const arr = await response.arrayBuffer();
94
+ const buf = Buffer.from(arr);
95
+ if (buf.length === 0) {
96
+ console.warn(`[export-docx] Diagram render returned empty buffer for "${label}"`);
97
+ return null;
98
+ }
99
+ return buf;
100
+ }
101
+ catch (err) {
102
+ console.warn(`[export-docx] Diagram render error for "${label}":`, err);
103
+ return null;
104
+ }
105
+ }
106
+ function isBrowserRenderedDiagram(value) {
107
+ return !!value
108
+ && typeof value === 'object'
109
+ && (typeof value.pngBase64 === 'string'
110
+ || typeof value.svgText === 'string')
111
+ && Number.isFinite(value.width)
112
+ && Number.isFinite(value.height);
113
+ }
114
+ function extractDiagrams(obj, path = '', label = '') {
115
+ if (!obj || typeof obj !== 'object')
116
+ return [];
117
+ const refs = [];
118
+ // Fields that may contain Mermaid source
119
+ const DIAGRAM_FIELDS = new Set(['diagram_source', 'diagram', 'mermaid', 'mermaid_source']);
120
+ for (const [k, v] of Object.entries(obj)) {
121
+ const p = path ? `${path}.${k}` : k;
122
+ const l = label ? `${label} › ${k}` : k;
123
+ if (DIAGRAM_FIELDS.has(k) && typeof v === 'string' && v.trim()) {
124
+ refs.push({ key: p, source: v.trim(), label: l });
125
+ }
126
+ else if (Array.isArray(v)) {
127
+ v.forEach((item, i) => refs.push(...extractDiagrams(item, `${p}[${i}]`, `${l}[${i}]`)));
128
+ }
129
+ else if (typeof v === 'object' && v !== null) {
130
+ refs.push(...extractDiagrams(v, p, l));
131
+ }
132
+ }
133
+ return refs;
134
+ }
135
+ // ─── docx helpers ─────────────────────────────────────────────────────────────
136
+ const NAVY = '1F3864';
137
+ const DARK_GREY = '404040';
138
+ const MID_GREY = '777777';
139
+ const ORANGE = 'C55A11';
140
+ const MAX_DIAGRAM_WIDTH = 580;
141
+ const MAX_DIAGRAM_HEIGHT = 340;
142
+ const TRANSPARENT_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9VE3VxkAAAAASUVORK5CYII=';
143
+ // Word ignores raw `\n` inside a text node, so emit explicit line breaks.
144
+ function textRuns(text, run) {
145
+ const lines = text.replace(/\r\n?/g, '\n').split('\n');
146
+ while (lines.length > 1 && lines[lines.length - 1] === '')
147
+ lines.pop();
148
+ return lines.map((line, index) => new TextRun({
149
+ ...run,
150
+ text: line === '' && lines.length > 1 ? ' ' : line,
151
+ ...(index > 0 ? { break: 1 } : {}),
152
+ }));
153
+ }
154
+ function h1(text) {
155
+ return new Paragraph({
156
+ text,
157
+ heading: HeadingLevel.HEADING_1,
158
+ spacing: { before: 400, after: 120 },
159
+ run: { color: NAVY, bold: true, font: 'Arial', size: 28 },
160
+ });
161
+ }
162
+ function h2(text) {
163
+ return new Paragraph({
164
+ text,
165
+ heading: HeadingLevel.HEADING_2,
166
+ spacing: { before: 300, after: 80 },
167
+ run: { color: NAVY, bold: true, font: 'Arial', size: 24 },
168
+ });
169
+ }
170
+ function h3(text) {
171
+ return new Paragraph({
172
+ text,
173
+ heading: HeadingLevel.HEADING_3,
174
+ spacing: { before: 200, after: 60 },
175
+ run: { color: ORANGE, bold: true, font: 'Arial', size: 22 },
176
+ });
177
+ }
178
+ function body(text, indent = 0) {
179
+ return new Paragraph({
180
+ style: 'Normal',
181
+ children: textRuns(text, { font: 'Arial', size: 20, color: DARK_GREY }),
182
+ spacing: { before: 60, after: 60 },
183
+ indent: indent ? { left: indent * 360 } : undefined,
184
+ });
185
+ }
186
+ function bullet(text, level = 0) {
187
+ return new Paragraph({
188
+ children: textRuns(text, { font: 'Arial', size: 20, color: DARK_GREY }),
189
+ bullet: { level },
190
+ spacing: { before: 40, after: 40 },
191
+ });
192
+ }
193
+ function label(key, value) {
194
+ return new Paragraph({
195
+ style: 'Normal',
196
+ children: [
197
+ new TextRun({ text: `${key}: `, font: 'Arial', size: 20, bold: true, color: DARK_GREY }),
198
+ ...textRuns(value, { font: 'Arial', size: 20, color: DARK_GREY }),
199
+ ],
200
+ spacing: { before: 40, after: 40 },
201
+ });
202
+ }
203
+ function italicNote(text) {
204
+ return new Paragraph({
205
+ style: 'Normal',
206
+ children: textRuns(text, { font: 'Arial', size: 18, italics: true, color: MID_GREY }),
207
+ spacing: { before: 60, after: 60 },
208
+ });
209
+ }
210
+ /** Indented consequence line under an assumption bullet */
211
+ function consequence(text) {
212
+ return new Paragraph({
213
+ style: 'Normal',
214
+ children: textRuns(`→ ${text}`, { font: 'Arial', size: 18, italics: true, color: MID_GREY }),
215
+ indent: { left: 720 },
216
+ spacing: { before: 20, after: 40 },
217
+ });
218
+ }
219
+ function pageBreak() {
220
+ return new Paragraph({ style: 'Normal', children: [new PageBreak()] });
221
+ }
222
+ function horizontalRule() {
223
+ return new Paragraph({
224
+ style: 'Normal',
225
+ border: { bottom: { color: 'CCCCCC', space: 1, style: BorderStyle.SINGLE, size: 6 } },
226
+ spacing: { before: 200, after: 200 },
227
+ children: [],
228
+ });
229
+ }
230
+ function diagImage(image) {
231
+ let width = MAX_DIAGRAM_WIDTH;
232
+ let height = MAX_DIAGRAM_HEIGHT;
233
+ if (Buffer.isBuffer(image)) {
234
+ return new Paragraph({
235
+ style: 'Normal',
236
+ children: [
237
+ new ImageRun({
238
+ data: image,
239
+ transformation: { width, height },
240
+ type: 'png',
241
+ }),
242
+ ],
243
+ alignment: AlignmentType.CENTER,
244
+ spacing: { before: 120, after: 120 },
245
+ });
246
+ }
247
+ const scale = Math.min(MAX_DIAGRAM_WIDTH / Math.max(image.width, 1), MAX_DIAGRAM_HEIGHT / Math.max(image.height, 1), 1);
248
+ width = Math.max(1, Math.round(image.width * scale));
249
+ height = Math.max(1, Math.round(image.height * scale));
250
+ const pngBuffer = typeof image.pngBase64 === 'string'
251
+ ? Buffer.from(image.pngBase64, 'base64')
252
+ : null;
253
+ const fallbackPng = pngBuffer ?? Buffer.from(TRANSPARENT_PNG_BASE64, 'base64');
254
+ const imageRun = pngBuffer
255
+ ? new ImageRun({
256
+ data: pngBuffer,
257
+ transformation: { width, height },
258
+ type: 'png',
259
+ })
260
+ : typeof image.svgText === 'string'
261
+ ? new ImageRun({
262
+ data: Buffer.from(image.svgText, 'utf8'),
263
+ transformation: { width, height },
264
+ type: 'svg',
265
+ fallback: { data: fallbackPng, type: 'png' },
266
+ })
267
+ : new ImageRun({
268
+ data: fallbackPng,
269
+ transformation: { width, height },
270
+ type: 'png',
271
+ });
272
+ return new Paragraph({
273
+ style: 'Normal',
274
+ children: [imageRun],
275
+ alignment: AlignmentType.CENTER,
276
+ spacing: { before: 120, after: 120 },
277
+ });
278
+ }
279
+ // Simple two-column key-value table
280
+ function kvTable(pairs) {
281
+ return new Table({
282
+ width: { size: 100, type: WidthType.PERCENTAGE },
283
+ rows: pairs.map(([k, v]) => new TableRow({
284
+ children: [
285
+ new TableCell({
286
+ width: { size: 25, type: WidthType.PERCENTAGE },
287
+ children: [new Paragraph({ style: 'Normal', children: textRuns(k, { bold: true, font: 'Arial', size: 18 }) })],
288
+ }),
289
+ new TableCell({
290
+ width: { size: 75, type: WidthType.PERCENTAGE },
291
+ children: [new Paragraph({ style: 'Normal', children: textRuns(v, { font: 'Arial', size: 18 }) })],
292
+ }),
293
+ ],
294
+ })),
295
+ });
296
+ }
297
+ // Generic header-row table
298
+ function dataTable(headers, rows) {
299
+ const columnCount = headers.length;
300
+ const headerRow = new TableRow({
301
+ tableHeader: true,
302
+ children: headers.map((h) => new TableCell({
303
+ shading: { fill: NAVY },
304
+ children: [
305
+ new Paragraph({
306
+ style: 'Normal',
307
+ children: textRuns(h, { bold: true, font: 'Arial', size: 18, color: 'FFFFFF' }),
308
+ }),
309
+ ],
310
+ })),
311
+ });
312
+ const dataRows = rows.map((row) => new TableRow({
313
+ children: Array.from({ length: columnCount }, (_, index) => row[index] ?? '').map((cell) => new TableCell({
314
+ children: [
315
+ new Paragraph({
316
+ style: 'Normal',
317
+ children: textRuns(cell ?? '', { font: 'Arial', size: 18 }),
318
+ }),
319
+ ],
320
+ })),
321
+ }));
322
+ return new Table({
323
+ width: { size: 100, type: WidthType.PERCENTAGE },
324
+ rows: [headerRow, ...dataRows],
325
+ });
326
+ }
327
+ function objectTable(items, schema) {
328
+ if (!items.length)
329
+ return null;
330
+ const keys = orderedObjectKeys(items, schema).filter((key) => items.every((item) => isScalarish(item?.[key])));
331
+ if (!keys.length)
332
+ return null;
333
+ return dataTable(keys.map((key) => displayLabel(key, propertySchema(schema, key))), items.map((item) => keys.map((key) => str(item?.[key]))));
334
+ }
335
+ // ─── Section renderers ────────────────────────────────────────────────────────
336
+ function str(v) {
337
+ if (v == null)
338
+ return '';
339
+ if (typeof v === 'string')
340
+ return v.trim();
341
+ if (typeof v === 'number' || typeof v === 'boolean')
342
+ return String(v);
343
+ return '';
344
+ }
345
+ function prettyLabel(key) {
346
+ return key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
347
+ }
348
+ function displayLabel(key, schema) {
349
+ return str(schema?.title) || prettyLabel(key);
350
+ }
351
+ function heading(level, text) {
352
+ if (level <= 1)
353
+ return h1(text);
354
+ if (level === 2)
355
+ return h2(text);
356
+ return h3(text);
357
+ }
358
+ function isPlainObject(value) {
359
+ return !!value && typeof value === 'object' && !Array.isArray(value);
360
+ }
361
+ function isScalarish(value) {
362
+ return value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
363
+ }
364
+ function schemaProperties(schema) {
365
+ return isPlainObject(schema?.properties) ? schema.properties : {};
366
+ }
367
+ function propertySchema(schema, key) {
368
+ return schemaProperties(schema)[key];
369
+ }
370
+ function itemSchema(schema) {
371
+ return isPlainObject(schema?.items) ? schema.items : undefined;
372
+ }
373
+ function sectionSchema(key) {
374
+ const sections = propertySchema(ARTIFACT_SCHEMA, 'sections');
375
+ return propertySchema(sections, key);
376
+ }
377
+ function topLevelSchema(key) {
378
+ return propertySchema(ARTIFACT_SCHEMA, key);
379
+ }
380
+ function orderedObjectKeys(items, schema) {
381
+ const objects = Array.isArray(items) ? items.filter(isPlainObject) : [items].filter(isPlainObject);
382
+ const keys = [];
383
+ const seen = new Set();
384
+ for (const key of Object.keys(schemaProperties(schema))) {
385
+ if (objects.some((item) => key in item)) {
386
+ seen.add(key);
387
+ keys.push(key);
388
+ }
389
+ }
390
+ for (const item of objects) {
391
+ for (const key of Object.keys(item)) {
392
+ if (seen.has(key))
393
+ continue;
394
+ seen.add(key);
395
+ keys.push(key);
396
+ }
397
+ }
398
+ return keys;
399
+ }
400
+ function isScalarObject(value) {
401
+ return isPlainObject(value) && Object.values(value).every(isScalarish);
402
+ }
403
+ function primaryObjectField(value, schema) {
404
+ const preferred = ['title', 'name', 'id', 'term', 'section', 'control', 'role', 'version'];
405
+ for (const key of preferred) {
406
+ const text = str(value[key]);
407
+ if (text)
408
+ return { key, text };
409
+ }
410
+ for (const key of orderedObjectKeys(value, schema)) {
411
+ const text = str(value[key]);
412
+ if (text)
413
+ return { key, text };
414
+ }
415
+ return null;
416
+ }
417
+ function renderStructuredArray(items, children, schema, level = 2) {
418
+ if (!items.length)
419
+ return;
420
+ if (items.every(isScalarish)) {
421
+ for (const item of items)
422
+ children.push(bullet(str(item), Math.max(level - 2, 0)));
423
+ return;
424
+ }
425
+ if (items.every(isScalarObject)) {
426
+ const table = objectTable(items, itemSchema(schema));
427
+ if (table) {
428
+ children.push(table);
429
+ return;
430
+ }
431
+ }
432
+ const childSchema = itemSchema(schema);
433
+ for (const [index, item] of items.entries()) {
434
+ if (isScalarish(item)) {
435
+ children.push(bullet(str(item), Math.max(level - 2, 0)));
436
+ continue;
437
+ }
438
+ if (!isPlainObject(item))
439
+ continue;
440
+ const primary = primaryObjectField(item, childSchema);
441
+ if (primary) {
442
+ children.push(heading(level, primary.text));
443
+ }
444
+ else {
445
+ children.push(heading(level, `Item ${index + 1}`));
446
+ }
447
+ if (isScalarObject(item)) {
448
+ const pairs = orderedObjectKeys(item, childSchema)
449
+ .filter((key) => key !== primary?.key && str(item[key]))
450
+ .map((key) => [displayLabel(key, propertySchema(childSchema, key)), str(item[key])]);
451
+ if (pairs.length)
452
+ children.push(kvTable(pairs));
453
+ continue;
454
+ }
455
+ renderStructuredObject(item, children, childSchema, Math.min(level + 1, 3), new Set(primary ? [primary.key] : []));
456
+ }
457
+ }
458
+ function renderStructuredObject(value, children, schema, level = 2, skipKeys = new Set()) {
459
+ for (const key of orderedObjectKeys(value, schema)) {
460
+ if (skipKeys.has(key))
461
+ continue;
462
+ const child = value[key];
463
+ if (child == null || child === '')
464
+ continue;
465
+ const childSchema = propertySchema(schema, key);
466
+ const title = displayLabel(key, childSchema);
467
+ children.push(heading(level, title));
468
+ renderStructuredValue(child, children, childSchema, Math.min(level + 1, 3));
469
+ }
470
+ }
471
+ function renderStructuredValue(value, children, schema, level = 2) {
472
+ if (value == null || value === '')
473
+ return;
474
+ if (isScalarish(value)) {
475
+ children.push(body(str(value)));
476
+ return;
477
+ }
478
+ if (Array.isArray(value)) {
479
+ renderStructuredArray(value, children, schema, level);
480
+ return;
481
+ }
482
+ if (isPlainObject(value)) {
483
+ renderStructuredObject(value, children, schema, level);
484
+ }
485
+ }
486
+ function renderStringList(items, children, indent = 1, schema) {
487
+ if (!Array.isArray(items))
488
+ return;
489
+ if (items.length && items.every(isScalarObject)) {
490
+ const table = objectTable(items, schema);
491
+ if (table) {
492
+ children.push(table);
493
+ return;
494
+ }
495
+ }
496
+ for (const item of items) {
497
+ if (isScalarish(item))
498
+ children.push(bullet(str(item), indent - 1));
499
+ else if (item && typeof item === 'object') {
500
+ const text = item.description ?? item.statement ?? item.text ?? JSON.stringify(item);
501
+ children.push(bullet(str(text), indent - 1));
502
+ }
503
+ }
504
+ }
505
+ function renderMetadata(metadata, children) {
506
+ children.push(h1('Metadata'));
507
+ const pairs = [];
508
+ const SKIP = new Set(['stakeholders', 'change_log']);
509
+ for (const [k, v] of Object.entries(metadata)) {
510
+ if (SKIP.has(k))
511
+ continue;
512
+ if (typeof v === 'string' || typeof v === 'number') {
513
+ pairs.push([k.replace(/_/g, ' '), str(v)]);
514
+ }
515
+ }
516
+ if (pairs.length)
517
+ children.push(kvTable(pairs));
518
+ // Stakeholders table
519
+ const stakeholders = metadata.stakeholders;
520
+ if (Array.isArray(stakeholders) && stakeholders.length) {
521
+ children.push(h2('Stakeholders'));
522
+ children.push(dataTable(['Role', 'Name / Team', 'Concerns'], stakeholders.map((s) => [str(s.role), str(s.name), str(s.concerns)])));
523
+ }
524
+ // Change log table
525
+ const changeLog = metadata.change_log;
526
+ if (Array.isArray(changeLog) && changeLog.length) {
527
+ children.push(h2('Change Log'));
528
+ children.push(dataTable(['Version', 'Date', 'Author', 'Changes'], changeLog.map((c) => [
529
+ str(c.version),
530
+ str(c.date),
531
+ str(c.author),
532
+ Array.isArray(c.changes) ? c.changes.map(str).join('\n') : str(c.changes),
533
+ ])));
534
+ }
535
+ }
536
+ function renderReadingGuide(data, children) {
537
+ children.push(h1('Reading Guide'));
538
+ if (data.how_to_use)
539
+ children.push(body(str(data.how_to_use)));
540
+ const map = data.section_map;
541
+ if (Array.isArray(map) && map.length) {
542
+ children.push(h2('Section Map'));
543
+ children.push(dataTable(['Section', 'Audience', 'Concern'], map.map((m) => [str(m.section), str(m.audience), str(m.concern)])));
544
+ }
545
+ }
546
+ function renderScope(data, children) {
547
+ children.push(h1('Scope'));
548
+ if (data.statement)
549
+ children.push(body(str(data.statement)));
550
+ if (Array.isArray(data.in_scope) && data.in_scope.length) {
551
+ children.push(h2('In Scope'));
552
+ renderStringList(data.in_scope, children);
553
+ }
554
+ if (Array.isArray(data.out_of_scope) && data.out_of_scope.length) {
555
+ children.push(h2('Out of Scope'));
556
+ renderStringList(data.out_of_scope, children);
557
+ }
558
+ if (data.boundary_definition) {
559
+ children.push(h2('Boundary Definition'));
560
+ children.push(body(str(data.boundary_definition)));
561
+ }
562
+ if (Array.isArray(data.assumptions) && data.assumptions.length) {
563
+ children.push(h2('Assumptions'));
564
+ for (const a of data.assumptions) {
565
+ const text = typeof a === 'string' ? a : str(a.assumption);
566
+ children.push(bullet(text));
567
+ if (a.consequence_if_violated)
568
+ children.push(consequence(str(a.consequence_if_violated)));
569
+ }
570
+ }
571
+ }
572
+ function renderDriversAndPrinciples(data, children) {
573
+ children.push(h1('Business Context and Drivers'));
574
+ if (Array.isArray(data.drivers) && data.drivers.length) {
575
+ children.push(h2('Business Drivers'));
576
+ for (const d of data.drivers) {
577
+ children.push(h3(`${str(d.id)} — ${str(d.description).split('\n')[0].substring(0, 80)}`));
578
+ if (d.description)
579
+ children.push(body(str(d.description)));
580
+ if (d.architecture_response) {
581
+ children.push(body('Architecture response:'));
582
+ children.push(body(str(d.architecture_response), 1));
583
+ }
584
+ }
585
+ }
586
+ if (Array.isArray(data.principles) && data.principles.length) {
587
+ children.push(h2('Architecture Principles'));
588
+ children.push(dataTable(['ID', 'Principle', 'How Applied'], data.principles.map((p) => [str(p.id), str(p.name), str(p.how_applied)])));
589
+ }
590
+ }
591
+ function renderView(viewLabel, data, children, diagramImages, diagramKey) {
592
+ children.push(h2(viewLabel));
593
+ const desc = data.description ?? data.narrative ?? data.overview;
594
+ if (desc)
595
+ children.push(body(str(desc)));
596
+ // Render diagram if available
597
+ const imgBuf = diagramImages.get(diagramKey);
598
+ if (imgBuf && (Buffer.isBuffer(imgBuf) ? imgBuf.length > 0 : !!imgBuf.pngBase64)) {
599
+ children.push(diagImage(imgBuf));
600
+ }
601
+ else if (data.diagram_source || data.diagram || data.mermaid || data.mermaid_source) {
602
+ children.push(italicNote(`[${viewLabel} diagram could not be rendered for export]`));
603
+ }
604
+ // Narrative steps (data flow view)
605
+ if (Array.isArray(data.narrative_steps)) {
606
+ for (const step of data.narrative_steps) {
607
+ children.push(bullet(`Step ${step.step ?? ''}: ${str(step.description)}`));
608
+ }
609
+ }
610
+ }
611
+ function renderArchitectureViews(data, children, diagramImages) {
612
+ children.push(h1('Architecture Views'));
613
+ const VIEW_LABELS = {
614
+ context: 'Context View (C4 Level 1)',
615
+ functional: 'Functional / Container View',
616
+ deployment: 'Deployment View',
617
+ data_flow: 'Data Flow View',
618
+ security: 'Security View',
619
+ };
620
+ for (const [key, viewData] of Object.entries(data)) {
621
+ if (typeof viewData !== 'object' || viewData === null)
622
+ continue;
623
+ const viewLabel = VIEW_LABELS[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
624
+ const diagramKey = `sections.architecture_views.${key}.diagram_source`;
625
+ renderView(viewLabel, viewData, children, diagramImages, diagramKey);
626
+ }
627
+ }
628
+ function renderCrosscuttingConcerns(data, children) {
629
+ children.push(h1('Crosscutting Concerns'));
630
+ for (const [key, value] of Object.entries(data)) {
631
+ children.push(h2(key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())));
632
+ if (typeof value === 'string') {
633
+ children.push(body(value));
634
+ }
635
+ else if (Array.isArray(value)) {
636
+ renderStringList(value, children);
637
+ }
638
+ else if (typeof value === 'object' && value !== null) {
639
+ for (const [k, v] of Object.entries(value)) {
640
+ children.push(h3(k.replace(/_/g, ' ')));
641
+ if (typeof v === 'string')
642
+ children.push(body(v));
643
+ else if (Array.isArray(v))
644
+ renderStringList(v, children);
645
+ }
646
+ }
647
+ }
648
+ }
649
+ function renderArchitectureDecisions(data, children) {
650
+ children.push(h1('Architecture Decisions (ADRs)'));
651
+ const adrs = Array.isArray(data) ? data : Object.values(data);
652
+ for (const adr of adrs) {
653
+ if (typeof adr !== 'object' || !adr)
654
+ continue;
655
+ const title = `${str(adr.id) || ''} — ${str(adr.title) || str(adr.decision_title) || ''}`;
656
+ children.push(h2(title.replace(/^— /, '')));
657
+ const pairs = [];
658
+ if (adr.status)
659
+ pairs.push(['Status', str(adr.status)]);
660
+ if (adr.date)
661
+ pairs.push(['Date', str(adr.date)]);
662
+ if (pairs.length)
663
+ children.push(kvTable(pairs));
664
+ if (adr.context) {
665
+ children.push(h3('Context'));
666
+ children.push(body(str(adr.context)));
667
+ }
668
+ if (adr.decision) {
669
+ children.push(h3('Decision'));
670
+ children.push(body(str(adr.decision)));
671
+ }
672
+ if (adr.rationale) {
673
+ children.push(h3('Rationale'));
674
+ children.push(body(str(adr.rationale)));
675
+ }
676
+ if (adr.consequences) {
677
+ children.push(h3('Consequences'));
678
+ children.push(body(str(adr.consequences)));
679
+ }
680
+ // Trade-offs (structured or string)
681
+ if (adr.trade_offs) {
682
+ children.push(h3('Trade-offs'));
683
+ if (typeof adr.trade_offs === 'string') {
684
+ children.push(body(str(adr.trade_offs)));
685
+ }
686
+ else if (typeof adr.trade_offs === 'object') {
687
+ if (Array.isArray(adr.trade_offs.benefits) && adr.trade_offs.benefits.length) {
688
+ children.push(body('Benefits:'));
689
+ renderStringList(adr.trade_offs.benefits, children);
690
+ }
691
+ if (Array.isArray(adr.trade_offs.drawbacks) && adr.trade_offs.drawbacks.length) {
692
+ children.push(body('Drawbacks:'));
693
+ renderStringList(adr.trade_offs.drawbacks, children);
694
+ }
695
+ }
696
+ }
697
+ if (adr.revisit_trigger) {
698
+ children.push(h3('Revisit Trigger'));
699
+ children.push(body(str(adr.revisit_trigger)));
700
+ }
701
+ // Handle both 'alternatives_considered' and 'options' field names
702
+ const alternatives = adr.alternatives_considered ?? adr.options;
703
+ if (Array.isArray(alternatives) && alternatives.length) {
704
+ children.push(h3('Options Considered'));
705
+ for (const alt of alternatives) {
706
+ if (typeof alt === 'string') {
707
+ children.push(bullet(alt));
708
+ }
709
+ else {
710
+ const id = str(alt.id ?? alt.option ?? alt.name ?? '');
711
+ const desc = str(alt.description ?? alt.reason ?? alt.rationale ?? '');
712
+ const optTitle = id && desc ? `Option ${id}: ${desc}` : id || desc;
713
+ children.push(bullet(optTitle));
714
+ if (Array.isArray(alt.pros) && alt.pros.length) {
715
+ children.push(body('Pros:', 1));
716
+ for (const p of alt.pros)
717
+ children.push(bullet(str(p), 1));
718
+ }
719
+ if (Array.isArray(alt.cons) && alt.cons.length) {
720
+ children.push(body('Cons:', 1));
721
+ for (const c of alt.cons)
722
+ children.push(bullet(str(c), 1));
723
+ }
724
+ }
725
+ }
726
+ }
727
+ }
728
+ }
729
+ function renderQualityAttributes(data, children) {
730
+ children.push(h1('Quality Attributes'));
731
+ const items = Array.isArray(data) ? data : Object.values(data);
732
+ if (!items.length)
733
+ return;
734
+ const table = objectTable(items.filter((item) => item && typeof item === 'object' && !Array.isArray(item)), itemSchema(sectionSchema('quality_attributes')));
735
+ if (table)
736
+ children.push(table);
737
+ }
738
+ function renderOperationalModel(data, children) {
739
+ children.push(h1('Operational Model'));
740
+ renderStructuredObject(data, children, sectionSchema('operational_model'), 2);
741
+ }
742
+ function renderImplementationGuidance(data, children) {
743
+ children.push(h1('Implementation'));
744
+ for (const [key, value] of Object.entries(data)) {
745
+ children.push(h2(key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())));
746
+ if (typeof value === 'string')
747
+ children.push(body(value));
748
+ else if (Array.isArray(value)) {
749
+ // If array of objects with name/description, render as sub-items
750
+ for (const item of value) {
751
+ if (typeof item === 'string') {
752
+ children.push(bullet(item));
753
+ }
754
+ else if (typeof item === 'object' && item !== null) {
755
+ const name = str(item.name ?? item.title ?? item.id ?? '');
756
+ const desc = str(item.description ?? item.purpose ?? '');
757
+ if (name)
758
+ children.push(h3(name));
759
+ if (desc)
760
+ children.push(body(desc));
761
+ // Remaining string fields as labels
762
+ for (const [k, v] of Object.entries(item)) {
763
+ if (k === 'name' || k === 'title' || k === 'id' || k === 'description' || k === 'purpose')
764
+ continue;
765
+ if (typeof v === 'string')
766
+ children.push(label(k.replace(/_/g, ' '), v));
767
+ }
768
+ }
769
+ }
770
+ }
771
+ else if (typeof value === 'object' && value !== null) {
772
+ for (const [k, v] of Object.entries(value)) {
773
+ children.push(h3(k.replace(/_/g, ' ')));
774
+ if (typeof v === 'string')
775
+ children.push(body(v));
776
+ else if (Array.isArray(v))
777
+ renderStringList(v, children);
778
+ }
779
+ }
780
+ }
781
+ }
782
+ function renderGovernance(data, children) {
783
+ children.push(h1('Governance and Compliance'));
784
+ const schema = sectionSchema('governance');
785
+ const compliance = data.compliance_mapping;
786
+ if (Array.isArray(compliance) && compliance.length) {
787
+ children.push(h2('Compliance Mapping'));
788
+ const table = objectTable(compliance.filter((item) => item && typeof item === 'object' && !Array.isArray(item)), itemSchema(propertySchema(schema, 'compliance_mapping')));
789
+ if (table)
790
+ children.push(table);
791
+ }
792
+ const risks = data.risk_register;
793
+ if (Array.isArray(risks) && risks.length) {
794
+ children.push(h2('Risk Register'));
795
+ children.push(dataTable(['Risk', 'Likelihood', 'Impact', 'Mitigation'], risks.map((r) => [str(r.risk), str(r.likelihood), str(r.impact), str(r.mitigation)])));
796
+ }
797
+ if (data.review_cadence) {
798
+ children.push(h2('Review Cadence'));
799
+ children.push(body(str(data.review_cadence)));
800
+ }
801
+ // Remaining object sub-sections
802
+ for (const [key, value] of Object.entries(data)) {
803
+ if (['compliance_mapping', 'risk_register', 'review_cadence'].includes(key))
804
+ continue;
805
+ children.push(h2(displayLabel(key, propertySchema(schema, key))));
806
+ if (typeof value === 'string')
807
+ children.push(body(value));
808
+ else if (Array.isArray(value))
809
+ renderStringList(value, children, 1, propertySchema(schema, key));
810
+ else if (typeof value === 'object' && value !== null) {
811
+ for (const [k, v] of Object.entries(value)) {
812
+ if (typeof v === 'string')
813
+ children.push(label(displayLabel(k, propertySchema(propertySchema(schema, key), k)), v));
814
+ else if (Array.isArray(v))
815
+ renderStringList(v, children, 1, propertySchema(propertySchema(schema, key), k));
816
+ }
817
+ }
818
+ }
819
+ }
820
+ function renderRaid(data, children) {
821
+ children.push(h1('RAID Register'));
822
+ const SECTION_LABELS = {
823
+ risks: 'Risks',
824
+ assumptions: 'Assumptions',
825
+ issues: 'Issues',
826
+ dependencies: 'Dependencies',
827
+ };
828
+ for (const [key, value] of Object.entries(data)) {
829
+ if (!Array.isArray(value) || !value.length)
830
+ continue;
831
+ const sectionTitle = SECTION_LABELS[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
832
+ children.push(h2(sectionTitle));
833
+ for (const item of value) {
834
+ if (typeof item === 'string') {
835
+ children.push(bullet(item));
836
+ continue;
837
+ }
838
+ if (typeof item !== 'object' || !item)
839
+ continue;
840
+ const id = str(item.id ?? '');
841
+ const desc = str(item.description ?? item.statement ?? item.text ?? '');
842
+ const heading = id ? `${id}: ${desc.substring(0, 80)}` : desc.substring(0, 80);
843
+ if (heading)
844
+ children.push(h3(heading));
845
+ if (desc && desc.length > 80)
846
+ children.push(body(desc));
847
+ const FIELD_LABELS = {
848
+ likelihood: 'Likelihood',
849
+ impact: 'Impact',
850
+ owner: 'Owner',
851
+ residual_risk: 'Residual Risk',
852
+ status: 'Status',
853
+ due_date: 'Due Date',
854
+ dependency_type: 'Dependency Type',
855
+ source: 'Source',
856
+ };
857
+ for (const [f, l] of Object.entries(FIELD_LABELS)) {
858
+ if (item[f])
859
+ children.push(label(l, str(item[f])));
860
+ }
861
+ if (item.mitigation) {
862
+ children.push(h3('Mitigation'));
863
+ children.push(body(str(item.mitigation), 1));
864
+ }
865
+ if (item.resolution) {
866
+ children.push(h3('Resolution'));
867
+ children.push(body(str(item.resolution), 1));
868
+ }
869
+ }
870
+ }
871
+ }
872
+ function renderDecisionsAndActions(data, children) {
873
+ children.push(h1('Governance Decision'));
874
+ if (data.governance_outcome) {
875
+ children.push(label('Outcome', str(data.governance_outcome)));
876
+ }
877
+ if (data.decision_statement) {
878
+ children.push(h2('Decision Statement'));
879
+ children.push(body(str(data.decision_statement)));
880
+ }
881
+ if (Array.isArray(data.conditions) && data.conditions.length) {
882
+ children.push(h2('Conditions'));
883
+ renderStringList(data.conditions, children);
884
+ }
885
+ if (Array.isArray(data.next_actions) && data.next_actions.length) {
886
+ children.push(h2('Next Actions'));
887
+ children.push(dataTable(['Action', 'Owner', 'Target Date'], data.next_actions.map((a) => [str(a.action), str(a.owner), str(a.target_date)])));
888
+ }
889
+ }
890
+ function renderGettingStarted(data, children) {
891
+ children.push(h1('Getting Started'));
892
+ if (data.estimated_time_to_first_deployment) {
893
+ children.push(body(str(data.estimated_time_to_first_deployment)));
894
+ }
895
+ if (Array.isArray(data.prerequisites) && data.prerequisites.length) {
896
+ children.push(h2('Prerequisites'));
897
+ renderStringList(data.prerequisites, children);
898
+ }
899
+ if (Array.isArray(data.steps) && data.steps.length) {
900
+ children.push(h2('Deployment Steps'));
901
+ for (const step of data.steps) {
902
+ if (typeof step === 'string') {
903
+ children.push(bullet(step));
904
+ continue;
905
+ }
906
+ if (typeof step !== 'object' || !step)
907
+ continue;
908
+ const stepTitle = str(step.title ?? step.name ?? `Step ${step.step ?? ''}`);
909
+ children.push(h3(stepTitle));
910
+ if (step.description)
911
+ children.push(body(str(step.description)));
912
+ if (step.command) {
913
+ children.push(italicNote(str(step.command)));
914
+ }
915
+ if (step.validation) {
916
+ children.push(body(`Validation: ${str(step.validation)}`, 1));
917
+ }
918
+ }
919
+ }
920
+ if (Array.isArray(data.troubleshooting) && data.troubleshooting.length) {
921
+ children.push(h2('Troubleshooting'));
922
+ for (const t of data.troubleshooting) {
923
+ if (typeof t === 'string') {
924
+ children.push(bullet(t));
925
+ }
926
+ else if (typeof t === 'object' && t) {
927
+ children.push(h3(str(t.symptom ?? '')));
928
+ if (t.cause)
929
+ children.push(label('Cause', str(t.cause)));
930
+ if (t.resolution)
931
+ children.push(body(str(t.resolution), 1));
932
+ }
933
+ }
934
+ }
935
+ // Any remaining fields
936
+ for (const [key, value] of Object.entries(data)) {
937
+ if (['estimated_time_to_first_deployment', 'prerequisites', 'steps', 'troubleshooting'].includes(key))
938
+ continue;
939
+ children.push(h2(key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())));
940
+ if (typeof value === 'string')
941
+ children.push(body(value));
942
+ else if (Array.isArray(value))
943
+ renderStringList(value, children);
944
+ }
945
+ }
946
+ function renderEvolution(data, children) {
947
+ children.push(h1('Evolution Roadmap'));
948
+ if (data.version)
949
+ children.push(label('Current Version', str(data.version)));
950
+ if (Array.isArray(data.known_limitations) && data.known_limitations.length) {
951
+ children.push(h2('Known Limitations'));
952
+ renderStringList(data.known_limitations, children);
953
+ }
954
+ if (Array.isArray(data.roadmap) && data.roadmap.length) {
955
+ children.push(h2('Roadmap'));
956
+ for (const milestone of data.roadmap) {
957
+ if (typeof milestone !== 'object' || !milestone)
958
+ continue;
959
+ const milestoneTitle = `v${str(milestone.version)} — ${str(milestone.planned_date)}`;
960
+ children.push(h3(milestoneTitle));
961
+ if (Array.isArray(milestone.planned_changes)) {
962
+ renderStringList(milestone.planned_changes, children);
963
+ }
964
+ }
965
+ }
966
+ // Remaining fields
967
+ for (const [key, value] of Object.entries(data)) {
968
+ if (['version', 'known_limitations', 'roadmap'].includes(key))
969
+ continue;
970
+ children.push(h2(key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())));
971
+ if (typeof value === 'string')
972
+ children.push(body(value));
973
+ else if (Array.isArray(value))
974
+ renderStringList(value, children);
975
+ }
976
+ }
977
+ function renderGlossary(data, children) {
978
+ children.push(h1('Glossary'));
979
+ const items = Array.isArray(data) ? data : Object.values(data);
980
+ if (!items.length)
981
+ return;
982
+ children.push(dataTable(['Term', 'Definition'], items.map((g) => typeof g === 'object' ? [str(g.term ?? g.name ?? ''), str(g.definition ?? '')] : [str(g), ''])));
983
+ }
984
+ // Generic fallback for unknown sections
985
+ function renderGenericSection(title, data, children, schema) {
986
+ children.push(h1(title));
987
+ renderStructuredValue(data, children, schema, 2);
988
+ }
989
+ // Component catalog / classification — array of components with name/type/etc.
990
+ function renderComponentSection(title, data, children) {
991
+ children.push(h1(title));
992
+ const items = Array.isArray(data.components ?? data) ? (data.components ?? data) : [];
993
+ if (items.length && typeof items[0] === 'object') {
994
+ const headers = ['Name', 'Type', 'Technology', 'Responsibility'];
995
+ const rows = items.map((c) => [
996
+ str(c.name),
997
+ str(c.type),
998
+ str(c.technology),
999
+ str(c.responsibility),
1000
+ ]);
1001
+ children.push(dataTable(headers, rows));
1002
+ }
1003
+ if (data.mandatory_components) {
1004
+ children.push(h2('Mandatory Components'));
1005
+ renderStringList(data.mandatory_components, children);
1006
+ }
1007
+ if (data.optional_components) {
1008
+ children.push(h2('Optional Components'));
1009
+ renderStringList(data.optional_components, children);
1010
+ }
1011
+ }
1012
+ // ─── Document assembly ────────────────────────────────────────────────────────
1013
+ const KNOWN_SECTIONS = {
1014
+ reading_guide: (d, c) => renderReadingGuide(d, c),
1015
+ scope: (d, c) => renderScope(d, c),
1016
+ drivers_and_principles: (d, c) => renderDriversAndPrinciples(d, c),
1017
+ architecture_views: (d, c, imgs) => renderArchitectureViews(d, c, imgs),
1018
+ crosscutting_concerns: (d, c) => renderCrosscuttingConcerns(d, c),
1019
+ decisions: (d, c) => renderArchitectureDecisions(d, c),
1020
+ architecture_decisions: (d, c) => renderArchitectureDecisions(d, c),
1021
+ quality_attributes: (d, c) => renderQualityAttributes(d, c),
1022
+ operational_model: (d, c) => renderOperationalModel(d, c),
1023
+ implementation_guidance: (d, c) => renderImplementationGuidance(d, c),
1024
+ implementation_artifacts: (d, c) => renderImplementationGuidance(d, c),
1025
+ governance: (d, c) => renderGovernance(d, c),
1026
+ raid: (d, c) => renderRaid(d, c),
1027
+ decisions_and_actions: (d, c) => renderDecisionsAndActions(d, c),
1028
+ getting_started: (d, c) => renderGettingStarted(d, c),
1029
+ evolution: (d, c) => renderEvolution(d, c),
1030
+ glossary: (d, c) => renderGlossary(d, c),
1031
+ component_classification: (d, c) => renderComponentSection('Component Classification', d, c),
1032
+ components: (d, c) => renderComponentSection('Components', d, c),
1033
+ };
1034
+ // Logical reading order — decisions before quality/operational, RAID after governance
1035
+ const SECTION_ORDER = [
1036
+ 'reading_guide',
1037
+ 'scope',
1038
+ 'drivers_and_principles',
1039
+ 'architecture_views',
1040
+ 'crosscutting_concerns',
1041
+ 'component_classification',
1042
+ 'components',
1043
+ 'decisions',
1044
+ 'architecture_decisions',
1045
+ 'quality_attributes',
1046
+ 'operational_model',
1047
+ 'implementation_guidance',
1048
+ 'implementation_artifacts',
1049
+ 'governance',
1050
+ 'raid',
1051
+ 'decisions_and_actions',
1052
+ 'getting_started',
1053
+ 'evolution',
1054
+ 'glossary',
1055
+ ];
1056
+ function titlePage(meta) {
1057
+ const title = str(meta.title) || 'Architecture Artifact';
1058
+ const subtitle = str(meta.artifact_type ?? '')
1059
+ .replace(/_/g, ' ')
1060
+ .replace(/\b\w/g, (c) => c.toUpperCase());
1061
+ return [
1062
+ new Paragraph({ style: 'Normal', children: [], spacing: { before: 2000 } }),
1063
+ new Paragraph({
1064
+ style: 'Title',
1065
+ children: [new TextRun({ text: title, font: 'Arial', size: 56, bold: true, color: NAVY })],
1066
+ alignment: AlignmentType.CENTER,
1067
+ spacing: { before: 0, after: 200 },
1068
+ }),
1069
+ new Paragraph({
1070
+ style: 'Normal',
1071
+ children: [new TextRun({ text: subtitle, font: 'Arial', size: 28, color: MID_GREY })],
1072
+ alignment: AlignmentType.CENTER,
1073
+ spacing: { before: 0, after: 400 },
1074
+ }),
1075
+ new Paragraph({
1076
+ style: 'Normal',
1077
+ children: [
1078
+ new TextRun({ text: `Version ${str(meta.version) || '—'}`, font: 'Arial', size: 22, color: DARK_GREY }),
1079
+ ],
1080
+ alignment: AlignmentType.CENTER,
1081
+ spacing: { before: 0, after: 80 },
1082
+ }),
1083
+ new Paragraph({
1084
+ style: 'Normal',
1085
+ children: [new TextRun({ text: `Status: ${str(meta.status) || '—'}`, font: 'Arial', size: 22, color: DARK_GREY })],
1086
+ alignment: AlignmentType.CENTER,
1087
+ spacing: { before: 0, after: 80 },
1088
+ }),
1089
+ new Paragraph({
1090
+ style: 'Normal',
1091
+ children: [new TextRun({ text: `Owner: ${str(meta.owner) || '—'}`, font: 'Arial', size: 22, color: DARK_GREY })],
1092
+ alignment: AlignmentType.CENTER,
1093
+ spacing: { before: 0, after: 80 },
1094
+ }),
1095
+ new Paragraph({
1096
+ style: 'Normal',
1097
+ children: [
1098
+ new TextRun({
1099
+ text: str(meta.effective_date) ? `Effective: ${str(meta.effective_date)}` : '',
1100
+ font: 'Arial',
1101
+ size: 20,
1102
+ color: MID_GREY,
1103
+ }),
1104
+ ],
1105
+ alignment: AlignmentType.CENTER,
1106
+ }),
1107
+ pageBreak(),
1108
+ ];
1109
+ }
1110
+ // ─── Main export function ─────────────────────────────────────────────────────
1111
+ export async function exportToDocx(artifactData, browserRenderedDiagrams) {
1112
+ const meta = artifactData.metadata ?? {};
1113
+ const sections = artifactData.sections ?? {};
1114
+ const isBrowserExport = browserRenderedDiagrams !== undefined;
1115
+ // 1. Extract all diagram references
1116
+ const diagRefs = extractDiagrams(artifactData);
1117
+ // 2. Load browser-rendered diagrams first, then fall back to Kroki when needed.
1118
+ const diagramImages = new Map();
1119
+ for (const diagRef of diagRefs) {
1120
+ const browserRendered = browserRenderedDiagrams?.[diagRef.key];
1121
+ if (isBrowserRenderedDiagram(browserRendered)) {
1122
+ diagramImages.set(diagRef.key, browserRendered);
1123
+ }
1124
+ }
1125
+ const missingDiagRefs = diagRefs.filter((diagRef) => !diagramImages.has(diagRef.key));
1126
+ if (isBrowserExport && missingDiagRefs.length) {
1127
+ throw new Error(`Missing browser-rendered diagrams for export: ${missingDiagRefs.map((diagRef) => diagRef.label).join(', ')}`);
1128
+ }
1129
+ if (!isBrowserExport) {
1130
+ const BATCH = 10;
1131
+ for (let i = 0; i < missingDiagRefs.length; i += BATCH) {
1132
+ const batch = missingDiagRefs.slice(i, i + BATCH);
1133
+ const results = await Promise.all(batch.map((r) => renderMermaidDiagram(r.source, r.label)));
1134
+ batch.forEach((r, idx) => diagramImages.set(r.key, results[idx]));
1135
+ }
1136
+ }
1137
+ // 3. Build document body
1138
+ const children = [];
1139
+ // Title page
1140
+ for (const p of titlePage(meta))
1141
+ children.push(p);
1142
+ // Metadata block
1143
+ if (Object.keys(meta).length) {
1144
+ renderMetadata(meta, children);
1145
+ children.push(horizontalRule());
1146
+ }
1147
+ // Table of Contents (after metadata, before content)
1148
+ children.push(pageBreak());
1149
+ children.push(new TableOfContents('Table of Contents', {
1150
+ hyperlink: true,
1151
+ headingStyleRange: '1-3',
1152
+ }));
1153
+ children.push(pageBreak());
1154
+ // Sections — known first (in preferred order), then unknowns
1155
+ const rendered = new Set();
1156
+ for (const key of SECTION_ORDER) {
1157
+ if (key in sections) {
1158
+ const renderer = KNOWN_SECTIONS[key];
1159
+ if (renderer) {
1160
+ children.push(pageBreak());
1161
+ renderer(sections[key], children, diagramImages);
1162
+ rendered.add(key);
1163
+ }
1164
+ }
1165
+ }
1166
+ // Unknown sections (anything not in SECTION_ORDER or KNOWN_SECTIONS)
1167
+ for (const [key, value] of Object.entries(sections)) {
1168
+ if (!rendered.has(key)) {
1169
+ children.push(pageBreak());
1170
+ const renderer = KNOWN_SECTIONS[key];
1171
+ if (renderer) {
1172
+ renderer(value, children, diagramImages);
1173
+ }
1174
+ else {
1175
+ const schema = sectionSchema(key);
1176
+ const title = displayLabel(key, schema);
1177
+ renderGenericSection(title, value, children, schema);
1178
+ }
1179
+ rendered.add(key);
1180
+ }
1181
+ }
1182
+ // Top-level keys outside of sections (e.g. glossary in some artifact formats)
1183
+ const TOP_LEVEL_SKIP = new Set(['kind', 'artifact_type', 'metadata', 'sections']);
1184
+ for (const [key, value] of Object.entries(artifactData)) {
1185
+ if (TOP_LEVEL_SKIP.has(key) || rendered.has(key))
1186
+ continue;
1187
+ children.push(pageBreak());
1188
+ const renderer = KNOWN_SECTIONS[key];
1189
+ if (renderer) {
1190
+ renderer(value, children, diagramImages);
1191
+ }
1192
+ else {
1193
+ const schema = topLevelSchema(key);
1194
+ const title = displayLabel(key, schema);
1195
+ renderGenericSection(title, value, children, schema);
1196
+ }
1197
+ }
1198
+ // 4. Assemble the Word document
1199
+ const title = str(meta.title) || 'Architecture Artifact';
1200
+ const doc = new Document({
1201
+ title,
1202
+ creator: str(meta.owner) || 'EaROS',
1203
+ description: str(meta.purpose),
1204
+ styles: {
1205
+ default: {
1206
+ document: {
1207
+ run: { font: 'Arial', size: 20 },
1208
+ },
1209
+ },
1210
+ },
1211
+ sections: [
1212
+ {
1213
+ properties: {
1214
+ type: SectionType.CONTINUOUS,
1215
+ page: {
1216
+ margin: { top: 720, right: 720, bottom: 720, left: 720 },
1217
+ },
1218
+ },
1219
+ headers: {
1220
+ default: new Header({
1221
+ children: [
1222
+ new Paragraph({
1223
+ style: 'Normal',
1224
+ children: [
1225
+ new TextRun({ text: title, font: 'Arial', size: 16, color: MID_GREY }),
1226
+ new TextRun({ text: '\t', font: 'Arial', size: 16 }),
1227
+ new TextRun({ text: 'EaROS Architecture Document', font: 'Arial', size: 16, color: MID_GREY }),
1228
+ ],
1229
+ border: { bottom: { color: 'CCCCCC', space: 1, style: BorderStyle.SINGLE, size: 4 } },
1230
+ }),
1231
+ ],
1232
+ }),
1233
+ },
1234
+ footers: {
1235
+ default: new Footer({
1236
+ children: [
1237
+ new Paragraph({
1238
+ style: 'Normal',
1239
+ alignment: AlignmentType.CENTER,
1240
+ children: [
1241
+ new TextRun({ text: 'Page ', font: 'Arial', size: 16, color: MID_GREY }),
1242
+ new SimpleField('PAGE'),
1243
+ new TextRun({ text: ' of ', font: 'Arial', size: 16, color: MID_GREY }),
1244
+ new SimpleField('NUMPAGES'),
1245
+ ],
1246
+ border: { top: { color: 'CCCCCC', space: 1, style: BorderStyle.SINGLE, size: 4 } },
1247
+ }),
1248
+ ],
1249
+ }),
1250
+ },
1251
+ children,
1252
+ },
1253
+ ],
1254
+ });
1255
+ return Packer.toBuffer(doc);
1256
+ }
1257
+ // ─── Rubric Word Export ──────────────────────────────────────────────────────
1258
+ function rubricTitlePage(data) {
1259
+ const title = str(data.title) || 'EaROS Rubric';
1260
+ const kind = str(data.kind ?? '').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
1261
+ return [
1262
+ new Paragraph({ style: 'Normal', children: [], spacing: { before: 2000 } }),
1263
+ new Paragraph({
1264
+ style: 'Title',
1265
+ children: [new TextRun({ text: title, font: 'Arial', size: 56, bold: true, color: NAVY })],
1266
+ alignment: AlignmentType.CENTER,
1267
+ spacing: { before: 0, after: 200 },
1268
+ }),
1269
+ new Paragraph({
1270
+ style: 'Normal',
1271
+ children: [new TextRun({ text: kind, font: 'Arial', size: 28, color: MID_GREY })],
1272
+ alignment: AlignmentType.CENTER,
1273
+ spacing: { before: 0, after: 400 },
1274
+ }),
1275
+ new Paragraph({
1276
+ style: 'Normal',
1277
+ children: [
1278
+ new TextRun({ text: `${str(data.rubric_id)} · v${str(data.version) || '—'}`, font: 'Arial', size: 22, color: DARK_GREY }),
1279
+ ],
1280
+ alignment: AlignmentType.CENTER,
1281
+ spacing: { before: 0, after: 80 },
1282
+ }),
1283
+ ];
1284
+ }
1285
+ export async function exportRubricToDocx(rubricData) {
1286
+ const children = [];
1287
+ // Title page
1288
+ for (const p of rubricTitlePage(rubricData))
1289
+ children.push(p);
1290
+ // Metadata
1291
+ const metaPairs = [];
1292
+ if (rubricData.rubric_id)
1293
+ metaPairs.push(['Rubric ID', str(rubricData.rubric_id)]);
1294
+ if (rubricData.version)
1295
+ metaPairs.push(['Version', str(rubricData.version)]);
1296
+ if (rubricData.kind)
1297
+ metaPairs.push(['Kind', str(rubricData.kind)]);
1298
+ if (rubricData.status)
1299
+ metaPairs.push(['Status', str(rubricData.status)]);
1300
+ if (rubricData.artifact_type)
1301
+ metaPairs.push(['Artifact Type', str(rubricData.artifact_type)]);
1302
+ if (rubricData.design_method)
1303
+ metaPairs.push(['Design Method', str(rubricData.design_method)]);
1304
+ if (rubricData.owner)
1305
+ metaPairs.push(['Owner', str(rubricData.owner)]);
1306
+ if (rubricData.effective_date)
1307
+ metaPairs.push(['Effective Date', str(rubricData.effective_date)]);
1308
+ if (rubricData.inherits?.length)
1309
+ metaPairs.push(['Inherits', rubricData.inherits.join(', ')]);
1310
+ if (metaPairs.length) {
1311
+ children.push(kvTable(metaPairs));
1312
+ children.push(horizontalRule());
1313
+ }
1314
+ // Table of Contents
1315
+ children.push(pageBreak());
1316
+ children.push(new TableOfContents('Table of Contents', { hyperlink: true, headingStyleRange: '1-3' }));
1317
+ // Dimensions & criteria
1318
+ const dimensions = rubricData.dimensions ?? [];
1319
+ for (const dim of dimensions) {
1320
+ children.push(pageBreak());
1321
+ const weight = dim.weight != null ? ` (weight: ${dim.weight})` : '';
1322
+ children.push(h1(`${str(dim.name)}${weight}`));
1323
+ if (dim.description)
1324
+ children.push(italicNote(str(dim.description)));
1325
+ const criteria = dim.criteria ?? [];
1326
+ for (const c of criteria) {
1327
+ children.push(h2(`${str(c.id)}: ${str(c.question)}`));
1328
+ if (c.description)
1329
+ children.push(body(str(c.description)));
1330
+ // Gate
1331
+ if (c.gate && typeof c.gate === 'object' && c.gate.enabled) {
1332
+ const gateText = `${c.gate.severity ?? 'unknown'}${c.gate.failure_effect ? ` — ${c.gate.failure_effect}` : ''}`;
1333
+ children.push(label('Gate', gateText));
1334
+ }
1335
+ else {
1336
+ children.push(label('Gate', 'None'));
1337
+ }
1338
+ // Required evidence
1339
+ if (c.required_evidence?.length) {
1340
+ children.push(h3('Required Evidence'));
1341
+ for (const ev of c.required_evidence)
1342
+ children.push(bullet(str(ev)));
1343
+ }
1344
+ // Scoring guide
1345
+ if (c.scoring_guide) {
1346
+ children.push(h3('Scoring Guide'));
1347
+ const sgRows = Object.entries(c.scoring_guide).map(([level, desc]) => [level, str(desc)]);
1348
+ children.push(dataTable(['Level', 'Description'], sgRows));
1349
+ }
1350
+ // Anti-patterns
1351
+ if (c.anti_patterns?.length) {
1352
+ children.push(h3('Anti-patterns'));
1353
+ for (const ap of c.anti_patterns)
1354
+ children.push(bullet(str(ap)));
1355
+ }
1356
+ // Examples
1357
+ if (c.examples?.good?.length) {
1358
+ children.push(h3('Good Examples'));
1359
+ for (const ex of c.examples.good)
1360
+ children.push(bullet(str(ex)));
1361
+ }
1362
+ if (c.examples?.bad?.length) {
1363
+ children.push(h3('Bad Examples'));
1364
+ for (const ex of c.examples.bad)
1365
+ children.push(bullet(str(ex)));
1366
+ }
1367
+ // Decision tree
1368
+ if (c.decision_tree) {
1369
+ children.push(h3('Decision Tree'));
1370
+ children.push(body(str(c.decision_tree), 1));
1371
+ }
1372
+ // Remediation hints
1373
+ if (c.remediation_hints?.length) {
1374
+ children.push(h3('Remediation Hints'));
1375
+ for (const hint of c.remediation_hints)
1376
+ children.push(bullet(str(hint)));
1377
+ }
1378
+ children.push(horizontalRule());
1379
+ }
1380
+ }
1381
+ // Scoring section
1382
+ if (rubricData.scoring) {
1383
+ children.push(pageBreak());
1384
+ children.push(h1('Scoring Model'));
1385
+ const scoring = rubricData.scoring;
1386
+ if (scoring.scale)
1387
+ children.push(label('Scale', str(scoring.scale)));
1388
+ if (scoring.method)
1389
+ children.push(label('Method', str(scoring.method)));
1390
+ if (scoring.thresholds) {
1391
+ children.push(h2('Thresholds'));
1392
+ const thPairs = Object.entries(scoring.thresholds).map(([k, v]) => [prettyLabel(k), str(v)]);
1393
+ children.push(kvTable(thPairs));
1394
+ }
1395
+ }
1396
+ // Outputs
1397
+ if (rubricData.outputs) {
1398
+ children.push(pageBreak());
1399
+ children.push(h1('Outputs'));
1400
+ const outPairs = Object.entries(rubricData.outputs).map(([k, v]) => [prettyLabel(k), str(v)]);
1401
+ children.push(kvTable(outPairs));
1402
+ }
1403
+ const title = str(rubricData.title) || 'EaROS Rubric';
1404
+ const doc = new Document({
1405
+ title,
1406
+ creator: str(rubricData.owner) || 'EaROS',
1407
+ styles: { default: { document: { run: { font: 'Arial', size: 20 } } } },
1408
+ sections: [{
1409
+ properties: {
1410
+ type: SectionType.CONTINUOUS,
1411
+ page: { margin: { top: 720, right: 720, bottom: 720, left: 720 } },
1412
+ },
1413
+ headers: {
1414
+ default: new Header({
1415
+ children: [new Paragraph({
1416
+ style: 'Normal',
1417
+ children: [
1418
+ new TextRun({ text: title, font: 'Arial', size: 16, color: MID_GREY }),
1419
+ new TextRun({ text: '\t', font: 'Arial', size: 16 }),
1420
+ new TextRun({ text: 'EaROS Rubric Document', font: 'Arial', size: 16, color: MID_GREY }),
1421
+ ],
1422
+ border: { bottom: { color: 'CCCCCC', space: 1, style: BorderStyle.SINGLE, size: 4 } },
1423
+ })],
1424
+ }),
1425
+ },
1426
+ footers: {
1427
+ default: new Footer({
1428
+ children: [new Paragraph({
1429
+ style: 'Normal',
1430
+ alignment: AlignmentType.CENTER,
1431
+ children: [
1432
+ new TextRun({ text: 'Page ', font: 'Arial', size: 16, color: MID_GREY }),
1433
+ new SimpleField('PAGE'),
1434
+ new TextRun({ text: ' of ', font: 'Arial', size: 16, color: MID_GREY }),
1435
+ new SimpleField('NUMPAGES'),
1436
+ ],
1437
+ border: { top: { color: 'CCCCCC', space: 1, style: BorderStyle.SINGLE, size: 4 } },
1438
+ })],
1439
+ }),
1440
+ },
1441
+ children,
1442
+ }],
1443
+ });
1444
+ return Packer.toBuffer(doc);
1445
+ }
1446
+ // ─── Evaluation Word Export ──────────────────────────────────────────────────
1447
+ export async function exportEvaluationToDocx(evalData) {
1448
+ const children = [];
1449
+ const artifactTitle = str(evalData.artifact_ref?.title ?? evalData.artifact_title ?? 'Architecture Assessment');
1450
+ const rubricId = str(evalData.rubric_id ?? '');
1451
+ const evalDate = str(evalData.evaluation_date ?? '');
1452
+ // Title page
1453
+ children.push(new Paragraph({ style: 'Normal', children: [], spacing: { before: 2000 } }));
1454
+ children.push(new Paragraph({
1455
+ style: 'Title',
1456
+ children: [new TextRun({ text: 'EaROS Architecture Assessment Report', font: 'Arial', size: 56, bold: true, color: NAVY })],
1457
+ alignment: AlignmentType.CENTER,
1458
+ spacing: { before: 0, after: 200 },
1459
+ }));
1460
+ children.push(new Paragraph({
1461
+ style: 'Normal',
1462
+ children: [new TextRun({ text: artifactTitle, font: 'Arial', size: 28, color: MID_GREY })],
1463
+ alignment: AlignmentType.CENTER,
1464
+ spacing: { before: 0, after: 400 },
1465
+ }));
1466
+ if (rubricId || evalDate) {
1467
+ children.push(new Paragraph({
1468
+ style: 'Normal',
1469
+ children: [new TextRun({ text: `${rubricId}${evalDate ? ` · ${evalDate}` : ''}`, font: 'Arial', size: 22, color: DARK_GREY })],
1470
+ alignment: AlignmentType.CENTER,
1471
+ }));
1472
+ }
1473
+ // Metadata
1474
+ const metaPairs = [];
1475
+ if (evalData.evaluation_id)
1476
+ metaPairs.push(['Evaluation ID', str(evalData.evaluation_id)]);
1477
+ if (rubricId)
1478
+ metaPairs.push(['Rubric', rubricId]);
1479
+ if (evalData.rubric_version)
1480
+ metaPairs.push(['Rubric Version', str(evalData.rubric_version)]);
1481
+ if (artifactTitle)
1482
+ metaPairs.push(['Artifact', artifactTitle]);
1483
+ if (evalData.artifact_ref?.artifact_version)
1484
+ metaPairs.push(['Artifact Version', str(evalData.artifact_ref.artifact_version)]);
1485
+ if (evalDate)
1486
+ metaPairs.push(['Evaluation Date', evalDate]);
1487
+ if (evalData.evaluators?.length) {
1488
+ metaPairs.push(['Evaluators', evalData.evaluators.map((e) => `${str(e.role)} (${str(e.mode)})`).join(', ')]);
1489
+ }
1490
+ if (metaPairs.length) {
1491
+ children.push(kvTable(metaPairs));
1492
+ children.push(horizontalRule());
1493
+ }
1494
+ // Table of Contents
1495
+ children.push(pageBreak());
1496
+ children.push(new TableOfContents('Table of Contents', { hyperlink: true, headingStyleRange: '1-3' }));
1497
+ // Criterion results
1498
+ const criterionResults = evalData.criterion_results ?? [];
1499
+ if (criterionResults.length > 0) {
1500
+ children.push(pageBreak());
1501
+ children.push(h1('Score Dashboard'));
1502
+ const dashHeaders = ['Criterion', 'Score', 'Confidence', 'Evidence Class'];
1503
+ const dashRows = criterionResults.map((cr) => [
1504
+ str(cr.criterion_id),
1505
+ cr.score != null ? String(cr.score) : '-',
1506
+ str(cr.confidence),
1507
+ str(cr.evidence_class ?? cr.judgment_type),
1508
+ ]);
1509
+ children.push(dataTable(dashHeaders, dashRows));
1510
+ // Per-criterion details
1511
+ children.push(pageBreak());
1512
+ children.push(h1('Criterion Details'));
1513
+ for (const cr of criterionResults) {
1514
+ children.push(h2(str(cr.criterion_id)));
1515
+ children.push(label('Score', cr.score != null ? String(cr.score) : '-'));
1516
+ if (cr.confidence)
1517
+ children.push(label('Confidence', str(cr.confidence)));
1518
+ const evClass = cr.evidence_class ?? cr.judgment_type;
1519
+ if (evClass)
1520
+ children.push(label('Evidence Class', str(evClass)));
1521
+ if (cr.rationale)
1522
+ children.push(body(str(cr.rationale)));
1523
+ if (cr.evidence_refs?.length) {
1524
+ children.push(h3('Evidence'));
1525
+ for (const ref of cr.evidence_refs) {
1526
+ if (typeof ref === 'string') {
1527
+ children.push(bullet(str(ref)));
1528
+ continue;
1529
+ }
1530
+ const quote = ref.quotation ?? ref.excerpt;
1531
+ const source = ref.section ?? ref.location;
1532
+ if (quote)
1533
+ children.push(bullet(str(quote)));
1534
+ if (source && source !== 'see evidence field')
1535
+ children.push(italicNote(`Location: ${str(source)}`));
1536
+ }
1537
+ }
1538
+ children.push(horizontalRule());
1539
+ }
1540
+ }
1541
+ const title = `EaROS Assessment — ${artifactTitle}`;
1542
+ const doc = new Document({
1543
+ title,
1544
+ creator: 'EaROS',
1545
+ styles: { default: { document: { run: { font: 'Arial', size: 20 } } } },
1546
+ sections: [{
1547
+ properties: {
1548
+ type: SectionType.CONTINUOUS,
1549
+ page: { margin: { top: 720, right: 720, bottom: 720, left: 720 } },
1550
+ },
1551
+ headers: {
1552
+ default: new Header({
1553
+ children: [new Paragraph({
1554
+ style: 'Normal',
1555
+ children: [
1556
+ new TextRun({ text: artifactTitle, font: 'Arial', size: 16, color: MID_GREY }),
1557
+ new TextRun({ text: '\t', font: 'Arial', size: 16 }),
1558
+ new TextRun({ text: 'EaROS Assessment Report', font: 'Arial', size: 16, color: MID_GREY }),
1559
+ ],
1560
+ border: { bottom: { color: 'CCCCCC', space: 1, style: BorderStyle.SINGLE, size: 4 } },
1561
+ })],
1562
+ }),
1563
+ },
1564
+ footers: {
1565
+ default: new Footer({
1566
+ children: [new Paragraph({
1567
+ style: 'Normal',
1568
+ alignment: AlignmentType.CENTER,
1569
+ children: [
1570
+ new TextRun({ text: 'Page ', font: 'Arial', size: 16, color: MID_GREY }),
1571
+ new SimpleField('PAGE'),
1572
+ new TextRun({ text: ' of ', font: 'Arial', size: 16, color: MID_GREY }),
1573
+ new SimpleField('NUMPAGES'),
1574
+ ],
1575
+ border: { top: { color: 'CCCCCC', space: 1, style: BorderStyle.SINGLE, size: 4 } },
1576
+ })],
1577
+ }),
1578
+ },
1579
+ children,
1580
+ }],
1581
+ });
1582
+ return Packer.toBuffer(doc);
1583
+ }