@monster-orange/den 0.0.0-stage → 0.1.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 (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -2
  4. package/bin/den +61 -0
  5. package/docs/distributions.md +256 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +104 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +406 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +108 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +729 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/applications.ts +138 -0
  91. package/src/shared/auth/api-token.ts +416 -0
  92. package/src/shared/auth/auth0-helper.ts +284 -0
  93. package/src/shared/auth/headless-login.ts +265 -0
  94. package/src/shared/auth/jwt-helper.ts +186 -0
  95. package/src/shared/auth/sudo-vault.ts +69 -0
  96. package/src/shared/auth/test-accounts.ts +98 -0
  97. package/src/shared/auth/token-injection.ts +184 -0
  98. package/src/shared/auth/token-providers/auth0.ts +80 -0
  99. package/src/shared/auth/token-providers/index.ts +182 -0
  100. package/src/shared/auth/token-refresh.ts +273 -0
  101. package/src/shared/banner.ts +15 -0
  102. package/src/shared/brand.ts +79 -0
  103. package/src/shared/cli-tabs.ts +131 -0
  104. package/src/shared/constants.ts +128 -0
  105. package/src/shared/den-config.ts +566 -0
  106. package/src/shared/dev-metrics/emit.ts +354 -0
  107. package/src/shared/edge/config.ts +229 -0
  108. package/src/shared/edge/render.ts +221 -0
  109. package/src/shared/edge/runtime.ts +268 -0
  110. package/src/shared/edge/types.ts +62 -0
  111. package/src/shared/engine.ts +67 -0
  112. package/src/shared/environment/aws-profiles.ts +49 -0
  113. package/src/shared/environment/env-selector.ts +161 -0
  114. package/src/shared/environment/playwright-env.ts +82 -0
  115. package/src/shared/environment-manager/catalog.ts +77 -0
  116. package/src/shared/environment-manager/clone-guard.ts +163 -0
  117. package/src/shared/environment-manager/dag.ts +286 -0
  118. package/src/shared/environment-manager/default-workspace.ts +182 -0
  119. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  120. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  121. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  122. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  123. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  124. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  125. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  126. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  127. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  128. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  129. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  130. package/src/shared/environment-manager/derivation/text.ts +40 -0
  131. package/src/shared/environment-manager/derivation/types.ts +69 -0
  132. package/src/shared/environment-manager/health.ts +589 -0
  133. package/src/shared/environment-manager/index.ts +6 -0
  134. package/src/shared/environment-manager/load-applications.ts +78 -0
  135. package/src/shared/environment-manager/log-tail.ts +212 -0
  136. package/src/shared/environment-manager/make-targets.ts +78 -0
  137. package/src/shared/environment-manager/opctl.ts +334 -0
  138. package/src/shared/environment-manager/readiness.ts +93 -0
  139. package/src/shared/environment-manager/register-local.ts +72 -0
  140. package/src/shared/environment-manager/secrets-check.ts +260 -0
  141. package/src/shared/environment-manager/service-env.ts +110 -0
  142. package/src/shared/environment-manager/service-runner.ts +1588 -0
  143. package/src/shared/environment-manager/up-plan.ts +302 -0
  144. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  145. package/src/shared/environment-manager/workspaces.ts +860 -0
  146. package/src/shared/manifest/deps.ts +27 -0
  147. package/src/shared/manifest/migrate-manifest.ts +101 -0
  148. package/src/shared/manifest/read-manifest.ts +160 -0
  149. package/src/shared/manifest/schema.ts +299 -0
  150. package/src/shared/manifest/service-links.ts +93 -0
  151. package/src/shared/manifest/validate-manifest.ts +140 -0
  152. package/src/shared/manifest/workspace-manifests.ts +92 -0
  153. package/src/shared/open-url.ts +122 -0
  154. package/src/shared/org.ts +44 -0
  155. package/src/shared/paths.ts +178 -0
  156. package/src/shared/perf/optional-fields.ts +105 -0
  157. package/src/shared/perf/report-normalizer.ts +404 -0
  158. package/src/shared/perf/report-paths.ts +19 -0
  159. package/src/shared/perf/report-store.ts +110 -0
  160. package/src/shared/perf/report-types.ts +70 -0
  161. package/src/shared/perf/test-paths.ts +25 -0
  162. package/src/shared/process/log-rotation.ts +147 -0
  163. package/src/shared/process/spawn-streaming.ts +229 -0
  164. package/src/shared/product.ts +92 -0
  165. package/src/shared/profile.ts +141 -0
  166. package/src/shared/repo-kind.ts +32 -0
  167. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  168. package/src/shared/reporting/slack-reporter.ts +340 -0
  169. package/src/shared/results/result-reader.ts +266 -0
  170. package/src/shared/results/result-writer.ts +168 -0
  171. package/src/shared/secrets/doppler.ts +126 -0
  172. package/src/shared/secrets/env.ts +48 -0
  173. package/src/shared/secrets/index.ts +197 -0
  174. package/src/shared/secrets/keys.ts +33 -0
  175. package/src/shared/secrets/onepassword.ts +75 -0
  176. package/src/shared/secrets/types.ts +52 -0
  177. package/src/shared/service-url.ts +119 -0
  178. package/src/shared/smoke/branch-resolver.ts +141 -0
  179. package/src/shared/smoke/git-remote.ts +70 -0
  180. package/src/shared/smoke/repo-cloner.ts +151 -0
  181. package/src/shared/stacks/config.ts +137 -0
  182. package/src/shared/stacks/manifest-commands.ts +88 -0
  183. package/src/shared/stacks/render.ts +253 -0
  184. package/src/shared/stacks/runtime.ts +334 -0
  185. package/src/shared/stacks/types.ts +53 -0
  186. package/src/shared/terminal/tabs.ts +212 -0
  187. package/src/shared/test-discovery/module-scanner.ts +569 -0
  188. package/src/shared/types/index.ts +678 -0
  189. package/src/shared/ui-marker.ts +19 -0
  190. package/src/ui/cli/applications-command.ts +116 -0
  191. package/src/ui/cli/config-command.ts +68 -0
  192. package/src/ui/cli/create-main.ts +9 -0
  193. package/src/ui/cli/create.ts +360 -0
  194. package/src/ui/cli/edge-command.ts +91 -0
  195. package/src/ui/cli/flag-parser.ts +246 -0
  196. package/src/ui/cli/graph-command.ts +524 -0
  197. package/src/ui/cli/health-display.ts +63 -0
  198. package/src/ui/cli/index.ts +3656 -0
  199. package/src/ui/cli/init.ts +855 -0
  200. package/src/ui/cli/invocation-target.ts +175 -0
  201. package/src/ui/cli/logs-command.ts +104 -0
  202. package/src/ui/cli/module-selection.ts +31 -0
  203. package/src/ui/cli/prompts.ts +767 -0
  204. package/src/ui/cli/results-viewer.ts +436 -0
  205. package/src/ui/cli/secrets-preflight.ts +91 -0
  206. package/src/ui/cli/stack-command.ts +144 -0
  207. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  208. package/src/ui/cli/test-runner.ts +1312 -0
  209. package/src/ui/cli/web-command.ts +1073 -0
  210. package/src/ui/server/changelog-parser.ts +94 -0
  211. package/src/ui/server/daemon-reaper.ts +129 -0
  212. package/src/ui/server/editor-manager.ts +301 -0
  213. package/src/ui/server/github-service.ts +223 -0
  214. package/src/ui/server/http-helpers.ts +98 -0
  215. package/src/ui/server/index.ts +3236 -0
  216. package/src/ui/server/log-history.ts +452 -0
  217. package/src/ui/server/perf-report.ts +42 -0
  218. package/src/ui/server/public/environments.html +494 -0
  219. package/src/ui/server/public/favicon.ico +0 -0
  220. package/src/ui/server/public/header.js +1371 -0
  221. package/src/ui/server/public/index.html +2595 -0
  222. package/src/ui/server/public/local-dev.html +7405 -0
  223. package/src/ui/server/public/logs.html +1489 -0
  224. package/src/ui/server/public/perf-report.js +359 -0
  225. package/src/ui/server/public/release.html +2111 -0
  226. package/src/ui/server/public/services.html +1098 -0
  227. package/src/ui/server/public/workspace-picker.js +69 -0
  228. package/src/ui/server/release-deploy-session.ts +246 -0
  229. package/src/ui/server/release-service.ts +1125 -0
  230. package/src/ui/server/sudo-store.ts +111 -0
  231. package/src/ui/server/workspace-routes.ts +2154 -0
@@ -0,0 +1,359 @@
1
+ function escapeHtml(value) {
2
+ return String(value)
3
+ .replaceAll("&", "&")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&#39;");
8
+ }
9
+
10
+ const METRIC_VALUE_ORDER = [
11
+ "avg",
12
+ "med",
13
+ "p(50)",
14
+ "p(90)",
15
+ "p(95)",
16
+ "p(99)",
17
+ "min",
18
+ "max",
19
+ "rate",
20
+ "count",
21
+ "value",
22
+ "passes",
23
+ "fails",
24
+ ];
25
+
26
+ function formatNumber(value) {
27
+ if (typeof value !== "number" || !Number.isFinite(value)) {
28
+ return String(value);
29
+ }
30
+
31
+ if (Number.isInteger(value)) {
32
+ return String(value);
33
+ }
34
+
35
+ const abs = Math.abs(value);
36
+ if (abs >= 100) {
37
+ return value.toFixed(2).replace(/\.?0+$/, "");
38
+ }
39
+
40
+ if (abs >= 1) {
41
+ return value.toFixed(2).replace(/\.?0+$/, "");
42
+ }
43
+
44
+ return value.toFixed(4).replace(/\.?0+$/, "");
45
+ }
46
+
47
+ function isRateLikeLabel(label) {
48
+ return /rate|failure/i.test(label ?? "");
49
+ }
50
+
51
+ function isRateLikeKey(key) {
52
+ return key === "rate" || /rate|error/i.test(key ?? "");
53
+ }
54
+
55
+ function formatRate(value) {
56
+ if (typeof value !== "number") {
57
+ return String(value);
58
+ }
59
+
60
+ return `${formatNumber(value * 100)}%`;
61
+ }
62
+
63
+ function formatDisplayValue(value, options = {}) {
64
+ if (value == null) {
65
+ return "—";
66
+ }
67
+
68
+ if (typeof value !== "number") {
69
+ return String(value);
70
+ }
71
+
72
+ if (options.asPercent) {
73
+ return formatRate(value);
74
+ }
75
+
76
+ const formatted = formatNumber(value);
77
+ return options.unit ? `${formatted} ${options.unit}` : formatted;
78
+ }
79
+
80
+ function renderMetricList(metrics) {
81
+ if (!metrics?.length) {
82
+ return '<div class="perf-empty">No metrics captured.</div>';
83
+ }
84
+
85
+ return metrics
86
+ .map(
87
+ (metric) => `
88
+ <div class="perf-stat">
89
+ <span class="perf-stat-label">${escapeHtml(metric.label)}</span>
90
+ <strong>${escapeHtml(
91
+ formatDisplayValue(metric.value, {
92
+ unit: metric.unit,
93
+ asPercent: isRateLikeLabel(metric.label),
94
+ }),
95
+ )}</strong>
96
+ </div>`,
97
+ )
98
+ .join("");
99
+ }
100
+
101
+ function renderMetricValueMap(values, metricType, metricContains) {
102
+ const entries = Object.entries(values ?? {}).sort(([left], [right]) => {
103
+ const leftIndex = METRIC_VALUE_ORDER.indexOf(left);
104
+ const rightIndex = METRIC_VALUE_ORDER.indexOf(right);
105
+
106
+ if (leftIndex === -1 && rightIndex === -1) {
107
+ return left.localeCompare(right);
108
+ }
109
+ if (leftIndex === -1) return 1;
110
+ if (rightIndex === -1) return -1;
111
+ return leftIndex - rightIndex;
112
+ });
113
+ if (!entries.length) {
114
+ return '<span class="perf-muted">No metric values captured</span>';
115
+ }
116
+
117
+ const unit = metricContains === "time" ? "ms" : undefined;
118
+
119
+ return entries
120
+ .map(([key, value]) => {
121
+ const usePercent = key === "rate" && metricType === "rate";
122
+ const suffix = key === "rate" && !usePercent ? "/s" : undefined;
123
+ return `<div class="perf-value-chip">${escapeHtml(key)}: ${escapeHtml(
124
+ formatDisplayValue(value, {
125
+ asPercent: usePercent,
126
+ unit: suffix ?? unit,
127
+ }),
128
+ )}</div>`;
129
+ })
130
+ .join("");
131
+ }
132
+
133
+ function renderTable(headers, rows, emptyMessage = "No data captured.") {
134
+ if (!rows.length) {
135
+ return `<div class="perf-empty">${escapeHtml(emptyMessage)}</div>`;
136
+ }
137
+
138
+ return `
139
+ <table class="perf-table">
140
+ <thead>
141
+ <tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr>
142
+ </thead>
143
+ <tbody>
144
+ ${rows
145
+ .map(
146
+ (cells) => `
147
+ <tr>${cells
148
+ .map((cell) => {
149
+ if (cell == null) return "<td>—</td>";
150
+ if (typeof cell === "object" && cell && "html" in cell) {
151
+ return `<td>${cell.html}</td>`;
152
+ }
153
+ return `<td>${escapeHtml(
154
+ typeof cell === "number" ? formatNumber(cell) : cell,
155
+ )}</td>`;
156
+ })
157
+ .join("")}</tr>`,
158
+ )
159
+ .join("")}
160
+ </tbody>
161
+ </table>`;
162
+ }
163
+
164
+ export function buildReportPaneModel({ pillar, selectedView, perfReportUrl }) {
165
+ if (pillar === "journeys") {
166
+ // Journeys produce per-run markdown reports + snapshot galleries under
167
+ // reports/journeys/<run-id>/ (no Playwright HTML report); the pane
168
+ // shows the raw run output which links to those artifacts.
169
+ return {
170
+ title: "Journeys Output",
171
+ activeView: "raw",
172
+ openUrl: null,
173
+ tabsVisible: false,
174
+ emptyMessage:
175
+ "Run journeys to see run output; per-run reports land under reports/journeys/",
176
+ };
177
+ }
178
+
179
+ if (pillar === "security") {
180
+ // ZAP writes report.json/report.html per scan under
181
+ // reports/security/<env>/<run>/ (no Playwright HTML report).
182
+ return {
183
+ title: "Security Scan Output",
184
+ activeView: "raw",
185
+ openUrl: null,
186
+ tabsVisible: false,
187
+ emptyMessage:
188
+ "Run security scans to see run output; ZAP reports land under reports/security/",
189
+ };
190
+ }
191
+
192
+ if (pillar === "suites") {
193
+ return {
194
+ title: "Suite Output",
195
+ activeView: "raw",
196
+ openUrl: null,
197
+ tabsVisible: false,
198
+ emptyMessage: "Run the repo suites to see their output",
199
+ };
200
+ }
201
+
202
+ if (pillar !== "performance") {
203
+ return {
204
+ title: "Playwright Report",
205
+ activeView: "playwright",
206
+ openUrl: "/report/",
207
+ tabsVisible: false,
208
+ emptyMessage: "Run tests to see the Playwright report",
209
+ };
210
+ }
211
+
212
+ return {
213
+ title: "Performance Report",
214
+ activeView: selectedView === "raw" ? "raw" : "rendered",
215
+ openUrl: perfReportUrl ?? null,
216
+ tabsVisible: true,
217
+ emptyMessage: "Run performance tests to see the rendered report",
218
+ };
219
+ }
220
+
221
+ export function getNextReportViewOnPillarChange({
222
+ currentPillar,
223
+ nextPillar,
224
+ currentReportView,
225
+ }) {
226
+ if (currentPillar === nextPillar) {
227
+ return currentReportView;
228
+ }
229
+
230
+ if (nextPillar === "performance") {
231
+ return currentReportView === "playwright" ? "rendered" : currentReportView;
232
+ }
233
+
234
+ return currentReportView;
235
+ }
236
+
237
+ export function renderPerfReportHtml(report) {
238
+ const moduleSections = (report.modules ?? [])
239
+ .map((moduleReport) => {
240
+ const thresholds = renderTable(
241
+ ["Metric", "Expression", "Actual", "Status"],
242
+ (moduleReport.thresholds ?? []).map((threshold) => [
243
+ threshold.metric,
244
+ threshold.expression,
245
+ formatDisplayValue(threshold.actualValue, {
246
+ asPercent: /^rate/.test(threshold.expression),
247
+ unit:
248
+ threshold.unit && !/^rate/.test(threshold.expression)
249
+ ? threshold.unit
250
+ : undefined,
251
+ }),
252
+ threshold.passed ? "passed" : "failed",
253
+ ]),
254
+ "No thresholds configured.",
255
+ );
256
+
257
+ const checks = renderTable(
258
+ ["Check", "Passes", "Fails", "Pass rate"],
259
+ (moduleReport.checks ?? []).map((check) => [
260
+ check.name,
261
+ check.passes,
262
+ check.fails,
263
+ check.passRate == null
264
+ ? null
265
+ : `${(check.passRate * 100).toFixed(1)}%`,
266
+ ]),
267
+ "No checks captured.",
268
+ );
269
+
270
+ const scenarios = renderTable(
271
+ ["Scenario", "Executor", "Details"],
272
+ (moduleReport.scenarios ?? []).map((scenario) => [
273
+ scenario.name,
274
+ scenario.executor ?? "—",
275
+ JSON.stringify(scenario.options ?? {}),
276
+ ]),
277
+ "No scenarios captured.",
278
+ );
279
+
280
+ const customMetrics = renderTable(
281
+ ["Metric", "Values", "Thresholds"],
282
+ (moduleReport.customMetrics ?? []).map((metric) => [
283
+ metric.key,
284
+ {
285
+ html: renderMetricValueMap(
286
+ metric.values,
287
+ metric.type,
288
+ metric.contains,
289
+ ),
290
+ },
291
+ metric.thresholds?.length
292
+ ? metric.thresholds
293
+ .map(
294
+ (threshold) =>
295
+ `${threshold.expression} (${threshold.passed ? "pass" : "fail"})`,
296
+ )
297
+ .join(", ")
298
+ : "—",
299
+ ]),
300
+ "No custom metrics captured.",
301
+ );
302
+
303
+ return `
304
+ <details class="perf-module"${moduleReport.status === "failed" ? " open" : ""}>
305
+ <summary class="perf-module-header">
306
+ <span class="perf-module-title-wrap">
307
+ <span class="perf-module-toggle" aria-hidden="true">▸</span>
308
+ <span class="perf-module-title">${escapeHtml(moduleReport.module)}</span>
309
+ </span>
310
+ <span class="perf-badge ${moduleReport.status === "passed" ? "perf-ok" : "perf-fail"}">${escapeHtml(moduleReport.status)}</span>
311
+ </summary>
312
+ <div class="perf-section">
313
+ <h4>Summary</h4>
314
+ <div class="perf-stat-grid">${renderMetricList(moduleReport.coreMetrics)}</div>
315
+ </div>
316
+ <div class="perf-section">
317
+ <h4>Latency</h4>
318
+ <div class="perf-stat-grid">${renderMetricList(moduleReport.latencyMetrics)}</div>
319
+ </div>
320
+ <div class="perf-section">
321
+ <h4>Throughput &amp; Errors</h4>
322
+ <div class="perf-stat-grid">${renderMetricList(moduleReport.throughputMetrics)}</div>
323
+ </div>
324
+ <div class="perf-section">
325
+ <h4>Thresholds</h4>
326
+ ${thresholds}
327
+ </div>
328
+ <div class="perf-section">
329
+ <h4>Scenarios</h4>
330
+ ${scenarios}
331
+ </div>
332
+ <div class="perf-section">
333
+ <h4>Checks</h4>
334
+ ${checks}
335
+ </div>
336
+ <div class="perf-section">
337
+ <h4>Custom Metrics</h4>
338
+ ${customMetrics}
339
+ </div>
340
+ </details>`;
341
+ })
342
+ .join("");
343
+
344
+ return `
345
+ <article class="perf-report">
346
+ <header class="perf-run-header">
347
+ <div>
348
+ <h2>Summary</h2>
349
+ <p>${escapeHtml(report.environment)} · ${escapeHtml(report.status)}</p>
350
+ </div>
351
+ <div class="perf-run-meta">
352
+ <span>Modules: ${escapeHtml(report.modules?.length ?? 0)}</span>
353
+ <span>Started: ${escapeHtml(report.startedAt ?? "—")}</span>
354
+ <span>Finished: ${escapeHtml(report.finishedAt ?? "—")}</span>
355
+ </div>
356
+ </header>
357
+ ${moduleSections}
358
+ </article>`;
359
+ }