@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.
- package/README.md +156 -0
- package/assets/init/.agents/skills/earos-artifact-gen/SKILL.md +106 -0
- package/assets/init/.agents/skills/earos-artifact-gen/references/interview-guide.md +313 -0
- package/assets/init/.agents/skills/earos-artifact-gen/references/output-guide.md +367 -0
- package/assets/init/.agents/skills/earos-assess/SKILL.md +212 -0
- package/assets/init/.agents/skills/earos-assess/references/calibration-benchmarks.md +160 -0
- package/assets/init/.agents/skills/earos-assess/references/output-templates.md +311 -0
- package/assets/init/.agents/skills/earos-assess/references/scoring-protocol.md +281 -0
- package/assets/init/.agents/skills/earos-calibrate/SKILL.md +153 -0
- package/assets/init/.agents/skills/earos-calibrate/references/agreement-metrics.md +188 -0
- package/assets/init/.agents/skills/earos-calibrate/references/calibration-protocol.md +263 -0
- package/assets/init/.agents/skills/earos-create/SKILL.md +257 -0
- package/assets/init/.agents/skills/earos-create/references/criterion-writing-guide.md +268 -0
- package/assets/init/.agents/skills/earos-create/references/dependency-rules.md +193 -0
- package/assets/init/.agents/skills/earos-create/references/rubric-interview-guide.md +123 -0
- package/assets/init/.agents/skills/earos-create/references/validation-checklist.md +238 -0
- package/assets/init/.agents/skills/earos-profile-author/SKILL.md +251 -0
- package/assets/init/.agents/skills/earos-profile-author/references/criterion-writing-guide.md +280 -0
- package/assets/init/.agents/skills/earos-profile-author/references/design-methods.md +158 -0
- package/assets/init/.agents/skills/earos-profile-author/references/profile-checklist.md +173 -0
- package/assets/init/.agents/skills/earos-remediate/SKILL.md +118 -0
- package/assets/init/.agents/skills/earos-remediate/references/output-template.md +199 -0
- package/assets/init/.agents/skills/earos-remediate/references/remediation-patterns.md +330 -0
- package/assets/init/.agents/skills/earos-report/SKILL.md +85 -0
- package/assets/init/.agents/skills/earos-report/references/portfolio-template.md +181 -0
- package/assets/init/.agents/skills/earos-report/references/single-artifact-template.md +168 -0
- package/assets/init/.agents/skills/earos-review/SKILL.md +130 -0
- package/assets/init/.agents/skills/earos-review/references/challenge-patterns.md +163 -0
- package/assets/init/.agents/skills/earos-review/references/output-template.md +180 -0
- package/assets/init/.agents/skills/earos-template-fill/SKILL.md +177 -0
- package/assets/init/.agents/skills/earos-template-fill/references/evidence-writing-guide.md +186 -0
- package/assets/init/.agents/skills/earos-template-fill/references/section-rubric-mapping.md +200 -0
- package/assets/init/.agents/skills/earos-validate/SKILL.md +113 -0
- package/assets/init/.agents/skills/earos-validate/references/fix-patterns.md +281 -0
- package/assets/init/.agents/skills/earos-validate/references/validation-checks.md +287 -0
- package/assets/init/.claude/CLAUDE.md +4 -0
- package/assets/init/AGENTS.md +293 -0
- package/assets/init/CLAUDE.md +635 -0
- package/assets/init/README.md +507 -0
- package/assets/init/calibration/gold-set/.gitkeep +0 -0
- package/assets/init/calibration/results/.gitkeep +0 -0
- package/assets/init/core/core-meta-rubric.yaml +643 -0
- package/assets/init/docs/consistency-report.md +325 -0
- package/assets/init/docs/getting-started.md +194 -0
- package/assets/init/docs/profile-authoring-guide.md +51 -0
- package/assets/init/docs/terminology.md +126 -0
- package/assets/init/earos.manifest.yaml +104 -0
- package/assets/init/evaluations/.gitkeep +0 -0
- package/assets/init/examples/aws-event-driven-order-processing/artifact.yaml +2056 -0
- package/assets/init/examples/aws-event-driven-order-processing/evaluation.yaml +973 -0
- package/assets/init/examples/aws-event-driven-order-processing/report.md +244 -0
- package/assets/init/examples/example-solution-architecture.evaluation.yaml +136 -0
- package/assets/init/examples/multi-cloud-data-analytics/artifact.yaml +715 -0
- package/assets/init/overlays/data-governance.yaml +94 -0
- package/assets/init/overlays/regulatory.yaml +154 -0
- package/assets/init/overlays/security.yaml +92 -0
- package/assets/init/profiles/adr.yaml +225 -0
- package/assets/init/profiles/capability-map.yaml +223 -0
- package/assets/init/profiles/reference-architecture.yaml +426 -0
- package/assets/init/profiles/roadmap.yaml +205 -0
- package/assets/init/profiles/solution-architecture.yaml +227 -0
- package/assets/init/research/architecture-assessment-rubrics-research.docx +0 -0
- package/assets/init/research/architecture-assessment-rubrics-research.md +566 -0
- package/assets/init/research/reference-architecture-research.md +751 -0
- package/assets/init/standard/EAROS.md +1426 -0
- package/assets/init/standard/schemas/artifact.schema.json +1295 -0
- package/assets/init/standard/schemas/artifact.uischema.json +65 -0
- package/assets/init/standard/schemas/evaluation.schema.json +284 -0
- package/assets/init/standard/schemas/rubric.schema.json +383 -0
- package/assets/init/templates/evaluation-record.template.yaml +58 -0
- package/assets/init/templates/new-profile.template.yaml +65 -0
- package/bin.js +188 -0
- package/dist/assets/_basePickBy-BVu6YmSW.js +1 -0
- package/dist/assets/_baseUniq-CWRzQDz_.js +1 -0
- package/dist/assets/arc-CyDBhtDM.js +1 -0
- package/dist/assets/architectureDiagram-2XIMDMQ5-BH6O4dvN.js +36 -0
- package/dist/assets/blockDiagram-WCTKOSBZ-2xmwdjpg.js +132 -0
- package/dist/assets/c4Diagram-IC4MRINW-BNmPRFJF.js +10 -0
- package/dist/assets/channel-CiySTNoJ.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-DGQTvirp.js +1 -0
- package/dist/assets/chunk-55IACEB6-DNMAQAC_.js +1 -0
- package/dist/assets/chunk-FMBD7UC4-BJbVTQ5o.js +15 -0
- package/dist/assets/chunk-JSJVCQXG-BCxUL74A.js +1 -0
- package/dist/assets/chunk-KX2RTZJC-H7wWZOfz.js +1 -0
- package/dist/assets/chunk-NQ4KR5QH-BK4RlTQF.js +220 -0
- package/dist/assets/chunk-QZHKN3VN-0chxDV5g.js +1 -0
- package/dist/assets/chunk-WL4C6EOR-DexfQ-AV.js +189 -0
- package/dist/assets/classDiagram-VBA2DB6C-D7luWJQn.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-D7luWJQn.js +1 -0
- package/dist/assets/clone-ylgRbd3D.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-DS2IOCfZ.js +1 -0
- package/dist/assets/cytoscape.esm-CyJtwmzi.js +331 -0
- package/dist/assets/dagre-KLK3FWXG-BbSoTTa3.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-E7M64L7V-C9TvYgv0.js +24 -0
- package/dist/assets/diagram-IFDJBPK2-DowUMWrg.js +43 -0
- package/dist/assets/diagram-P4PSJMXO-BL6nrnQF.js +24 -0
- package/dist/assets/erDiagram-INFDFZHY-rXPRl8VM.js +70 -0
- package/dist/assets/flowDiagram-PKNHOUZH-DBRM99-W.js +162 -0
- package/dist/assets/ganttDiagram-A5KZAMGK-INcWFsBT.js +292 -0
- package/dist/assets/gitGraphDiagram-K3NZZRJ6-DMwpfE91.js +65 -0
- package/dist/assets/graph-DLQn37b-.js +1 -0
- package/dist/assets/index-BFFITMT8.js +650 -0
- package/dist/assets/index-H7f6VTz1.css +1 -0
- package/dist/assets/infoDiagram-LFFYTUFH-B0f4TWRM.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-PHBUUO56-CsU6XimZ.js +70 -0
- package/dist/assets/journeyDiagram-4ABVD52K-CQ7ibNib.js +139 -0
- package/dist/assets/kanban-definition-K7BYSVSG-DzEN7THt.js +89 -0
- package/dist/assets/katex-B1X10hvy.js +261 -0
- package/dist/assets/layout-C0dvb42R.js +1 -0
- package/dist/assets/linear-j4a8mGj7.js +1 -0
- package/dist/assets/mindmap-definition-YRQLILUH-DP8iEuCf.js +68 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-SKSYHLDU-BpIAXgAm.js +30 -0
- package/dist/assets/quadrantDiagram-337W2JSQ-DrpXn5Eg.js +7 -0
- package/dist/assets/requirementDiagram-Z7DCOOCP-Bg7EwHlG.js +73 -0
- package/dist/assets/sankeyDiagram-WA2Y5GQK-BWagRs1F.js +10 -0
- package/dist/assets/sequenceDiagram-2WXFIKYE-q5jwhivG.js +145 -0
- package/dist/assets/stateDiagram-RAJIS63D-B_J9pE-2.js +1 -0
- package/dist/assets/stateDiagram-v2-FVOUBMTO-Q_1GcybB.js +1 -0
- package/dist/assets/timeline-definition-YZTLITO2-dv0jgQ0z.js +61 -0
- package/dist/assets/treemap-KZPCXAKY-Dt1dkIE7.js +162 -0
- package/dist/assets/vennDiagram-LZ73GAT5-BdO5RgRZ.js +34 -0
- package/dist/assets/xychartDiagram-JWTSCODW-CpDVe-8v.js +7 -0
- package/dist/index.html +23 -0
- package/export-docx.js +1583 -0
- package/init.js +353 -0
- package/manifest-cli.mjs +207 -0
- package/package.json +83 -0
- package/schemas/artifact.schema.json +1295 -0
- package/schemas/artifact.uischema.json +65 -0
- package/schemas/evaluation.schema.json +284 -0
- package/schemas/rubric.schema.json +383 -0
- 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
|
+
}
|