@crvouga/atlas 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (429) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +178 -604
  3. package/dist/bun/src/driver.d.ts +41 -7
  4. package/dist/bun/src/driver.js +47 -6
  5. package/dist/bun/src/index.d.ts +8 -0
  6. package/dist/bun/src/index.js +8 -0
  7. package/dist/core/src/cli.d.ts +14 -1
  8. package/dist/core/src/cli.js +30 -1
  9. package/dist/core/src/config.d.ts +70 -4
  10. package/dist/core/src/config.js +62 -3
  11. package/dist/core/src/events/cloudevents.d.ts +54 -9
  12. package/dist/core/src/events/cloudevents.js +60 -9
  13. package/dist/core/src/graph.d.ts +21 -0
  14. package/dist/core/src/graph.js +26 -0
  15. package/dist/core/src/index.d.ts +11 -0
  16. package/dist/core/src/index.js +11 -0
  17. package/dist/core/src/lint.d.ts +30 -0
  18. package/dist/core/src/lint.js +13 -0
  19. package/dist/core/src/plan.d.ts +39 -1
  20. package/dist/core/src/plan.js +28 -1
  21. package/dist/core/src/privacy.d.ts +29 -2
  22. package/dist/core/src/privacy.js +19 -2
  23. package/dist/core/src/report/formats.d.ts +34 -4
  24. package/dist/core/src/report/formats.js +24 -4
  25. package/dist/core/src/report/markdown.d.ts +15 -0
  26. package/dist/core/src/report/markdown.js +8 -0
  27. package/dist/core/src/run/clients.d.ts +44 -5
  28. package/dist/core/src/run/clients.js +46 -2
  29. package/dist/core/src/run/execute.d.ts +69 -16
  30. package/dist/core/src/run/execute.js +67 -16
  31. package/dist/core/src/run/frames.d.ts +13 -4
  32. package/dist/core/src/run/frames.js +19 -4
  33. package/dist/core/src/run/http.d.ts +34 -1
  34. package/dist/core/src/run/http.js +14 -1
  35. package/dist/core/src/run/types.d.ts +76 -3
  36. package/dist/core/src/run/types.js +19 -3
  37. package/dist/core/src/signals.d.ts +49 -5
  38. package/dist/core/src/signals.js +22 -5
  39. package/dist/core/src/spec/compose.d.ts +23 -1
  40. package/dist/core/src/spec/compose.js +10 -1
  41. package/dist/core/src/spec/index.d.ts +9 -1
  42. package/dist/core/src/spec/index.js +9 -1
  43. package/dist/core/src/spec/load.d.ts +22 -1
  44. package/dist/core/src/spec/load.js +31 -1
  45. package/dist/core/src/spec/parse.d.ts +16 -2
  46. package/dist/core/src/spec/parse.js +25 -2
  47. package/dist/core/src/spec/scxml.d.ts +25 -4
  48. package/dist/core/src/spec/scxml.js +36 -4
  49. package/dist/core/src/spec/types.d.ts +52 -1
  50. package/dist/core/src/spec/types.js +24 -0
  51. package/dist/detox/src/index.d.ts +54 -11
  52. package/dist/detox/src/index.js +45 -7
  53. package/dist/playwright/src/driver.d.ts +39 -3
  54. package/dist/playwright/src/driver.js +27 -3
  55. package/dist/playwright/src/index.d.ts +10 -0
  56. package/dist/playwright/src/index.js +10 -0
  57. package/dist/playwright/src/media.d.ts +50 -4
  58. package/dist/playwright/src/media.js +55 -4
  59. package/dist/playwright/src/signals.d.ts +44 -5
  60. package/dist/playwright/src/signals.js +29 -5
  61. package/dist/reports/src/attachment-files.d.ts +44 -0
  62. package/dist/reports/src/attachment-files.js +121 -0
  63. package/dist/reports/src/attachment-response.d.ts +31 -0
  64. package/dist/reports/src/attachment-response.js +55 -0
  65. package/dist/reports/src/aws-credentials.d.ts +48 -0
  66. package/dist/reports/src/aws-credentials.js +221 -0
  67. package/dist/reports/src/browser-navigation.d.ts +14 -0
  68. package/dist/reports/src/browser-navigation.js +7 -0
  69. package/dist/reports/src/bundle.d.ts +34 -2
  70. package/dist/reports/src/bundle.js +38 -2
  71. package/dist/reports/src/catalog.d.ts +34 -3
  72. package/dist/reports/src/catalog.js +17 -3
  73. package/dist/reports/src/cli-stores.d.ts +36 -0
  74. package/dist/reports/src/cli-stores.js +73 -0
  75. package/dist/reports/src/clock.d.ts +7 -2
  76. package/dist/reports/src/clock.js +5 -2
  77. package/dist/reports/src/completion-order.d.ts +18 -5
  78. package/dist/reports/src/completion-order.js +18 -5
  79. package/dist/reports/src/compose.d.ts +80 -0
  80. package/dist/reports/src/compose.js +117 -0
  81. package/dist/reports/src/copy-store.d.ts +29 -0
  82. package/dist/reports/src/copy-store.js +52 -0
  83. package/dist/reports/src/directory-outline.d.ts +38 -0
  84. package/dist/reports/src/directory-outline.js +103 -0
  85. package/dist/reports/src/directory-store.d.ts +20 -0
  86. package/dist/reports/src/directory-store.js +166 -0
  87. package/dist/reports/src/doctor.d.ts +29 -0
  88. package/dist/reports/src/doctor.js +83 -0
  89. package/dist/reports/src/effective-delta.d.ts +34 -0
  90. package/dist/reports/src/effective-delta.js +25 -0
  91. package/dist/reports/src/evidence-detail.d.ts +64 -0
  92. package/dist/reports/src/evidence-detail.js +92 -0
  93. package/dist/reports/src/evidence-live.d.ts +40 -4
  94. package/dist/reports/src/evidence-live.js +52 -4
  95. package/dist/reports/src/evidence-types.d.ts +70 -1
  96. package/dist/reports/src/evidence-types.js +5 -1
  97. package/dist/reports/src/evidence-view.d.ts +58 -6
  98. package/dist/reports/src/evidence-view.js +44 -5
  99. package/dist/reports/src/explorer-types.d.ts +40 -3
  100. package/dist/reports/src/explorer-types.js +8 -0
  101. package/dist/reports/src/explorer-versions.d.ts +9 -1
  102. package/dist/reports/src/explorer-versions.js +18 -1
  103. package/dist/reports/src/export-evidence.d.ts +16 -1
  104. package/dist/reports/src/export-evidence.js +52 -1
  105. package/dist/reports/src/fetcher.d.ts +27 -3
  106. package/dist/reports/src/fetcher.js +11 -3
  107. package/dist/reports/src/format-date.d.ts +6 -0
  108. package/dist/reports/src/format-date.js +6 -0
  109. package/dist/reports/src/format.d.ts +16 -0
  110. package/dist/reports/src/format.js +10 -0
  111. package/dist/reports/src/frames.d.ts +36 -3
  112. package/dist/reports/src/frames.js +22 -3
  113. package/dist/reports/src/hash.d.ts +18 -0
  114. package/dist/reports/src/hash.js +13 -0
  115. package/dist/reports/src/host.d.ts +29 -0
  116. package/dist/reports/src/host.js +64 -0
  117. package/dist/reports/src/html-frame.d.ts +5 -6
  118. package/dist/reports/src/html-frame.js +12 -1
  119. package/dist/reports/src/http-store.d.ts +52 -0
  120. package/dist/reports/src/http-store.js +145 -0
  121. package/dist/reports/src/http.d.ts +7 -0
  122. package/dist/reports/src/http.js +6 -0
  123. package/dist/reports/src/ingest.d.ts +54 -0
  124. package/dist/reports/src/ingest.js +210 -0
  125. package/dist/reports/src/init.d.ts +40 -0
  126. package/dist/reports/src/init.js +364 -0
  127. package/dist/reports/src/junit.d.ts +33 -6
  128. package/dist/reports/src/junit.js +87 -21
  129. package/dist/reports/src/keyboard.d.ts +20 -0
  130. package/dist/reports/src/keyboard.js +11 -0
  131. package/dist/reports/src/keys.d.ts +21 -3
  132. package/dist/reports/src/keys.js +21 -3
  133. package/dist/reports/src/layer-axes.d.ts +48 -0
  134. package/dist/reports/src/layer-axes.js +71 -0
  135. package/dist/reports/src/layer-catalog.d.ts +28 -1
  136. package/dist/reports/src/layer-catalog.js +19 -2
  137. package/dist/reports/src/layer-merge.d.ts +98 -9
  138. package/dist/reports/src/layer-merge.js +154 -33
  139. package/dist/reports/src/layer-source.d.ts +18 -0
  140. package/dist/reports/src/layer-source.js +14 -0
  141. package/dist/reports/src/layer-store.d.ts +42 -0
  142. package/dist/reports/src/layer-store.js +63 -5
  143. package/dist/reports/src/layer-types.d.ts +34 -1
  144. package/dist/reports/src/layers-route.d.ts +47 -4
  145. package/dist/reports/src/layers-route.js +88 -12
  146. package/dist/reports/src/media-types.d.ts +11 -0
  147. package/dist/reports/src/media-types.js +22 -0
  148. package/dist/reports/src/namespace-guard.d.ts +10 -1
  149. package/dist/reports/src/namespace-guard.js +10 -1
  150. package/dist/reports/src/navigation.d.ts +12 -0
  151. package/dist/reports/src/navigation.js +7 -0
  152. package/dist/reports/src/object-io.d.ts +11 -4
  153. package/dist/reports/src/object-io.js +4 -4
  154. package/dist/reports/src/object-store-conformance.d.ts +27 -0
  155. package/dist/reports/src/object-store-conformance.js +87 -0
  156. package/dist/reports/src/object-store.d.ts +33 -24
  157. package/dist/reports/src/object-store.js +17 -24
  158. package/dist/reports/src/parse-with.d.ts +16 -0
  159. package/dist/reports/src/parse-with.js +14 -0
  160. package/dist/reports/src/performance-part.d.ts +25 -1
  161. package/dist/reports/src/performance-part.js +15 -1
  162. package/dist/reports/src/performance.d.ts +22 -2
  163. package/dist/reports/src/performance.js +12 -2
  164. package/dist/reports/src/persist.d.ts +12 -3
  165. package/dist/reports/src/persist.js +12 -3
  166. package/dist/reports/src/phone.d.ts +18 -1
  167. package/dist/reports/src/phone.js +18 -1
  168. package/dist/reports/src/protocol.d.ts +30 -4
  169. package/dist/reports/src/protocol.js +15 -4
  170. package/dist/reports/src/publication.d.ts +21 -1
  171. package/dist/reports/src/publication.js +21 -1
  172. package/dist/reports/src/publish-checks.d.ts +35 -0
  173. package/dist/reports/src/publish-checks.js +122 -0
  174. package/dist/reports/src/publish.d.ts +44 -9
  175. package/dist/reports/src/publish.js +40 -7
  176. package/dist/reports/src/read.d.ts +46 -16
  177. package/dist/reports/src/read.js +38 -14
  178. package/dist/reports/src/recording-bundle.d.ts +13 -2
  179. package/dist/reports/src/recording-bundle.js +4 -2
  180. package/dist/reports/src/recording-keys.d.ts +15 -1
  181. package/dist/reports/src/recording-keys.js +15 -1
  182. package/dist/reports/src/remote-report.d.ts +28 -7
  183. package/dist/reports/src/remote-report.js +15 -7
  184. package/dist/reports/src/report-cli.d.ts +51 -0
  185. package/dist/reports/src/report-cli.js +485 -0
  186. package/dist/reports/src/report-index.d.ts +99 -0
  187. package/dist/reports/src/report-index.js +337 -0
  188. package/dist/reports/src/report-loader.d.ts +10 -0
  189. package/dist/reports/src/report-loader.js +6 -0
  190. package/dist/reports/src/report-parts.d.ts +29 -3
  191. package/dist/reports/src/report-parts.js +40 -3
  192. package/dist/reports/src/report-status.d.ts +48 -0
  193. package/dist/reports/src/report-status.js +156 -0
  194. package/dist/reports/src/report-types.d.ts +73 -0
  195. package/dist/reports/src/report-types.js +3 -0
  196. package/dist/reports/src/reporter.d.ts +67 -0
  197. package/dist/reports/src/reporter.js +128 -0
  198. package/dist/reports/src/reports-explorer-route.d.ts +541 -0
  199. package/dist/reports/src/reports-explorer-route.js +11 -0
  200. package/dist/reports/src/reports-list-route.d.ts +9 -0
  201. package/dist/reports/src/reports-list-route.js +9 -0
  202. package/dist/reports/src/reports-route.d.ts +561 -22
  203. package/dist/reports/src/reports-route.js +11 -3
  204. package/dist/reports/src/reports-store.d.ts +19 -2
  205. package/dist/reports/src/reports-store.js +10 -2
  206. package/dist/reports/src/resilient-cache.d.ts +14 -1
  207. package/dist/reports/src/resilient-cache.js +8 -1
  208. package/dist/reports/src/retention.d.ts +17 -1
  209. package/dist/reports/src/retention.js +5 -1
  210. package/dist/reports/src/run-bundle.d.ts +63 -0
  211. package/dist/reports/src/run-bundle.js +159 -0
  212. package/dist/reports/src/s3-store.d.ts +79 -0
  213. package/dist/reports/src/s3-store.js +295 -0
  214. package/dist/reports/src/scenario.d.ts +14 -4
  215. package/dist/reports/src/scenario.js +5 -4
  216. package/dist/reports/src/sigv4.d.ts +72 -0
  217. package/dist/reports/src/sigv4.js +96 -0
  218. package/dist/reports/src/snapshot-bundle.d.ts +24 -1
  219. package/dist/reports/src/snapshot-bundle.js +27 -2
  220. package/dist/reports/src/snapshot-surface.d.ts +20 -9
  221. package/dist/reports/src/snapshot-surface.js +32 -19
  222. package/dist/reports/src/source-content.d.ts +23 -0
  223. package/dist/reports/src/source-content.js +30 -1
  224. package/dist/reports/src/source-entries.d.ts +13 -0
  225. package/dist/reports/src/source-entries.js +6 -0
  226. package/dist/reports/src/source-part.d.ts +15 -0
  227. package/dist/reports/src/source-part.js +8 -0
  228. package/dist/reports/src/source-reader.d.ts +11 -0
  229. package/dist/reports/src/source-reader.js +5 -0
  230. package/dist/reports/src/spec-evidence.d.ts +12 -2
  231. package/dist/reports/src/spec-evidence.js +6 -2
  232. package/dist/reports/src/spec-inventory.d.ts +15 -1
  233. package/dist/reports/src/spec-inventory.js +22 -1
  234. package/dist/reports/src/spec-link.d.ts +28 -0
  235. package/dist/reports/src/spec-link.js +70 -0
  236. package/dist/reports/src/spec-set.d.ts +63 -0
  237. package/dist/reports/src/spec-set.js +177 -0
  238. package/dist/reports/src/status.d.ts +10 -0
  239. package/dist/reports/src/status.js +5 -0
  240. package/dist/reports/src/store-prefix.d.ts +28 -0
  241. package/dist/reports/src/store-prefix.js +22 -0
  242. package/dist/reports/src/store-url.d.ts +89 -0
  243. package/dist/reports/src/store-url.js +160 -0
  244. package/dist/reports/src/test-catalog-entity.d.ts +11 -0
  245. package/dist/reports/src/test-catalog-entity.js +6 -0
  246. package/dist/reports/src/types.d.ts +6 -1
  247. package/dist/reports/src/use-report-nav.d.ts +35 -0
  248. package/dist/reports/src/use-report-nav.js +12 -0
  249. package/dist/schema/src/api.d.ts +799 -0
  250. package/dist/schema/src/api.js +201 -0
  251. package/dist/schema/src/cli-results.d.ts +210 -0
  252. package/dist/schema/src/cli-results.js +124 -0
  253. package/dist/schema/src/common.d.ts +53 -0
  254. package/dist/schema/src/common.js +42 -0
  255. package/dist/schema/src/index.d.ts +14 -0
  256. package/dist/schema/src/index.js +14 -0
  257. package/dist/schema/src/layer.d.ts +2273 -0
  258. package/dist/schema/src/layer.js +462 -0
  259. package/dist/schema/src/manifest.d.ts +95 -0
  260. package/dist/schema/src/manifest.js +84 -0
  261. package/dist/schema/src/report-sources.d.ts +28 -0
  262. package/dist/schema/src/report-sources.js +25 -0
  263. package/dist/schema/src/runs-index.d.ts +16 -0
  264. package/dist/schema/src/runs-index.js +15 -0
  265. package/dist/schema/src/server-config.d.ts +93 -0
  266. package/dist/schema/src/server-config.js +73 -0
  267. package/dist/schema/src/spec.d.ts +69 -0
  268. package/dist/schema/src/spec.js +58 -0
  269. package/dist/server/src/app.d.ts +62 -0
  270. package/dist/server/src/app.js +419 -0
  271. package/dist/server/src/auth.d.ts +41 -0
  272. package/dist/server/src/auth.js +59 -0
  273. package/dist/server/src/config.d.ts +83 -0
  274. package/dist/server/src/config.js +187 -0
  275. package/dist/server/src/dev.d.ts +12 -0
  276. package/dist/server/src/dev.js +80 -0
  277. package/dist/server/src/events.d.ts +29 -0
  278. package/dist/server/src/events.js +101 -0
  279. package/dist/server/src/host.d.ts +30 -0
  280. package/dist/server/src/host.js +115 -0
  281. package/dist/server/src/index.d.ts +16 -0
  282. package/dist/server/src/index.js +16 -0
  283. package/dist/server/src/llms.d.ts +19 -0
  284. package/dist/server/src/llms.js +63 -0
  285. package/dist/server/src/main.d.ts +30 -0
  286. package/dist/server/src/main.js +112 -0
  287. package/dist/server/src/openapi.d.ts +59 -0
  288. package/dist/server/src/openapi.js +374 -0
  289. package/dist/server/src/problem.d.ts +8 -0
  290. package/dist/server/src/problem.js +39 -0
  291. package/dist/server/src/site.d.ts +13 -0
  292. package/dist/server/src/site.js +43 -0
  293. package/dist/server/src/store-api.d.ts +28 -0
  294. package/dist/server/src/store-api.js +159 -0
  295. package/dist/server/src/validate.d.ts +71 -0
  296. package/dist/server/src/validate.js +197 -0
  297. package/dist/vitest/src/index.d.ts +27 -5
  298. package/dist/vitest/src/index.js +18 -5
  299. package/dist/webdriver/src/driver.d.ts +63 -11
  300. package/dist/webdriver/src/driver.js +57 -6
  301. package/dist/webdriver/src/index.d.ts +8 -0
  302. package/dist/webdriver/src/index.js +1 -0
  303. package/dist/webdriver/src/protocol.d.ts +75 -6
  304. package/dist/webdriver/src/protocol.js +79 -6
  305. package/docs/agents.md +137 -0
  306. package/docs/deploying.md +345 -0
  307. package/docs/dogfooding.md +175 -0
  308. package/docs/extending.md +242 -0
  309. package/docs/integrating.md +485 -0
  310. package/docs/reference/bun.md +141 -0
  311. package/docs/reference/cli.md +146 -0
  312. package/docs/reference/core.md +2426 -0
  313. package/docs/reference/detox.md +150 -0
  314. package/docs/reference/http-api.md +303 -0
  315. package/docs/reference/playwright.md +303 -0
  316. package/docs/reference/reports.md +7098 -0
  317. package/docs/reference/schema.md +1651 -0
  318. package/docs/reference/server.md +685 -0
  319. package/docs/reference/vitest.md +45 -0
  320. package/docs/reference/webdriver.md +330 -0
  321. package/docs/report-backends.md +144 -0
  322. package/docs/report-contract.md +400 -0
  323. package/docs/reporting-issues.md +160 -0
  324. package/docs/server.md +285 -0
  325. package/docs/statecharts.md +641 -0
  326. package/llms-full.txt +3506 -0
  327. package/llms.txt +47 -0
  328. package/package.json +33 -5
  329. package/packages/core/README.md +2 -1
  330. package/packages/reports/README.md +69 -2
  331. package/packages/reports/STORAGE.md +5 -1
  332. package/packages/schema/README.md +14 -0
  333. package/packages/schema/schemas/check-detail.schema.json +1032 -0
  334. package/packages/schema/schemas/checks-result.schema.json +26 -0
  335. package/packages/schema/schemas/command-error.schema.json +35 -0
  336. package/packages/schema/schemas/doctor-result.schema.json +60 -0
  337. package/packages/schema/schemas/effective-delta.schema.json +490 -0
  338. package/packages/schema/schemas/effective-evidence.schema.json +471 -0
  339. package/packages/schema/schemas/evidence-page.schema.json +271 -0
  340. package/packages/schema/schemas/hello-event.schema.json +22 -0
  341. package/packages/schema/schemas/init-plan.schema.json +347 -0
  342. package/packages/schema/schemas/layer-rows.schema.json +199 -0
  343. package/packages/schema/schemas/layer.schema.json +2888 -0
  344. package/packages/schema/schemas/merge-result.schema.json +40 -0
  345. package/packages/schema/schemas/outline.schema.json +62 -0
  346. package/packages/schema/schemas/problem.schema.json +37 -0
  347. package/packages/schema/schemas/publish-checks.schema.json +1413 -0
  348. package/packages/schema/schemas/publish-result.schema.json +168 -0
  349. package/packages/schema/schemas/ready.schema.json +68 -0
  350. package/packages/schema/schemas/report-event.schema.json +100 -0
  351. package/packages/schema/schemas/report-status.schema.json +370 -0
  352. package/packages/schema/schemas/server-config.schema.json +194 -0
  353. package/packages/schema/schemas/server-info.schema.json +114 -0
  354. package/packages/schema/schemas/spec-set.schema.json +103 -0
  355. package/packages/schema/schemas/store-listing.schema.json +29 -0
  356. package/packages/schema/schemas/store-write-result.schema.json +28 -0
  357. package/packages/schema/schemas/stream-result.schema.json +59 -0
  358. package/packages/server/README.md +43 -0
  359. package/packages/server/openapi.json +6699 -0
  360. package/packages/visualizer/README.md +27 -5
  361. package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
  362. package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  363. package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
  364. package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
  365. package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
  366. package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
  367. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
  368. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  369. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
  370. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
  371. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
  372. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
  373. package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
  374. package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  375. package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
  376. package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
  377. package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
  378. package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
  379. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
  380. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  381. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
  382. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
  383. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
  384. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
  385. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
  386. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  387. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
  388. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
  389. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
  390. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
  391. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
  392. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  393. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
  394. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
  395. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
  396. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
  397. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
  398. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  399. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
  400. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
  401. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
  402. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
  403. package/packages/visualizer/scripts/make-fixtures.ts +2 -0
  404. package/packages/visualizer/scripts/publish-data.ts +1 -2
  405. package/packages/visualizer/src/app/AppShell.tsx +9 -2
  406. package/packages/visualizer/src/app/report-sources.tsx +6 -4
  407. package/packages/visualizer/src/app/router.tsx +3 -3
  408. package/packages/visualizer/src/components/Header.module.css +35 -0
  409. package/packages/visualizer/src/components/Header.tsx +41 -4
  410. package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
  411. package/packages/visualizer/src/data/adapters/index.ts +10 -1
  412. package/packages/visualizer/src/evidence/api.ts +14 -2
  413. package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
  414. package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
  415. package/packages/visualizer/src/evidence/check-detail.tsx +38 -4
  416. package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
  417. package/packages/visualizer/src/evidence/evidence.module.css +20 -0
  418. package/packages/visualizer/src/evidence/marks.tsx +2 -0
  419. package/packages/visualizer/src/evidence/overview.tsx +3 -3
  420. package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
  421. package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
  422. package/packages/visualizer/src/find/Finder.tsx +1 -7
  423. package/packages/visualizer/src/find/open.ts +4 -0
  424. package/packages/visualizer/src/live/live-stream.ts +204 -0
  425. package/packages/visualizer/src/runtime.ts +19 -0
  426. package/packages/visualizer/src/state/ui-store.ts +6 -2
  427. package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
  428. package/packages/visualizer/vite/example-reports.ts +475 -0
  429. package/packages/visualizer/vite.config.ts +18 -2
