@crvouga/atlas 0.4.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 (379) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +178 -617
  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 +35 -5
  94. package/dist/reports/src/evidence-live.js +18 -2
  95. package/dist/reports/src/evidence-types.d.ts +68 -1
  96. package/dist/reports/src/evidence-types.js +5 -1
  97. package/dist/reports/src/evidence-view.d.ts +43 -4
  98. package/dist/reports/src/evidence-view.js +13 -2
  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 +35 -5
  134. package/dist/reports/src/layer-axes.js +25 -5
  135. package/dist/reports/src/layer-catalog.d.ts +28 -1
  136. package/dist/reports/src/layer-catalog.js +18 -1
  137. package/dist/reports/src/layer-merge.d.ts +98 -9
  138. package/dist/reports/src/layer-merge.js +147 -31
  139. package/dist/reports/src/layer-source.d.ts +18 -0
  140. package/dist/reports/src/layer-source.js +11 -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 +32 -1
  144. package/dist/reports/src/layers-route.d.ts +47 -4
  145. package/dist/reports/src/layers-route.js +86 -7
  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 +26 -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 +42 -9
  175. package/dist/reports/src/publish.js +36 -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 +37 -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 +32 -4
  329. package/packages/core/README.md +2 -1
  330. package/packages/reports/README.md +65 -0
  331. package/packages/reports/STORAGE.md +4 -0
  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 +21 -0
  361. package/packages/visualizer/src/app/AppShell.tsx +9 -2
  362. package/packages/visualizer/src/app/report-sources.tsx +6 -4
  363. package/packages/visualizer/src/app/router.tsx +3 -3
  364. package/packages/visualizer/src/components/Header.module.css +35 -0
  365. package/packages/visualizer/src/components/Header.tsx +40 -3
  366. package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
  367. package/packages/visualizer/src/data/adapters/index.ts +10 -1
  368. package/packages/visualizer/src/evidence/api.ts +14 -2
  369. package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
  370. package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
  371. package/packages/visualizer/src/evidence/check-detail.tsx +3 -1
  372. package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
  373. package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
  374. package/packages/visualizer/src/find/Finder.tsx +1 -7
  375. package/packages/visualizer/src/find/open.ts +4 -0
  376. package/packages/visualizer/src/live/live-stream.ts +204 -0
  377. package/packages/visualizer/src/runtime.ts +19 -0
  378. package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
  379. package/packages/visualizer/vite.config.ts +11 -3
@@ -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
+ }
@@ -8,6 +8,7 @@ import { useQuery } from '@tanstack/react-query';
8
8
  import { useMemo, useState, type ReactNode } from 'react';
9
9
 
10
10
  import { reportJson } from './api';
11
+ import { ResultEvidence } from './artifacts';
11
12
  import { useEvidence } from './evidence-context';
12
13
  import styles from './evidence.module.css';
13
14
  import { ago, duration, plural, statusWord, useNow } from './format';
@@ -77,7 +78,7 @@ export function ResultRow({ label, slot, open, onToggle, children }: { label: st
77
78
  </span>
78
79
  {slot?.live && <span className={styles.liveTag}>Running now</span>}
79
80
  <span className={styles.resultAside}>
80
- {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'}
81
82
  </span>
82
83
  <span className={styles.chevron} aria-hidden="true" />
83
84
  </button>
@@ -253,6 +254,7 @@ export function CheckDetail({ slot, layerId, select, reads, written }: Props) {
253
254
  </p>
254
255
  );
255
256
  }))}
