@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,404 @@
1
+ import type {
2
+ PerfCheckResult,
3
+ PerfMetricSummary,
4
+ PerfModuleRunReport,
5
+ PerfReportValue,
6
+ PerfRunReport,
7
+ PerfScenarioSummary,
8
+ PerfThresholdResult,
9
+ } from "./report-types.js";
10
+
11
+ interface K6Metric {
12
+ type?: string;
13
+ contains?: string;
14
+ values?: Record<string, number>;
15
+ thresholds?: Record<string, boolean | { ok?: boolean }>;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ interface K6Check {
20
+ name?: string;
21
+ passes?: number;
22
+ fails?: number;
23
+ }
24
+
25
+ export interface K6Summary {
26
+ metrics?: Record<string, K6Metric>;
27
+ root_group?: {
28
+ checks?: K6Check[] | Record<string, Omit<K6Check, "name">>;
29
+ };
30
+ options?: {
31
+ scenarios?: Record<string, Record<string, unknown>>;
32
+ };
33
+ }
34
+
35
+ interface NormalizeSummaryInput {
36
+ module: string;
37
+ summary: K6Summary;
38
+ artifactPath: string;
39
+ startedAt?: string;
40
+ finishedAt?: string;
41
+ }
42
+
43
+ interface BuildRunReportInput {
44
+ environment: string;
45
+ modules: NormalizeSummaryInput[];
46
+ startedAt?: string;
47
+ finishedAt?: string;
48
+ }
49
+
50
+ const CORE_METRIC_DEFS: Array<{
51
+ key: string;
52
+ source: string;
53
+ label: string;
54
+ unit?: string;
55
+ }> = [
56
+ { key: "count", source: "http_reqs", label: "HTTP requests" },
57
+ { key: "count", source: "iterations", label: "Iterations" },
58
+ { key: "value", source: "vus", label: "Virtual users" },
59
+ ];
60
+
61
+ const LATENCY_METRIC_DEFS: Array<{
62
+ key: string;
63
+ source: string;
64
+ label: string;
65
+ }> = [
66
+ { key: "avg", source: "http_req_duration", label: "HTTP avg" },
67
+ { key: "med", source: "http_req_duration", label: "HTTP p50" },
68
+ { key: "p(95)", source: "http_req_duration", label: "HTTP p95" },
69
+ { key: "p(99)", source: "http_req_duration", label: "HTTP p99" },
70
+ { key: "max", source: "http_req_duration", label: "HTTP max" },
71
+ ];
72
+
73
+ const THROUGHPUT_METRIC_DEFS: Array<{
74
+ key: string;
75
+ source: string;
76
+ label: string;
77
+ }> = [
78
+ { key: "rate", source: "http_reqs", label: "Requests / sec" },
79
+ { key: "rate", source: "iterations", label: "Iterations / sec" },
80
+ { key: "rate", source: "http_req_failed", label: "Failure rate" },
81
+ ];
82
+
83
+ export function normalizeK6Summary(
84
+ input: NormalizeSummaryInput,
85
+ ): PerfModuleRunReport {
86
+ const metrics = input.summary.metrics ?? {};
87
+ const thresholds = flattenThresholds(metrics);
88
+ const checks = normalizeChecks(input.summary.root_group?.checks);
89
+ const scenarios = normalizeScenarios(input.summary.options?.scenarios);
90
+ const customMetrics = normalizeCustomMetrics(metrics);
91
+
92
+ return {
93
+ module: input.module,
94
+ status: thresholds.some((threshold) => !threshold.passed)
95
+ ? "failed"
96
+ : "passed",
97
+ startedAt: input.startedAt,
98
+ finishedAt: input.finishedAt,
99
+ thresholds,
100
+ coreMetrics: selectReportValues(metrics, CORE_METRIC_DEFS),
101
+ latencyMetrics: selectReportValues(metrics, LATENCY_METRIC_DEFS, "ms"),
102
+ throughputMetrics: selectReportValues(metrics, THROUGHPUT_METRIC_DEFS),
103
+ scenarios,
104
+ checks,
105
+ customMetrics,
106
+ rawArtifacts: [
107
+ {
108
+ kind: "k6-summary",
109
+ path: input.artifactPath,
110
+ },
111
+ ],
112
+ };
113
+ }
114
+
115
+ export function buildPerfRunReport(input: BuildRunReportInput): PerfRunReport {
116
+ const modules = input.modules.map((moduleInput) =>
117
+ normalizeK6Summary(moduleInput),
118
+ );
119
+
120
+ return {
121
+ pillar: "performance",
122
+ environment: input.environment,
123
+ status: modules.some((module) => module.status === "failed")
124
+ ? "failed"
125
+ : "passed",
126
+ startedAt: input.startedAt,
127
+ finishedAt: input.finishedAt,
128
+ modules,
129
+ rawArtifacts: modules.flatMap((module) => module.rawArtifacts),
130
+ };
131
+ }
132
+
133
+ function flattenThresholds(
134
+ metrics: Record<string, K6Metric>,
135
+ ): PerfThresholdResult[] {
136
+ return Object.entries(metrics).flatMap(([metricKey, metric]) => {
137
+ const thresholdEntries = Object.entries(metric.thresholds ?? {});
138
+ return thresholdEntries.map(([expression, passed]) => {
139
+ const actualValue = deriveThresholdActualValue(metric, expression);
140
+ return {
141
+ metric: metricKey,
142
+ name: `${metricKey}:${expression}`,
143
+ expression,
144
+ passed: normalizeThresholdPassed(actualValue, expression, passed),
145
+ actualValue,
146
+ unit: metric.contains === "time" ? "ms" : undefined,
147
+ };
148
+ });
149
+ });
150
+ }
151
+
152
+ function deriveThresholdActualValue(
153
+ metric: K6Metric,
154
+ expression: string,
155
+ ): number | string | null {
156
+ const values = getMetricValues(metric);
157
+ const percentile = expression.match(/^p\(([\d.]+)\)/);
158
+ if (percentile) {
159
+ return values[`p(${percentile[1]})`] ?? null;
160
+ }
161
+
162
+ if (expression.startsWith("rate")) {
163
+ return firstDefinedNumber(values.rate, values.value) ?? null;
164
+ }
165
+
166
+ if (expression.startsWith("avg")) {
167
+ return values.avg ?? null;
168
+ }
169
+
170
+ if (expression.startsWith("count")) {
171
+ return values.count ?? null;
172
+ }
173
+
174
+ return values.value ?? values.avg ?? values.rate ?? values.count ?? null;
175
+ }
176
+
177
+ function selectReportValues(
178
+ metrics: Record<string, K6Metric>,
179
+ definitions: Array<{ key: string; source: string; label: string }>,
180
+ unit?: string,
181
+ ): PerfReportValue[] {
182
+ return definitions.flatMap((definition) => {
183
+ const value = getMetricDisplayValue(
184
+ metrics[definition.source],
185
+ definition.key,
186
+ );
187
+ if (typeof value !== "number") {
188
+ return [];
189
+ }
190
+
191
+ return [
192
+ {
193
+ label: definition.label,
194
+ value,
195
+ unit,
196
+ },
197
+ ];
198
+ });
199
+ }
200
+
201
+ function normalizeChecks(
202
+ checks?: K6Check[] | Record<string, Omit<K6Check, "name">>,
203
+ ): PerfCheckResult[] {
204
+ const normalizedChecks = Array.isArray(checks)
205
+ ? checks
206
+ : Object.entries(checks ?? {}).map(([name, check]) => ({
207
+ ...check,
208
+ name,
209
+ }));
210
+
211
+ return normalizedChecks
212
+ .filter(
213
+ (check): check is Required<Pick<K6Check, "name">> & K6Check =>
214
+ typeof check.name === "string",
215
+ )
216
+ .map((check) => {
217
+ const passes = check.passes ?? 0;
218
+ const fails = check.fails ?? 0;
219
+ const total = passes + fails;
220
+ return {
221
+ name: check.name,
222
+ passes,
223
+ fails,
224
+ passRate: total > 0 ? passes / total : null,
225
+ };
226
+ });
227
+ }
228
+
229
+ function normalizeScenarios(
230
+ scenarios?: Record<string, Record<string, unknown>>,
231
+ ): PerfScenarioSummary[] {
232
+ return Object.entries(scenarios ?? {}).map(([name, scenario]) => ({
233
+ name,
234
+ executor:
235
+ typeof scenario.executor === "string" ? scenario.executor : undefined,
236
+ startTime:
237
+ typeof scenario.startTime === "string" ? scenario.startTime : undefined,
238
+ gracefulStop:
239
+ typeof scenario.gracefulStop === "string"
240
+ ? scenario.gracefulStop
241
+ : undefined,
242
+ options: scenario,
243
+ }));
244
+ }
245
+
246
+ function normalizeCustomMetrics(
247
+ metrics: Record<string, K6Metric>,
248
+ ): PerfMetricSummary[] {
249
+ const builtInMetrics = new Set([
250
+ "http_reqs",
251
+ "http_req_failed",
252
+ "http_req_duration",
253
+ "http_req_blocked",
254
+ "http_req_connecting",
255
+ "http_req_receiving",
256
+ "http_req_sending",
257
+ "http_req_tls_handshaking",
258
+ "http_req_waiting",
259
+ "iteration_duration",
260
+ "iterations",
261
+ "vus",
262
+ "vus_max",
263
+ "checks",
264
+ "data_received",
265
+ "data_sent",
266
+ "group_duration",
267
+ ]);
268
+
269
+ return Object.entries(metrics)
270
+ .filter(([metricKey]) => {
271
+ const baseKey = metricKey.replace(/\{.*\}$/, "");
272
+ return !builtInMetrics.has(baseKey);
273
+ })
274
+ .map(([metricKey, metric]) => ({
275
+ key: metricKey,
276
+ label: metricKey.replace(/_/g, " "),
277
+ type: metric.type,
278
+ contains: metric.contains,
279
+ values: normalizeMetricValues(getMetricValues(metric)),
280
+ thresholds: flattenThresholds({ [metricKey]: metric }),
281
+ isCustom: true,
282
+ }));
283
+ }
284
+
285
+ function normalizeMetricValues(
286
+ values?: Record<string, number>,
287
+ ): Record<string, number | null> {
288
+ return Object.fromEntries(
289
+ Object.entries(values ?? {}).map(([key, value]) => [
290
+ key,
291
+ typeof value === "number" ? value : null,
292
+ ]),
293
+ );
294
+ }
295
+
296
+ function getMetricValues(metric?: K6Metric): Record<string, number> {
297
+ if (!metric) {
298
+ return {};
299
+ }
300
+
301
+ if (metric.values && Object.keys(metric.values).length > 0) {
302
+ return normalizeNumericEntries(metric.values);
303
+ }
304
+
305
+ const topLevelEntries = Object.entries(metric).filter(
306
+ (entry): entry is [string, number] => {
307
+ const [key, value] = entry;
308
+ if (key === "type" || key === "contains" || key === "thresholds") {
309
+ return false;
310
+ }
311
+
312
+ return typeof value === "number";
313
+ },
314
+ );
315
+
316
+ return Object.fromEntries(topLevelEntries);
317
+ }
318
+
319
+ function getMetricDisplayValue(
320
+ metric: K6Metric | undefined,
321
+ preferredKey: string,
322
+ ): number | undefined {
323
+ const values = getMetricValues(metric);
324
+
325
+ if (metric?.type === "gauge" && preferredKey === "value") {
326
+ return firstDefinedNumber(values.max, values.value, values.min);
327
+ }
328
+
329
+ if (preferredKey === "rate") {
330
+ return firstDefinedNumber(values.rate, values.value);
331
+ }
332
+
333
+ return values[preferredKey];
334
+ }
335
+
336
+ function firstDefinedNumber(
337
+ ...candidates: Array<number | undefined>
338
+ ): number | undefined {
339
+ return candidates.find((candidate) => typeof candidate === "number");
340
+ }
341
+
342
+ function normalizeNumericEntries(
343
+ values: Record<string, number>,
344
+ ): Record<string, number> {
345
+ return Object.fromEntries(
346
+ Object.entries(values).filter(
347
+ (entry): entry is [string, number] => typeof entry[1] === "number",
348
+ ),
349
+ );
350
+ }
351
+
352
+ function normalizeThresholdPassed(
353
+ actualValue: number | string | null,
354
+ expression: string,
355
+ value: boolean | { ok?: boolean },
356
+ ): boolean {
357
+ const evaluated = evaluateThresholdExpression(actualValue, expression);
358
+ if (typeof evaluated === "boolean") {
359
+ return evaluated;
360
+ }
361
+
362
+ if (typeof value === "boolean") {
363
+ return value;
364
+ }
365
+
366
+ return value.ok ?? false;
367
+ }
368
+
369
+ function evaluateThresholdExpression(
370
+ actualValue: number | string | null,
371
+ expression: string,
372
+ ): boolean | undefined {
373
+ if (typeof actualValue !== "number") {
374
+ return undefined;
375
+ }
376
+
377
+ const match = expression.match(
378
+ /^[a-zA-Z0-9_.()]+\s*(<=|>=|<|>|==)\s*(-?\d+(?:\.\d+)?)$/,
379
+ );
380
+ if (!match) {
381
+ return undefined;
382
+ }
383
+
384
+ const [, operator, targetRaw] = match;
385
+ const target = Number(targetRaw);
386
+ if (!Number.isFinite(target)) {
387
+ return undefined;
388
+ }
389
+
390
+ switch (operator) {
391
+ case "<":
392
+ return actualValue < target;
393
+ case "<=":
394
+ return actualValue <= target;
395
+ case ">":
396
+ return actualValue > target;
397
+ case ">=":
398
+ return actualValue >= target;
399
+ case "==":
400
+ return actualValue === target;
401
+ default:
402
+ return undefined;
403
+ }
404
+ }
@@ -0,0 +1,19 @@
1
+ import * as path from "path";
2
+
3
+ export const PERF_REPORT_ROOT = ".perf-report";
4
+ export const PERF_CURRENT_REPORT_DIR = path.join(PERF_REPORT_ROOT, "current");
5
+ export const PERF_CURRENT_REPORT_FILE = path.join(
6
+ PERF_CURRENT_REPORT_DIR,
7
+ "report.json",
8
+ );
9
+
10
+ export function getPerfSummaryArtifactName(moduleName: string): string {
11
+ return `${moduleName.replace(/\//g, "-")}.summary.json`;
12
+ }
13
+
14
+ export function getPerfSummaryArtifactPath(moduleName: string): string {
15
+ return path.join(
16
+ PERF_CURRENT_REPORT_DIR,
17
+ getPerfSummaryArtifactName(moduleName),
18
+ );
19
+ }
@@ -0,0 +1,110 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import type { PerfRunReport } from "./report-types.js";
4
+ import { getDistRoot } from "../paths.js";
5
+ import { buildPerfRunReport, type K6Summary } from "./report-normalizer.js";
6
+ import {
7
+ PERF_CURRENT_REPORT_DIR,
8
+ PERF_CURRENT_REPORT_FILE,
9
+ getPerfSummaryArtifactPath,
10
+ } from "./report-paths.js";
11
+
12
+ interface WriteCurrentPerfRunReportInput {
13
+ rootDir?: string;
14
+ environment: string;
15
+ modules: string[];
16
+ startedAt?: string;
17
+ finishedAt?: string;
18
+ }
19
+
20
+ export function writeCurrentPerfRunReport(
21
+ input: WriteCurrentPerfRunReportInput,
22
+ ): PerfRunReport {
23
+ // Default anchored to the den root, NOT process.cwd(): this WRITES
24
+ // .perf-report/, and perf children spawned by the CLI inherit an arbitrary
25
+ // user cwd (bin/den never cds).
26
+ const rootDir = input.rootDir ?? getDistRoot();
27
+ const report = buildPerfRunReport({
28
+ environment: input.environment,
29
+ modules: input.modules.map((moduleName) => ({
30
+ module: moduleName,
31
+ summary: readSummaryArtifact(rootDir, moduleName),
32
+ artifactPath: getPerfSummaryArtifactPath(moduleName),
33
+ startedAt: input.startedAt,
34
+ finishedAt: input.finishedAt,
35
+ })),
36
+ startedAt: input.startedAt,
37
+ finishedAt: input.finishedAt,
38
+ });
39
+
40
+ fs.mkdirSync(path.join(rootDir, PERF_CURRENT_REPORT_DIR), {
41
+ recursive: true,
42
+ });
43
+ fs.writeFileSync(
44
+ path.join(rootDir, PERF_CURRENT_REPORT_FILE),
45
+ JSON.stringify(report, null, 2),
46
+ "utf-8",
47
+ );
48
+
49
+ return report;
50
+ }
51
+
52
+ export function loadCurrentPerfRunReport(
53
+ rootDir = getDistRoot(),
54
+ ): PerfRunReport | null {
55
+ const reportPath = path.join(rootDir, PERF_CURRENT_REPORT_FILE);
56
+ if (!fs.existsSync(reportPath)) {
57
+ return null;
58
+ }
59
+
60
+ // A truncated/corrupt report.json must not crash the request handler — treat
61
+ // an unparseable report as "no report yet".
62
+ try {
63
+ return JSON.parse(fs.readFileSync(reportPath, "utf-8")) as PerfRunReport;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Remove the previous run's normalized report + raw summary artifacts at the
71
+ * START of a run.
72
+ *
73
+ * `.perf-report/current/` is reused across runs, so without this the prior
74
+ * run's `report.json` (and per-module `*.summary.json`) linger. If the report
75
+ * is read before the current run regenerates it — or a threshold-breaching run
76
+ * never regenerates it — `loadCurrentPerfRunReport` silently serves STALE prior
77
+ * results (the back-to-back staleness bug). Clearing here makes such a read
78
+ * return `null` ("no report yet", loud) instead of stale data.
79
+ */
80
+ export function resetCurrentPerfRunReport(rootDir = getDistRoot()): void {
81
+ try {
82
+ fs.rmSync(path.join(rootDir, PERF_CURRENT_REPORT_FILE), { force: true });
83
+ } catch {
84
+ /* nothing to remove */
85
+ }
86
+ try {
87
+ const dir = path.join(rootDir, PERF_CURRENT_REPORT_DIR);
88
+ for (const entry of fs.readdirSync(dir)) {
89
+ if (entry.endsWith(".summary.json")) {
90
+ fs.rmSync(path.join(dir, entry), { force: true });
91
+ }
92
+ }
93
+ } catch {
94
+ /* dir may not exist yet */
95
+ }
96
+ }
97
+
98
+ function readSummaryArtifact(rootDir: string, moduleName: string): K6Summary {
99
+ const artifactPath = path.join(
100
+ rootDir,
101
+ getPerfSummaryArtifactPath(moduleName),
102
+ );
103
+ if (!fs.existsSync(artifactPath)) {
104
+ throw new Error(`Missing k6 summary artifact for module "${moduleName}"`);
105
+ }
106
+
107
+ // JSON boundary: k6 wrote this artifact, so the shape is trusted here and
108
+ // every field access in the normalizer is already optional/defensive.
109
+ return JSON.parse(fs.readFileSync(artifactPath, "utf-8")) as K6Summary;
110
+ }
@@ -0,0 +1,70 @@
1
+ export interface PerfArtifactRef {
2
+ kind: "k6-summary" | "normalized-report";
3
+ path: string;
4
+ }
5
+
6
+ export interface PerfReportValue {
7
+ label: string;
8
+ value: number | string | null;
9
+ unit?: string;
10
+ }
11
+
12
+ export interface PerfThresholdResult {
13
+ metric: string;
14
+ name: string;
15
+ expression: string;
16
+ passed: boolean;
17
+ actualValue: number | string | null;
18
+ unit?: string;
19
+ }
20
+
21
+ export interface PerfScenarioSummary {
22
+ name: string;
23
+ executor?: string;
24
+ startTime?: string;
25
+ gracefulStop?: string;
26
+ env?: Record<string, string>;
27
+ options?: Record<string, unknown>;
28
+ }
29
+
30
+ export interface PerfCheckResult {
31
+ name: string;
32
+ passes: number;
33
+ fails: number;
34
+ passRate: number | null;
35
+ }
36
+
37
+ export interface PerfMetricSummary {
38
+ key: string;
39
+ label: string;
40
+ type?: string;
41
+ contains?: string;
42
+ values: Record<string, number | null>;
43
+ thresholds: PerfThresholdResult[];
44
+ isCustom?: boolean;
45
+ }
46
+
47
+ export interface PerfModuleRunReport {
48
+ module: string;
49
+ status: "passed" | "failed";
50
+ startedAt?: string;
51
+ finishedAt?: string;
52
+ thresholds: PerfThresholdResult[];
53
+ coreMetrics: PerfReportValue[];
54
+ latencyMetrics: PerfReportValue[];
55
+ throughputMetrics: PerfReportValue[];
56
+ scenarios: PerfScenarioSummary[];
57
+ checks: PerfCheckResult[];
58
+ customMetrics: PerfMetricSummary[];
59
+ rawArtifacts: PerfArtifactRef[];
60
+ }
61
+
62
+ export interface PerfRunReport {
63
+ pillar: "performance";
64
+ environment: string;
65
+ status: "passed" | "failed";
66
+ startedAt?: string;
67
+ finishedAt?: string;
68
+ modules: PerfModuleRunReport[];
69
+ rawArtifacts: PerfArtifactRef[];
70
+ }
@@ -0,0 +1,25 @@
1
+ import * as path from "path";
2
+ import { getDistRoot } from "../paths.js";
3
+
4
+ /**
5
+ * Where a distribution keeps its own k6 suites, relative to the distribution
6
+ * root: `<dist root>/tests/pillars/perf/<module>/test.js`. The engine ships
7
+ * none; repo-owned suites are declared in each repo's manifest instead.
8
+ */
9
+ export const PERF_DIST_TESTS_DIR = path.join("tests", "pillars", "perf");
10
+
11
+ /** The k6 entry file every distribution perf module provides. */
12
+ export const PERF_TEST_ENTRY = "test.js";
13
+
14
+ /** Absolute path of the distribution's perf suites directory. */
15
+ export function getPerfTestsRoot(root: string = getDistRoot()): string {
16
+ return path.resolve(root, PERF_DIST_TESTS_DIR);
17
+ }
18
+
19
+ /** Absolute path of a distribution perf module's k6 entry file. */
20
+ export function getPerfTestFile(
21
+ moduleName: string,
22
+ root: string = getDistRoot(),
23
+ ): string {
24
+ return path.join(getPerfTestsRoot(root), moduleName, PERF_TEST_ENTRY);
25
+ }