@@ -0,0 +1,272 @@
1
+ import type { LayerHeader, LayerRow } from '@crvouga/atlas/reports/evidence-types';
2
+ import type { CheckDetail, EvidenceHolder } from '@crvouga/atlas/reports/evidence-detail';
3
+ import { roleOfType } from '@crvouga/atlas/reports/evidence-detail';
4
+ import { contentTypeOf } from '@crvouga/atlas/reports/media-types';
5
+ import type { Annotation, Attachment, Attempt, Step } from '@crvouga/atlas/schema';
6
+ import { useQuery } from '@tanstack/react-query';
7
+ import { useMemo, type ReactNode } from 'react';
8
+
9
+ import { reportAssetUrl, reportJson, reportText } from './api';
10
+ import styles from './artifacts.module.css';
11
+ import { duration, plural } from './format';
12
+ import { StatusMark } from './marks';
13
+
14
+ /**
15
+ * How one kind of attachment is shown. The first renderer that matches wins, so a new kind of artifact (a
16
+ * flame graph, a HAR, a coverage map) is one more entry, and an attachment nobody has a renderer for is still
17
+ * listed, with its name, what it is and a way to download it. Nothing here fails on what it does not know.
18
+ */
19
+ export type ArtifactRenderer = {
20
+ readonly id: string;
21
+ readonly matches: (attachment: Attachment, type: string) => boolean;
22
+ /** `url` is where the bytes are, if they are anywhere a browser can reach; `path` is the report API path of a stored file. */
23
+ readonly render: (attachment: Attachment, context: { url: string | null; path: string | null; type: string }) => ReactNode;
24
+ /** Wide renderers take a whole row of the gallery. */
25
+ readonly wide?: boolean;
26
+ };
27
+
28
+ const mediaType = (attachment: Attachment) => (attachment.contentType ?? contentTypeOf(attachment.name)).split(';')[0]!.trim().toLowerCase();
29
+ const label = (attachment: Attachment) => attachment.description ?? attachment.name;
30
+
31
+ /** Only addresses a browser may follow: never `javascript:` or `data:` from a producer. */
32
+ function safeUri(uri: string | undefined): string | null {
33
+ if (!uri) return null;
34
+ try {
35
+ const url = new URL(uri, 'https://atlas.invalid/');
36
+ return url.protocol === 'http:' || url.protocol === 'https:' ? uri : null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function TextValue({ attachment, path }: { attachment: Attachment; path: string | null }) {
43
+ const stored = useQuery({ queryKey: ['artifact-text', path ?? ''], queryFn: ({ signal }) => reportText(path!, signal), enabled: attachment.text === undefined && path !== null, staleTime: Number.POSITIVE_INFINITY, retry: false });
44
+ const raw = attachment.text ?? stored.data;
45
+ if (raw === undefined) return <p className={styles.where}>{stored.isError ? 'Could not be read.' : 'Loading…'}</p>;
46
+ const shown = raw.length > 200_000 ? raw.slice(0, 200_000) : raw;
47
+ const pretty = /json/.test(mediaType(attachment)) ? tryPretty(shown) : shown;
48
+ return (
49
+ <>
50
+ <pre className={styles.text}>{pretty}</pre>
51
+ {shown.length < raw.length && <p className={styles.where}>Showing the first 200 000 characters.</p>}
52
+ </>
53
+ );
54
+ }
55
+
56
+ function tryPretty(text: string) {
57
+ try {
58
+ return JSON.stringify(JSON.parse(text), null, 2);
59
+ } catch {
60
+ return text;
61
+ }
62
+ }
63
+
64
+ /** In order: the first that matches. Add to the front with {@link registerArtifactRenderer}. */
65
+ export const artifactRenderers: ArtifactRenderer[] = [
66
+ { id: 'image', matches: (_, type) => type.startsWith('image/') && type !== 'image/svg+xml', render: (a, { url }) => (url ? <a href={url} target="_blank" rel="noreferrer"><img src={url} alt={label(a)} loading="lazy" width={a.width} height={a.height} /></a> : null) },
67
+ // SVG can carry script: shown as a picture only, never inlined.
68
+ { id: 'svg', matches: (_, type) => type === 'image/svg+xml', render: (a, { url }) => (url ? <img src={url} alt={label(a)} loading="lazy" /> : null) },
69
+ { id: 'video', wide: true, matches: (_, type) => type.startsWith('video/'), render: (a, { url }) => (url ? <video src={url} controls preload="metadata" aria-label={label(a)} /> : null) },
70
+ { id: 'audio', matches: (_, type) => type.startsWith('audio/'), render: (a, { url }) => (url ? <audio src={url} controls preload="none" aria-label={label(a)} /> : null) },
71
+ // A page snapshot runs nothing: the frame has no permissions and the file is served sandboxed.
72
+ { id: 'snapshot', wide: true, matches: (_, type) => type === 'text/html' || type === 'application/xhtml+xml', render: (a, { url }) => (url ? <iframe sandbox="" src={url} title={label(a)} loading="lazy" /> : null) },
73
+ { id: 'pdf', wide: true, matches: (_, type) => type === 'application/pdf', render: (a, { url }) => (url ? <iframe sandbox="" src={url} title={label(a)} loading="lazy" /> : null) },
74
+ { id: 'text', wide: true, matches: (a, type) => a.text !== undefined || type.startsWith('text/') || /(json|xml|yaml|x-ndjson|x-diff)/.test(type), render: (a, { path }) => <TextValue attachment={a} path={path} /> }
75
+ ];
76
+
77
+ export function registerArtifactRenderer(renderer: ArtifactRenderer) {
78
+ artifactRenderers.unshift(renderer);
79
+ }
80
+
81
+ function sizeOf(bytes: number | undefined) {
82
+ if (bytes === undefined) return '';
83
+ if (bytes < 1024) return `${bytes} B`;
84
+ return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(0)} KiB` : `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
85
+ }
86
+
87
+ function Artifact({ attachment, layer }: { attachment: Attachment; layer: string }) {
88
+ const type = mediaType(attachment);
89
+ const path = attachment.bundleKey ? `/layers/${encodeURIComponent(layer)}/blobs/${attachment.bundleKey.split('/').map(encodeURIComponent).join('/')}` : null;
90
+ const url = path ? reportAssetUrl(path) : safeUri(attachment.uri);
91
+ const renderer = artifactRenderers.find((candidate) => candidate.matches(attachment, type));
92
+ const body = renderer?.render(attachment, { url, path, type });
93
+ return (
94
+ <li className={styles.figure} data-wide={renderer?.wide || undefined} data-role={attachment.role ?? roleOfType(type)}>
95
+ {body}
96
+ <figcaption>
97
+ <strong>{attachment.name}</strong>
98
+ {attachment.role && <span className={styles.role}>{attachment.role}</span>}
99
+ <span>{[type, sizeOf(attachment.bytes), attachment.durationMs ? duration(attachment.durationMs) : ''].filter(Boolean).join(' · ')}</span>
100
+ {url && <a href={url} download={attachment.name} target="_blank" rel="noreferrer">{body ? 'Download' : 'Open'}</a>}
101
+ {!url && attachment.text === undefined && <span>No file to show</span>}
102
+ </figcaption>
103
+ </li>
104
+ );
105
+ }
106
+
107
+ export function Attachments({ attachments, layer }: { attachments: readonly Attachment[]; layer: string }) {
108
+ if (!attachments.length) return null;
109
+ return (
110
+ <section aria-label="Attachments">
111
+ <h4>{plural(attachments.length, 'attachment', 'attachments')}</h4>
112
+ <ul className={styles.gallery}>
113
+ {attachments.map((attachment, index) => <Artifact key={`${attachment.id ?? attachment.name}:${index}`} attachment={attachment} layer={layer} />)}
114
+ </ul>
115
+ </section>
116
+ );
117
+ }
118
+
119
+ const STEP_STATUS: Record<string, 'passed' | 'failed' | 'running' | 'not-run'> = { passed: 'passed', pass: 'passed', ok: 'passed', failed: 'failed', fail: 'failed', error: 'failed', running: 'running', skipped: 'not-run', 'not-run': 'not-run', 'not-reached': 'not-run', pending: 'not-run' };
120
+
121
+ /** The timeline: ordered by when each step started, nested by `parentId`. A step whose parent is missing is shown at the top. */
122
+ export function Timeline({ steps, layer }: { steps: readonly Step[]; layer: string }) {
123
+ const tree = useMemo(() => {
124
+ const ids = new Set(steps.flatMap((step) => (step.id ? [step.id] : [])));
125
+ const children = new Map<string | null, Step[]>();
126
+ for (const step of steps) {
127
+ const parent = step.parentId && ids.has(step.parentId) ? step.parentId : null;
128
+ children.set(parent, [...(children.get(parent) ?? []), step]);
129
+ }
130
+ for (const list of children.values()) list.sort((a, b) => (a.offsetMs ?? 0) - (b.offsetMs ?? 0));
131
+ return children;
132
+ }, [steps]);
133
+ const render = (parent: string | null): ReactNode => (
134
+ <ol>
135
+ {(tree.get(parent) ?? []).map((step, index) => {
136
+ const status = STEP_STATUS[(step.status ?? '').toLowerCase()];
137
+ return (
138
+ <li key={`${step.id ?? step.name}:${index}`} className={styles.step} data-status={status ?? step.status ?? 'none'}>
139
+ <div className={styles.stepRow}>
140
+ <StatusMark status={status ?? 'none'} size={14} />
141
+ <span className={styles.stepName}>{step.name}</span>
142
+ {step.kind && <span className={styles.stepKind}>{step.kind}</span>}
143
+ {step.status && !status && <span className={styles.stepKind}>{step.status}</span>}
144
+ <span className={styles.spacer} />
145
+ <span className={styles.stepTime}>{[step.offsetMs !== undefined ? `+${duration(step.offsetMs)}` : '', duration(step.durationMs)].filter(Boolean).join(' · ')}</span>
146
+ </div>
147
+ {step.detail && <p className={styles.stepDetail}>{step.detail}</p>}
148
+ {step.annotations && <Notes notes={step.annotations} />}
149
+ {step.attachments && <Attachments attachments={step.attachments} layer={layer} />}
150
+ {step.id && tree.has(step.id) && render(step.id)}
151
+ </li>
152
+ );
153
+ })}
154
+ </ol>
155
+ );
156
+ return (
157
+ <section aria-label="Timeline" className={styles.timeline}>
158
+ <h4>{plural(steps.length, 'step', 'steps')}</h4>
159
+ {render(null)}
160
+ </section>
161
+ );
162
+ }
163
+
164
+ export function Notes({ notes }: { notes: readonly Annotation[] }) {
165
+ if (!notes.length) return null;
166
+ return (
167
+ <ul className={styles.notes} aria-label="Annotations">
168
+ {notes.map((note, index) => (
169
+ <li key={index} className={styles.note}>
170
+ <span className={styles.level} data-level={note.level ?? 'info'}>{note.level && ['info', 'warning', 'error'].includes(note.level) ? note.level : note.type}</span>
171
+ <span>
172
+ {note.message}
173
+ {note.source?.file && <span className={styles.where}> {note.source.file}{note.source.line ? `:${note.source.line}` : ''}</span>}
174
+ {note.level && !['info', 'warning', 'error'].includes(note.level) && <span className={styles.where}> ({note.level})</span>}
175
+ </span>
176
+ </li>
177
+ ))}
178
+ </ul>
179
+ );
180
+ }
181
+
182
+ function Tries({ attempts, layer }: { attempts: readonly Attempt[]; layer: string }) {
183
+ if (!attempts.length) return null;
184
+ return (
185
+ <section aria-label="Tries">
186
+ <h4>{plural(attempts.length, 'try', 'tries')}</h4>
187
+ <ol className={styles.timeline}>
188
+ {attempts.map((attempt) => (
189
+ <li key={attempt.index} className={styles.step}>
190
+ <div className={styles.stepRow}>
191
+ <StatusMark status={attempt.status} size={14} />
192
+ <span className={styles.stepName}>Try {attempt.index + 1}</span>
193
+ <span className={styles.spacer} />
194
+ <span className={styles.stepTime}>{duration(attempt.durationMs)}</span>
195
+ </div>
196
+ {attempt.detail && <p className={styles.stepDetail}>{attempt.detail}</p>}
197
+ {attempt.attachments && <Attachments attachments={attempt.attachments} layer={layer} />}
198
+ </li>
199
+ ))}
200
+ </ol>
201
+ </section>
202
+ );
203
+ }
204
+
205
+ /** What is not known: kept, and shown as it is, so nothing a tool recorded is out of reach. */
206
+ function Raw({ extensions }: { extensions: Readonly<Record<string, unknown>> }) {
207
+ const names = Object.keys(extensions);
208
+ if (!names.length) return null;
209
+ return (
210
+ <details className={styles.raw}>
211
+ <summary>{plural(names.length, 'extension', 'extensions')}: {names.join(', ')}</summary>
212
+ <pre className={styles.text}>{JSON.stringify(extensions, null, 2)}</pre>
213
+ </details>
214
+ );
215
+ }
216
+
217
+ function Facts({ holder }: { holder: EvidenceHolder }) {
218
+ const labels = Object.entries(holder.labels ?? {});
219
+ const tags = holder.tags ?? [];
220
+ const links = (holder.links ?? []).flatMap((link) => { const url = safeUri(link.url); return url ? [{ ...link, url }] : []; });
221
+ if (!labels.length && !tags.length && !links.length) return null;
222
+ return (
223
+ <div>
224
+ <ul className={styles.chips} aria-label="Facts">
225
+ {tags.map((tag) => <li key={`t:${tag}`} className={styles.chip}>{tag}</li>)}
226
+ {labels.map(([name, value]) => <li key={`l:${name}`} className={styles.chip}>{name}<b>{value}</b></li>)}
227
+ {links.map((link) => <li key={`k:${link.url}`} className={styles.chip}><a href={link.url} target="_blank" rel="noreferrer">{link.name ?? link.rel ?? link.url}</a></li>)}
228
+ </ul>
229
+ </div>
230
+ );
231
+ }
232
+
233
+ function Evidence({ holder, layer, steps }: { holder: EvidenceHolder; layer: string; steps?: boolean }) {
234
+ return (
235
+ <>
236
+ <Facts holder={holder} />
237
+ {holder.annotations && <Notes notes={holder.annotations} />}
238
+ {steps && holder.steps && holder.steps.length > 0 && <Timeline steps={holder.steps} layer={layer} />}
239
+ {holder.attempts && holder.attempts.length > 1 && <Tries attempts={holder.attempts} layer={layer} />}
240
+ {holder.attachments && <Attachments attachments={holder.attachments} layer={layer} />}
241
+ {holder.extensions && <Raw extensions={holder.extensions} />}
242
+ </>
243
+ );
244
+ }
245
+
246
+ /** Everything one result carries beyond its outcome, read when it is opened, and what the run around it carried. */
247
+ export function ResultEvidence({ layer, row }: { layer: LayerHeader; row: LayerRow }) {
248
+ const wanted = row.evidence !== undefined || layer.evidence !== undefined;
249
+ const detail = useQuery({
250
+ queryKey: ['evidence-detail', layer.backend, layer.id, row.kind, row.id],
251
+ queryFn: ({ signal }) => reportJson<CheckDetail>(`/evidence/detail/${encodeURIComponent(layer.backend)}/${encodeURIComponent(layer.id)}?check=${encodeURIComponent(row.id)}&kind=${encodeURIComponent(row.kind)}`, signal),
252
+ enabled: wanted && /^[0-9a-f]{64}$/.test(layer.id),
253
+ staleTime: Number.POSITIVE_INFINITY,
254
+ retry: false
255
+ });
256
+ if (!wanted) return null;
257
+ if (detail.isLoading) return <p className={styles.where} role="status">Loading what this result carries…</p>;
258
+ if (detail.isError || !detail.data) return <p className={styles.where} role="alert">What this result carries could not be read.</p>;
259
+ const { check, run } = detail.data;
260
+ const hasRun = Object.values(run).some((value) => (Array.isArray(value) ? value.length > 0 : value && typeof value === 'object' ? Object.keys(value).length > 0 : false));
261
+ return (
262
+ <div className={styles.panel} aria-label="Evidence">
263
+ <Evidence holder={check} layer={layer.id} steps />
264
+ {hasRun && (
265
+ <section aria-label="From the run">
266
+ <h4>From the run</h4>
267
+ <Evidence holder={run} layer={layer.id} />
268
+ </section>
269
+ )}
270
+ </div>
271
+ );
272
+ }
@@ -1,11 +1,14 @@
1
1
  import { recordingPaths } from '@crvouga/atlas/reports/evidence-types';
2
2
  import { checkHistory, type CheckSlot, type HistoryEntry } from '@crvouga/atlas/reports/evidence-view';
3
+ import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
4
+ import type { EvidenceResult } from '@crvouga/atlas/reports/layer-merge';
3
5
  import { flattenFrames } from '@crvouga/atlas/reports/frames';
4
6
  import type { ReportStep, RunReport } from '@crvouga/atlas/reports/report-types';
5
7
  import { useQuery } from '@tanstack/react-query';
6
8
  import { useMemo, useState, type ReactNode } from 'react';
7
9
 
8
10
  import { reportJson } from './api';
11
+ import { ResultEvidence } from './artifacts';
9
12
  import { useEvidence } from './evidence-context';
10
13
  import styles from './evidence.module.css';
11
14
  import { ago, duration, plural, statusWord, useNow } from './format';
@@ -34,18 +37,48 @@ export function useRecordedReport(entry: HistoryEntry | null, enabled = true) {
34
37
  }
35
38
 
36
39
  /** One check in a list: its outcome at a glance, and everything recorded for it when opened. */
40
+ const ENV_RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 } as const;
41
+
42
+ /** One mark per client when the same check was recorded on more than one. */
43
+ function environmentMarks(slot: CheckSlot) {
44
+ const seen = new Map<string, CheckSlot['status']>();
45
+ for (const outcome of slot.outcomes) {
46
+ const id = outcome.layer.environment ?? '';
47
+ const prior = seen.get(id);
48
+ if (prior === undefined || ENV_RANK[outcome.row.status] > ENV_RANK[prior]) seen.set(id, outcome.row.status);
49
+ }
50
+ if (seen.size < 2) return [];
51
+ return [...seen].map(([id, status]) => ({ id, status }));
52
+ }
53
+
54
+ function outcomeName(outcome: EvidenceResult) {
55
+ const environment = outcome.layer.environment ? environmentLabel(outcome.layer.environment) : '';
56
+ const parameters = Object.entries(outcome.parameters).map(([name, value]) => `${name}: ${value}`).join(', ');
57
+ return [environment, parameters].filter(Boolean).join(' · ') || outcome.layer.producer;
58
+ }
59
+
37
60
  export function ResultRow({ label, slot, open, onToggle, children }: { label: string; slot: CheckSlot | null; open: boolean; onToggle: () => void; children: ReactNode }) {
38
61
  const now = useNow();
39
62
  const newest = slot?.outcomes[0];
63
+ const marks = slot ? environmentMarks(slot) : [];
40
64
  const failure = slot?.status === 'failed' ? slot.outcomes.find((outcome) => outcome.row.status === 'failed')?.row.detail.split('\n')[0] : '';
41
65
  return (
42
66
  <li className={styles.result} data-status={slot?.status ?? 'none'} data-open={open || undefined}>
43
67
  <button type="button" className={styles.resultHead} aria-expanded={open} onClick={onToggle}>
44
68
  <StatusMark status={slot?.status ?? 'none'} />
45
- <span className={styles.resultName}>{label}</span>
69
+ <span className={styles.resultName}>
70
+ {label}
71
+ {marks.length > 0 && (
72
+ <span className={styles.envMarks}>
73
+ {marks.map((mark) => (
74
+ <span key={mark.id || 'unspecified'} data-status={mark.status}>{environmentLabel(mark.id || null)}</span>
75
+ ))}
76
+ </span>
77
+ )}
78
+ </span>
46
79
  {slot?.live && <span className={styles.liveTag}>Running now</span>}
47
80
  <span className={styles.resultAside}>
48
- {newest ? [duration(newest.row.durationMs), slot.live ? '' : ago(slot.latestAtMs, now)].filter(Boolean).join(' · ') : 'No result'}
81
+ {newest ? [newest.row.evidence?.attachments ? plural(newest.row.evidence.attachments, 'attachment', 'attachments') : '', duration(newest.row.durationMs), slot.live ? '' : ago(slot.latestAtMs, now)].filter(Boolean).join(' · ') : 'No result'}
49
82
  </span>
50
83
  <span className={styles.chevron} aria-hidden="true" />
51
84
  </button>
@@ -185,11 +218,11 @@ export function CheckDetail({ slot, layerId, select, reads, written }: Props) {
185
218
  return (
186
219
  <div className={styles.detail}>
187
220
  {slot.outcomes.length > 1 && !earlier && (
188
- <div className={styles.variants} role="group" aria-label="Parameter sets">
221
+ <div className={styles.variants} role="group" aria-label="Environments">
189
222
  {slot.outcomes.map((outcome, index) => (
190
223
  <button key={outcome.key} type="button" aria-pressed={index === variant} onClick={() => setVariant(index)}>
191
224
  <StatusMark status={outcome.row.status} size={12} />
192
- {Object.entries(outcome.layer.parameters).map(([name, value]) => `${name}: ${value}`).join(', ') || outcome.layer.producer}
225
+ {outcomeName(outcome)}
193
226
  </button>
194
227
  ))}
195
228
  </div>
@@ -221,6 +254,7 @@ export function CheckDetail({ slot, layerId, select, reads, written }: Props) {
221
254
  </p>
222
255
  );
223
256
  }))}
257
+ <ResultEvidence layer={chosen.layer} row={chosen.row} />
224
258
  {(moments > 0 || chosen.layer.runUrl) && (
225
259
  <p className={styles.actionsLine}>
226
260
  {moments > 0 && (
@@ -1,20 +1,29 @@
1
1
  import type { ActiveRun, BackendInfo, EvidencePage, LayerEvidence, LayerHeader, LayerRows } from '@crvouga/atlas/reports/evidence-types';
2
+ import { applyEffectiveDelta, isEffectiveDelta, type EffectiveDelta } from '@crvouga/atlas/reports/effective-delta';
3
+ import type { EffectiveEvidence } from '@crvouga/atlas/reports/report-index';
2
4
  import { contributes, evidenceView, type EvidenceView, type SourceSelection } from '@crvouga/atlas/reports/evidence-view';
3
5
  import type { SpecEntry } from '@crvouga/atlas/reports/explorer-types';
4
6
  import { combineEvidence, distinctHeaders } from '@crvouga/atlas/reports/layer-merge';
5
7
  import type { ReportOutline } from '@crvouga/atlas/reports/layer-types';
6
8
  import { SpecIndexSchema } from '@crvouga/atlas/schema';
7
- import { useInfiniteQuery, useQueries, useQuery } from '@tanstack/react-query';
9
+ import { useInfiniteQuery, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
8
10
  import { createContext, useContext, useEffect, useMemo, useRef, type ReactNode } from 'react';
9
11
 
10
12
  import { keys, specKey, useAdapter } from '../data/queries';
13
+ import { eventsAddress, listenLive, throttled, useLiveStatus } from '../live/live-stream';
11
14
  import { fileName, folderOf, kindOfFile, outlinePath } from '../spec-kind';
12
15
  import { useUiStore } from '../state/ui-store';
13
- import { reportJson, staticEvidence } from './api';
16
+ import { effectiveEvidence, reportJson, staticEvidence } from './api';
14
17
 
15
18
  /** How often the layer list is asked for while a spec run is writing results, and while nothing is. */
16
19
  const LIVE_POLL_MS = 2_000;
17
20
  const IDLE_POLL_MS = 15_000;
21
+ /** The effective report is revalidated with its ETag, so asking often costs one small answer. */
22
+ const EFFECTIVE_POLL_MS = 5_000;
23
+ /** While the server's stream is open it says when to read again; this is only the safety net behind it. */
24
+ const LIVE_SAFETY_POLL_MS = 60_000;
25
+ /** At most this often is the report read again because the server said it changed. */
26
+ const LIVE_REREAD_MS = 250;
18
27
 
19
28
  export type EvidenceState = {
20
29
  /** The layer list has been read at least once. */
@@ -63,26 +72,76 @@ function outlineOf(files: readonly string[]): ReportOutline {
63
72
  return { specs, sha: null, changedFiles: [] };
64
73
  }
65
74
 
75
+ const NONE: readonly LayerEvidence[] = [];
76
+
77
+ function hasExclusions(excluded: SourceSelection): boolean {
78
+ return Object.values(excluded).some((list) => list.length > 0);
79
+ }
80
+
66
81
  export function EvidenceProvider({ children }: { children: ReactNode }) {
67
82
  const adapter = useAdapter();
68
83
  const source = specKey(adapter);
69
84
  const excluded = useUiStore((s) => s.excluded);
70
85
  const setExcluded = useUiStore((s) => s.setExcluded);
71
86
 
87
+ const client = useQueryClient();
88
+ const stream = useLiveStatus();
89
+ const streaming = stream === 'live';
90
+ // The server says when results arrive: read the report again then, not on a timer.
91
+ useEffect(() => {
92
+ const url = eventsAddress();
93
+ if (!url) return;
94
+ // A burst of results (a CI run finishing, many shards at once) is read once, not once for each.
95
+ const reread = throttled(() => {
96
+ void client.invalidateQueries({ queryKey: ['evidence-effective'] });
97
+ void client.invalidateQueries({ queryKey: ['evidence-index'] });
98
+ }, LIVE_REREAD_MS);
99
+ const reoutline = throttled(() => {
100
+ void client.invalidateQueries({ queryKey: ['evidence-outline'] });
101
+ void client.invalidateQueries({ queryKey: ['evidence-effective'] });
102
+ }, LIVE_REREAD_MS);
103
+ const stop = listenLive(url, (event) => {
104
+ if (event.type === 'change' && event.change.scope === 'specs') return reoutline();
105
+ if (event.type !== 'report' && event.type !== 'open') return;
106
+ reread();
107
+ if (event.type === 'open') reoutline();
108
+ });
109
+ return () => { stop(); reread.cancel(); reoutline.cancel(); };
110
+ }, [client]);
111
+
72
112
  const specIndex = useQuery({ queryKey: keys.specIndex(source), queryFn: adapter.specIndex, retry: false });
73
113
  const files = useMemo(() => {
74
114
  const parsed = SpecIndexSchema.safeParse(specIndex.data);
75
115
  return parsed.success ? parsed.data.files : [];
76
116
  }, [specIndex.data]);
77
117
 
118
+ // The node answers with only what the report shows, in one request. Leaving a source out needs the
119
+ // history it hides, so that one case reads every layer, as a store without a node always does.
120
+ const effective = effectiveEvidence() && !staticEvidence && !hasExclusions(excluded);
121
+ const overview = useQuery({
122
+ queryKey: ['evidence-effective'],
123
+ // Holding a report, ask only for what changed since it; the server answers with all of it when it cannot.
124
+ queryFn: async ({ signal }) => {
125
+ const held = client.getQueryData<EffectiveEvidence>(['evidence-effective']);
126
+ const answer = await reportJson<EffectiveEvidence | EffectiveDelta>(held ? `/evidence/effective?since=${encodeURIComponent(held.version)}` : '/evidence/effective', signal);
127
+ if (!isEffectiveDelta(answer)) return answer;
128
+ return (held && applyEffectiveDelta(held, answer)) ?? reportJson<EffectiveEvidence>('/evidence/effective', signal);
129
+ },
130
+ enabled: effective,
131
+ staleTime: 0,
132
+ retry: false,
133
+ refetchInterval: streaming ? LIVE_SAFETY_POLL_MS : EFFECTIVE_POLL_MS
134
+ });
135
+
78
136
  const index = useInfiniteQuery({
79
137
  queryKey: ['evidence-index'],
138
+ enabled: !effective,
80
139
  initialPageParam: null as string | null,
81
140
  queryFn: ({ pageParam, signal }) => reportJson<EvidencePage>(`/evidence${pageParam ? `?cursor=${encodeURIComponent(pageParam)}` : ''}`, signal),
82
141
  getNextPageParam: (page) => page.cursor,
83
142
  staleTime: 0,
84
143
  retry: false,
85
- refetchInterval: (query) => (staticEvidence ? false : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
144
+ refetchInterval: (query) => (staticEvidence ? false : streaming ? LIVE_SAFETY_POLL_MS : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
86
145
  });
87
146
  // Show the newest layers at once, then read the older pages behind them.
88
147
  const { hasNextPage, isFetching, isError, fetchNextPage } = index;
@@ -98,7 +157,7 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
98
157
  const serverOutline = useQuery({ queryKey: ['evidence-outline'], queryFn: ({ signal }) => reportJson<ReportOutline>('/outline', signal), staleTime: 30_000, retry: false });
99
158
  const outline = useMemo(() => serverOutline.data ?? outlineOf(files), [serverOutline.data, files]);
100
159
 
101
- const wanted = useMemo(() => layers.filter((layer) => contributes(layer, excluded)), [layers, excluded]);
160
+ const wanted = useMemo(() => (effective ? [] : layers.filter((layer) => contributes(layer, excluded))), [layers, excluded, effective]);
102
161
  const rows = useQueries({
103
162
  queries: wanted.map((layer) => ({
104
163
  queryKey: rowsKey(layer),
@@ -126,23 +185,26 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
126
185
  // `stamp` changes exactly when a layer's rows do.
127
186
  }, [wanted, rows.stamp]);
128
187
 
129
- const view = useMemo(() => evidenceView(combineEvidence(outline, evidence)), [outline, evidence]);
188
+ const shown = effective ? overview.data?.layers ?? NONE : evidence;
189
+ const view = useMemo(() => evidenceView(combineEvidence(outline, shown)), [outline, shown]);
130
190
  const titles = useMemo(() => new Map(outline.specs.map((spec) => [spec.path, spec.title])), [outline]);
191
+ const effectiveHeaders = useMemo(() => (overview.data?.layers ?? NONE).map((entry) => entry.layer), [overview.data]);
131
192
 
132
193
  const state: EvidenceState = {
133
- ready: pages !== undefined,
134
- unavailable: pages === undefined && index.error ? index.error.message : null,
194
+ ready: effective ? overview.data !== undefined : pages !== undefined,
195
+ unavailable: effective ? (overview.data === undefined && overview.error ? overview.error.message : null) : pages === undefined && index.error ? index.error.message : null,
135
196
  retry: () => {
197
+ if (effective) { void Promise.all([overview.refetch(), serverOutline.refetch()]); return; }
136
198
  void reportJson<EvidencePage>('/evidence?retry=1')
137
199
  .catch(() => null)
138
200
  .then(() => Promise.all([index.refetch(), serverOutline.refetch()]));
139
201
  },
140
202
  pending: wanted.length - evidence.length,
141
- layers,
142
- evidence,
143
- backends: first?.backends ?? [],
144
- active: first?.active ?? [],
145
- notices,
203
+ layers: effective ? effectiveHeaders : layers,
204
+ evidence: shown,
205
+ backends: effective ? overview.data?.backends ?? [] : first?.backends ?? [],
206
+ active: effective ? [] : first?.active ?? [],
207
+ notices: effective ? overview.data?.notices ?? [] : notices,
146
208
  view,
147
209
  files,
148
210
  filesPending: specIndex.isPending,
@@ -455,11 +455,31 @@
455
455
  }
456
456
 
457
457
  .resultName {
458
+ display: flex;
458
459
  flex: 1;
460
+ flex-direction: column;
459
461
  min-width: 0;
460
462
  font-weight: 500;
461
463
  }
462
464
 
465
+ .envMarks {
466
+ display: flex;
467
+ flex-wrap: wrap;
468
+ gap: 8px;
469
+ margin-top: 2px;
470
+ color: var(--ink-3);
471
+ font-size: 0.75rem;
472
+ font-weight: 400;
473
+ }
474
+
475
+ .envMarks span[data-status='passed'] {
476
+ color: var(--passed-ink);
477
+ }
478
+
479
+ .envMarks span[data-status='failed'] {
480
+ color: var(--failed-ink);
481
+ }
482
+
463
483
  .result[data-status='none'] .resultName {
464
484
  color: var(--ink-3);
465
485
  font-weight: 400;
@@ -1,5 +1,6 @@
1
1
  import type { BackendInfo, LayerHeader, StatusCounts } from '@crvouga/atlas/reports/evidence-types';
2
2
  import type { SpecStatus, SpecSummary } from '@crvouga/atlas/reports/evidence-view';
3
+ import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
3
4
  import { useState, type ReactNode } from 'react';
4
5
 
5
6
  import styles from './evidence.module.css';
@@ -89,6 +90,7 @@ export function Origin({ layer, backends, now }: { layer: LayerHeader; backends:
89
90
  {layer.live && <span className={styles.liveTag}>Running now</span>}
90
91
  <span>{backendLabel(backends, layer.backend)}</span>
91
92
  {layer.branch && <span>{layer.branch}</span>}
93
+ {layer.environment && <span>{environmentLabel(layer.environment)}</span>}
92
94
  {sha && <code>{sha}</code>}
93
95
  {layer.freshness === 'modified' && <span>with local changes</span>}
94
96
  {layer.freshness === 'unknown' && <span>origin not captured</span>}
@@ -67,7 +67,7 @@ export function Overview({ kind }: { kind: SpecKind }) {
67
67
  </p>
68
68
  ))}
69
69
  {!evidence.ready && !evidence.unavailable && <p className={styles.aside} role="status">Loading recorded results…</p>}
70
- {evidence.ready && evidence.pending > 0 && <p className={styles.aside} role="status">Reading {plural(evidence.pending, 'more run', 'more runs')}…</p>}
70
+ {evidence.ready && evidence.pending > 0 && <p className={styles.aside} role="status">Reading {plural(evidence.pending, 'more layer', 'more layers')}…</p>}
71
71
 
72
72
  {evidence.active.length > 0 && (
73
73
  <section className={styles.block}>
@@ -111,13 +111,13 @@ export function Overview({ kind }: { kind: SpecKind }) {
111
111
  <p className={styles.aside}>
112
112
  {evidence.layers.length === 0
113
113
  ? `Nothing has been recorded yet. Results appear here as a spec run finishes each test.`
114
- : `None of the ${plural(evidence.layers.length, 'run', 'runs')} in the combined report checked a ${kind.one}.`}
114
+ : `None of the ${plural(evidence.layers.length, 'layer', 'layers')} in the combined report checked a ${kind.one}.`}
115
115
  </p>
116
116
  )}
117
117
 
118
118
  {evidence.ready && (
119
119
  <p className={styles.footLine}>
120
- {plural(evidence.layers.length, 'run', 'runs')} from {evidence.backends.filter((backend) => backend.available).map((backend) => backendLabel(evidence.backends, backend.id)).join(' and ') || 'no report store'}.{' '}
120
+ {plural(evidence.layers.length, 'layer', 'layers')} from {evidence.backends.filter((backend) => backend.available).map((backend) => backendLabel(evidence.backends, backend.id)).join(' and ') || 'no report store'}.{' '}
121
121
  <button type="button" className={styles.link} onClick={() => setSourcesOpen(true)}>Choose sources</button>
122
122
  {empty > 0 && (
123
123
  <>