257
+ <ResultEvidence layer={chosen.layer} row={chosen.row} />
256
258
  {(moments > 0 || chosen.layer.runUrl) && (
257
259
  <p className={styles.actionsLine}>
258
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,
@@ -1,16 +1,23 @@
1
1
  import { useQuery } from '@tanstack/react-query';
2
2
  import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
3
+ import { lazy, Suspense } from 'react';
3
4
 
4
5
  import { keys, specKey, useAdapter } from '../data/queries';
5
6
  import { outlinePath, specFile, specKind } from '../spec-kind';
6
7
  import type { SelectCheck } from './check-detail';
7
- import { DocumentPage } from './document-page';
8
8
  import { useEvidence } from './evidence-context';
9
9
  import styles from './evidence.module.css';
10
- import { FeaturePage } from './feature-page';
11
- import { ModelPage } from './model-page';
12
10
  import { Overview } from './overview';
13
- import { RecordingPage } from './recording-page';
11
+
12
+ // Each kind of spec has a page with a parser of its own (Gherkin, Quint, DMN); a page loads only the one it shows.
13
+ const DocumentPage = lazy(() => import('./document-page').then((m) => ({ default: m.DocumentPage })));
14
+ const FeaturePage = lazy(() => import('./feature-page').then((m) => ({ default: m.FeaturePage })));
15
+ const ModelPage = lazy(() => import('./model-page').then((m) => ({ default: m.ModelPage })));
16
+ const RecordingPage = lazy(() => import('./recording-page').then((m) => ({ default: m.RecordingPage })));
17
+
18
+ function Loading({ children }: { children: React.ReactNode }) {
19
+ return <Suspense fallback={<article className={styles.page}><p className={styles.aside} role="status">Loading…</p></article>}>{children}</Suspense>;
20
+ }
14
21
 
15
22
  /** One spec tab: its overview, one of its documents, or a recording opened from that document. */
16
23
  export function SpecPage() {
@@ -63,15 +70,17 @@ export function SpecPage() {
63
70
  const spec = `${evidence.titleOf(file)}: `;
64
71
  const label = slot ? (slot.title.startsWith(spec) ? slot.title.slice(spec.length) : slot.title) : 'Recorded test';
65
72
  return (
66
- <RecordingPage
67
- slot={slot}
68
- layerId={search.layer}
69
- label={label}
70
- back={evidence.titleOf(file)}
71
- moment={search.moment ?? null}
72
- onMoment={(moment, replace) => void navigate({ search: (prev) => ({ ...prev, moment }), replace })}
73
- onClose={() => select({ view: null })}
74
- />
73
+ <Loading>
74
+ <RecordingPage
75
+ slot={slot}
76
+ layerId={search.layer}
77
+ label={label}
78
+ back={evidence.titleOf(file)}
79
+ moment={search.moment ?? null}
80
+ onMoment={(moment, replace) => void navigate({ search: (prev) => ({ ...prev, moment }), replace })}
81
+ onClose={() => select({ view: null })}
82
+ />
83
+ </Loading>
75
84
  );
76
85
  }
77
86
  if (text.isPending) return <article className={styles.page}><p className={styles.aside} role="status">Loading the {kind.one}…</p></article>;
@@ -86,7 +95,5 @@ export function SpecPage() {
86
95
  );
87
96
  }
88
97
  const page = { file, text: text.data, kind, entry, selection: { check: search.check, layer: search.layer }, select };
89
- if (kind.id === 'gherkin') return <FeaturePage {...page} />;
90
- if (kind.id === 'quint') return <ModelPage {...page} />;
91
- return <DocumentPage {...page} />;
98
+ return <Loading>{kind.id === 'gherkin' ? <FeaturePage {...page} /> : kind.id === 'quint' ? <ModelPage {...page} /> : <DocumentPage {...page} />}</Loading>;
92
99
  }
@@ -9,7 +9,7 @@ import { StatusMark } from '../evidence/marks';
9
9
  import { useEvidence } from '../evidence/evidence-context';
10
10
  import { kindOfFile } from '../spec-kind';
11
11
  import { corpusFrom } from './corpus';
12
- import { setFinderOpen, setHelpOpen, toggleFinder, useFinderOpen, useHelpOpen, isHelpOpen } from './open';
12
+ import { commandKey, setFinderOpen, setHelpOpen, toggleFinder, useFinderOpen, useHelpOpen, isHelpOpen } from './open';
13
13
  import { FIND_GROUPS, indexCorpus, notationOf, rankIndex, splitQuery, type FindDoc, type FindKind, type FindTarget, type Ranked } from './rank';
14
14
  import { applySemantic, createWorkerEmbedder, embeddingsAvailable, embedText, textHash, type Embedder } from './semantic';
15
15
  import styles from './Finder.module.css';
@@ -36,10 +36,6 @@ const SHORTCUTS: readonly { readonly keys: string; readonly does: string }[] = [
36
36
  { keys: 'Home End', does: 'Start and end of a recording' }
37
37
  ];
38
38
 
39
- function commandKey() {
40
- return typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent) ? '⌘K' : 'Ctrl+K';
41
- }
42
-
43
39
  function isTypingTarget(target: EventTarget | null) {
44
40
  if (!(target instanceof HTMLElement)) return false;
45
41
  const tag = target.tagName;
@@ -299,5 +295,3 @@ function Row({ hit }: { hit: Ranked }) {
299
295
  </>
300
296
  );
301
297
  }
302
-
303
- export { commandKey };
@@ -49,3 +49,7 @@ export function useFinderOpen() {
49
49
  export function useHelpOpen() {
50
50
  return useSyncExternalStore(subscribeFinder, isHelpOpen, () => false);
51
51
  }
52
+
53
+ export function commandKey() {
54
+ return typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent) ? '⌘K' : 'Ctrl+K';
55
+ }