@crvouga/atlas 0.4.0 → 0.6.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 (444) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +191 -611
  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 +35 -1
  9. package/dist/core/src/config.d.ts +77 -4
  10. package/dist/core/src/config.js +110 -17
  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 +75 -16
  30. package/dist/core/src/run/execute.js +71 -18
  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 +85 -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 +56 -0
  120. package/dist/reports/src/http-store.js +148 -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 +369 -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/live-reporter.d.ts +34 -0
  147. package/dist/reports/src/live-reporter.js +122 -0
  148. package/dist/reports/src/media-types.d.ts +11 -0
  149. package/dist/reports/src/media-types.js +22 -0
  150. package/dist/reports/src/namespace-guard.d.ts +10 -1
  151. package/dist/reports/src/namespace-guard.js +10 -1
  152. package/dist/reports/src/navigation.d.ts +12 -0
  153. package/dist/reports/src/navigation.js +7 -0
  154. package/dist/reports/src/object-io.d.ts +11 -4
  155. package/dist/reports/src/object-io.js +4 -4
  156. package/dist/reports/src/object-store-conformance.d.ts +27 -0
  157. package/dist/reports/src/object-store-conformance.js +87 -0
  158. package/dist/reports/src/object-store.d.ts +33 -24
  159. package/dist/reports/src/object-store.js +17 -24
  160. package/dist/reports/src/parse-with.d.ts +16 -0
  161. package/dist/reports/src/parse-with.js +14 -0
  162. package/dist/reports/src/performance-part.d.ts +25 -1
  163. package/dist/reports/src/performance-part.js +15 -1
  164. package/dist/reports/src/performance.d.ts +22 -2
  165. package/dist/reports/src/performance.js +12 -2
  166. package/dist/reports/src/persist.d.ts +12 -3
  167. package/dist/reports/src/persist.js +12 -3
  168. package/dist/reports/src/phone.d.ts +18 -1
  169. package/dist/reports/src/phone.js +18 -1
  170. package/dist/reports/src/protocol.d.ts +26 -4
  171. package/dist/reports/src/protocol.js +15 -4
  172. package/dist/reports/src/publication.d.ts +21 -1
  173. package/dist/reports/src/publication.js +21 -1
  174. package/dist/reports/src/publish-checks.d.ts +35 -0
  175. package/dist/reports/src/publish-checks.js +122 -0
  176. package/dist/reports/src/publish.d.ts +42 -9
  177. package/dist/reports/src/publish.js +36 -7
  178. package/dist/reports/src/read.d.ts +46 -16
  179. package/dist/reports/src/read.js +38 -14
  180. package/dist/reports/src/recording-bundle.d.ts +13 -2
  181. package/dist/reports/src/recording-bundle.js +4 -2
  182. package/dist/reports/src/recording-keys.d.ts +15 -1
  183. package/dist/reports/src/recording-keys.js +15 -1
  184. package/dist/reports/src/remote-report.d.ts +28 -7
  185. package/dist/reports/src/remote-report.js +15 -7
  186. package/dist/reports/src/report-cli.d.ts +51 -0
  187. package/dist/reports/src/report-cli.js +486 -0
  188. package/dist/reports/src/report-index.d.ts +99 -0
  189. package/dist/reports/src/report-index.js +337 -0
  190. package/dist/reports/src/report-loader.d.ts +10 -0
  191. package/dist/reports/src/report-loader.js +6 -0
  192. package/dist/reports/src/report-parts.d.ts +29 -3
  193. package/dist/reports/src/report-parts.js +37 -3
  194. package/dist/reports/src/report-status.d.ts +48 -0
  195. package/dist/reports/src/report-status.js +156 -0
  196. package/dist/reports/src/report-types.d.ts +73 -0
  197. package/dist/reports/src/report-types.js +3 -0
  198. package/dist/reports/src/reporter.d.ts +67 -0
  199. package/dist/reports/src/reporter.js +128 -0
  200. package/dist/reports/src/reports-explorer-route.d.ts +541 -0
  201. package/dist/reports/src/reports-explorer-route.js +11 -0
  202. package/dist/reports/src/reports-list-route.d.ts +9 -0
  203. package/dist/reports/src/reports-list-route.js +9 -0
  204. package/dist/reports/src/reports-route.d.ts +561 -22
  205. package/dist/reports/src/reports-route.js +11 -3
  206. package/dist/reports/src/reports-store.d.ts +19 -2
  207. package/dist/reports/src/reports-store.js +10 -2
  208. package/dist/reports/src/resilient-cache.d.ts +14 -1
  209. package/dist/reports/src/resilient-cache.js +8 -1
  210. package/dist/reports/src/retention.d.ts +17 -1
  211. package/dist/reports/src/retention.js +5 -1
  212. package/dist/reports/src/run-bundle.d.ts +63 -0
  213. package/dist/reports/src/run-bundle.js +159 -0
  214. package/dist/reports/src/s3-store.d.ts +79 -0
  215. package/dist/reports/src/s3-store.js +295 -0
  216. package/dist/reports/src/scenario.d.ts +14 -4
  217. package/dist/reports/src/scenario.js +5 -4
  218. package/dist/reports/src/sigv4.d.ts +72 -0
  219. package/dist/reports/src/sigv4.js +96 -0
  220. package/dist/reports/src/snapshot-bundle.d.ts +24 -1
  221. package/dist/reports/src/snapshot-bundle.js +27 -2
  222. package/dist/reports/src/snapshot-surface.d.ts +20 -9
  223. package/dist/reports/src/snapshot-surface.js +32 -19
  224. package/dist/reports/src/source-content.d.ts +23 -0
  225. package/dist/reports/src/source-content.js +30 -1
  226. package/dist/reports/src/source-entries.d.ts +13 -0
  227. package/dist/reports/src/source-entries.js +6 -0
  228. package/dist/reports/src/source-part.d.ts +15 -0
  229. package/dist/reports/src/source-part.js +8 -0
  230. package/dist/reports/src/source-reader.d.ts +11 -0
  231. package/dist/reports/src/source-reader.js +5 -0
  232. package/dist/reports/src/spec-evidence.d.ts +12 -2
  233. package/dist/reports/src/spec-evidence.js +6 -2
  234. package/dist/reports/src/spec-inventory.d.ts +15 -1
  235. package/dist/reports/src/spec-inventory.js +22 -1
  236. package/dist/reports/src/spec-link.d.ts +28 -0
  237. package/dist/reports/src/spec-link.js +70 -0
  238. package/dist/reports/src/spec-set.d.ts +63 -0
  239. package/dist/reports/src/spec-set.js +177 -0
  240. package/dist/reports/src/status.d.ts +10 -0
  241. package/dist/reports/src/status.js +5 -0
  242. package/dist/reports/src/store-prefix.d.ts +28 -0
  243. package/dist/reports/src/store-prefix.js +22 -0
  244. package/dist/reports/src/store-url.d.ts +89 -0
  245. package/dist/reports/src/store-url.js +160 -0
  246. package/dist/reports/src/test-catalog-entity.d.ts +11 -0
  247. package/dist/reports/src/test-catalog-entity.js +6 -0
  248. package/dist/reports/src/types.d.ts +6 -1
  249. package/dist/reports/src/use-report-nav.d.ts +35 -0
  250. package/dist/reports/src/use-report-nav.js +12 -0
  251. package/dist/runners/src/codecept.d.ts +14 -0
  252. package/dist/runners/src/codecept.js +16 -0
  253. package/dist/runners/src/jest.d.ts +50 -0
  254. package/dist/runners/src/jest.js +52 -0
  255. package/dist/runners/src/mocha.d.ts +40 -0
  256. package/dist/runners/src/mocha.js +39 -0
  257. package/dist/runners/src/node-test.d.ts +28 -0
  258. package/dist/runners/src/node-test.js +44 -0
  259. package/dist/runners/src/publish.d.ts +30 -0
  260. package/dist/runners/src/publish.js +68 -0
  261. package/dist/schema/src/api.d.ts +800 -0
  262. package/dist/schema/src/api.js +201 -0
  263. package/dist/schema/src/cli-results.d.ts +210 -0
  264. package/dist/schema/src/cli-results.js +124 -0
  265. package/dist/schema/src/common.d.ts +53 -0
  266. package/dist/schema/src/common.js +42 -0
  267. package/dist/schema/src/index.d.ts +16 -0
  268. package/dist/schema/src/index.js +16 -0
  269. package/dist/schema/src/layer.d.ts +2273 -0
  270. package/dist/schema/src/layer.js +462 -0
  271. package/dist/schema/src/live-run.d.ts +123 -0
  272. package/dist/schema/src/live-run.js +30 -0
  273. package/dist/schema/src/manifest.d.ts +97 -2
  274. package/dist/schema/src/manifest.js +84 -0
  275. package/dist/schema/src/project.d.ts +20 -0
  276. package/dist/schema/src/project.js +10 -0
  277. package/dist/schema/src/report-sources.d.ts +29 -0
  278. package/dist/schema/src/report-sources.js +27 -1
  279. package/dist/schema/src/runs-index.d.ts +17 -1
  280. package/dist/schema/src/runs-index.js +15 -0
  281. package/dist/schema/src/server-config.d.ts +177 -0
  282. package/dist/schema/src/server-config.js +85 -0
  283. package/dist/schema/src/spec.d.ts +69 -0
  284. package/dist/schema/src/spec.js +58 -0
  285. package/dist/server/src/app.d.ts +72 -0
  286. package/dist/server/src/app.js +557 -0
  287. package/dist/server/src/auth.d.ts +41 -0
  288. package/dist/server/src/auth.js +59 -0
  289. package/dist/server/src/config.d.ts +83 -0
  290. package/dist/server/src/config.js +204 -0
  291. package/dist/server/src/dev.d.ts +12 -0
  292. package/dist/server/src/dev.js +89 -0
  293. package/dist/server/src/events.d.ts +29 -0
  294. package/dist/server/src/events.js +101 -0
  295. package/dist/server/src/host.d.ts +30 -0
  296. package/dist/server/src/host.js +115 -0
  297. package/dist/server/src/index.d.ts +18 -0
  298. package/dist/server/src/index.js +18 -0
  299. package/dist/server/src/live-runs.d.ts +52 -0
  300. package/dist/server/src/live-runs.js +229 -0
  301. package/dist/server/src/llms.d.ts +19 -0
  302. package/dist/server/src/llms.js +63 -0
  303. package/dist/server/src/main.d.ts +30 -0
  304. package/dist/server/src/main.js +114 -0
  305. package/dist/server/src/openapi.d.ts +56 -0
  306. package/dist/server/src/openapi.js +438 -0
  307. package/dist/server/src/problem.d.ts +8 -0
  308. package/dist/server/src/problem.js +39 -0
  309. package/dist/server/src/projects.d.ts +3 -0
  310. package/dist/server/src/projects.js +154 -0
  311. package/dist/server/src/settings-page.d.ts +9 -0
  312. package/dist/server/src/settings-page.js +93 -0
  313. package/dist/server/src/settings.d.ts +23 -0
  314. package/dist/server/src/settings.js +72 -0
  315. package/dist/server/src/site.d.ts +15 -0
  316. package/dist/server/src/site.js +78 -0
  317. package/dist/server/src/store-api.d.ts +28 -0
  318. package/dist/server/src/store-api.js +180 -0
  319. package/dist/server/src/validate.d.ts +71 -0
  320. package/dist/server/src/validate.js +197 -0
  321. package/dist/vitest/src/index.d.ts +27 -5
  322. package/dist/vitest/src/index.js +18 -5
  323. package/dist/webdriver/src/driver.d.ts +63 -11
  324. package/dist/webdriver/src/driver.js +57 -6
  325. package/dist/webdriver/src/index.d.ts +8 -0
  326. package/dist/webdriver/src/index.js +1 -0
  327. package/dist/webdriver/src/protocol.d.ts +75 -6
  328. package/dist/webdriver/src/protocol.js +79 -6
  329. package/docs/agents.md +137 -0
  330. package/docs/deploying.md +345 -0
  331. package/docs/dogfooding.md +175 -0
  332. package/docs/extending.md +242 -0
  333. package/docs/integrating.md +485 -0
  334. package/docs/reference/bun.md +141 -0
  335. package/docs/reference/cli.md +148 -0
  336. package/docs/reference/core.md +2432 -0
  337. package/docs/reference/detox.md +150 -0
  338. package/docs/reference/http-api.md +799 -0
  339. package/docs/reference/playwright.md +303 -0
  340. package/docs/reference/reports.md +7152 -0
  341. package/docs/reference/runners.md +169 -0
  342. package/docs/reference/schema.md +1762 -0
  343. package/docs/reference/server.md +825 -0
  344. package/docs/reference/vitest.md +45 -0
  345. package/docs/reference/webdriver.md +330 -0
  346. package/docs/report-backends.md +144 -0
  347. package/docs/report-contract.md +400 -0
  348. package/docs/reporting-issues.md +160 -0
  349. package/docs/runners.md +69 -0
  350. package/docs/server.md +321 -0
  351. package/docs/statecharts.md +643 -0
  352. package/llms-full.txt +4114 -0
  353. package/llms.txt +49 -0
  354. package/package.json +61 -11
  355. package/packages/core/README.md +2 -1
  356. package/packages/reports/README.md +65 -0
  357. package/packages/reports/STORAGE.md +4 -0
  358. package/packages/runners/README.md +6 -0
  359. package/packages/runners/compat/codecept.cjs +2 -0
  360. package/packages/runners/compat/jest.cjs +2 -0
  361. package/packages/runners/compat/mocha.cjs +2 -0
  362. package/packages/schema/README.md +14 -0
  363. package/packages/schema/schemas/atlas-change.schema.json +4 -0
  364. package/packages/schema/schemas/check-detail.schema.json +1032 -0
  365. package/packages/schema/schemas/checks-result.schema.json +26 -0
  366. package/packages/schema/schemas/command-error.schema.json +35 -0
  367. package/packages/schema/schemas/doctor-result.schema.json +60 -0
  368. package/packages/schema/schemas/effective-delta.schema.json +490 -0
  369. package/packages/schema/schemas/effective-evidence.schema.json +471 -0
  370. package/packages/schema/schemas/evidence-page.schema.json +271 -0
  371. package/packages/schema/schemas/hello-event.schema.json +26 -0
  372. package/packages/schema/schemas/init-plan.schema.json +347 -0
  373. package/packages/schema/schemas/layer-rows.schema.json +199 -0
  374. package/packages/schema/schemas/layer.schema.json +2888 -0
  375. package/packages/schema/schemas/live-run-start.schema.json +42 -0
  376. package/packages/schema/schemas/live-run-update.schema.json +93 -0
  377. package/packages/schema/schemas/live-run.schema.json +225 -0
  378. package/packages/schema/schemas/merge-result.schema.json +40 -0
  379. package/packages/schema/schemas/outline.schema.json +62 -0
  380. package/packages/schema/schemas/problem.schema.json +37 -0
  381. package/packages/schema/schemas/project-slug.schema.json +8 -0
  382. package/packages/schema/schemas/project.schema.json +22 -0
  383. package/packages/schema/schemas/projects.schema.json +33 -0
  384. package/packages/schema/schemas/publish-checks.schema.json +1413 -0
  385. package/packages/schema/schemas/publish-result.schema.json +168 -0
  386. package/packages/schema/schemas/ready.schema.json +68 -0
  387. package/packages/schema/schemas/report-event.schema.json +100 -0
  388. package/packages/schema/schemas/report-status.schema.json +370 -0
  389. package/packages/schema/schemas/server-config.schema.json +208 -0
  390. package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
  391. package/packages/schema/schemas/server-info.schema.json +114 -0
  392. package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
  393. package/packages/schema/schemas/spec-set.schema.json +103 -0
  394. package/packages/schema/schemas/store-listing.schema.json +29 -0
  395. package/packages/schema/schemas/store-write-result.schema.json +28 -0
  396. package/packages/schema/schemas/stream-result.schema.json +59 -0
  397. package/packages/server/README.md +43 -0
  398. package/packages/server/openapi.json +11021 -0
  399. package/packages/visualizer/README.md +32 -11
  400. package/packages/visualizer/bin/atlas-client.mjs +176 -0
  401. package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
  402. package/packages/visualizer/src/app/AppShell.tsx +32 -16
  403. package/packages/visualizer/src/app/report-sources.tsx +7 -4
  404. package/packages/visualizer/src/app/router.tsx +3 -3
  405. package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
  406. package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
  407. package/packages/visualizer/src/components/Header.module.css +101 -192
  408. package/packages/visualizer/src/components/Header.tsx +88 -13
  409. package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
  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/data/adapters/report-backend.test.ts +3 -2
  413. package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
  414. package/packages/visualizer/src/data/queries.ts +14 -7
  415. package/packages/visualizer/src/evidence/api.ts +16 -3
  416. package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
  417. package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
  418. package/packages/visualizer/src/evidence/check-detail.tsx +21 -4
  419. package/packages/visualizer/src/evidence/document-page.tsx +6 -2
  420. package/packages/visualizer/src/evidence/evidence-context.tsx +79 -12
  421. package/packages/visualizer/src/evidence/evidence.module.css +11 -1
  422. package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
  423. package/packages/visualizer/src/evidence/model-page.tsx +6 -2
  424. package/packages/visualizer/src/evidence/project-home.tsx +45 -0
  425. package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
  426. package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
  427. package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
  428. package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
  429. package/packages/visualizer/src/find/Finder.tsx +1 -7
  430. package/packages/visualizer/src/find/open.ts +4 -0
  431. package/packages/visualizer/src/live/live-query.ts +21 -0
  432. package/packages/visualizer/src/live/live-runs.tsx +69 -0
  433. package/packages/visualizer/src/live/live-stream.ts +225 -0
  434. package/packages/visualizer/src/live/report-progress.ts +62 -0
  435. package/packages/visualizer/src/live/test-progress.tsx +34 -0
  436. package/packages/visualizer/src/runtime.ts +35 -0
  437. package/packages/visualizer/src/state/ui-store.ts +3 -2
  438. package/packages/visualizer/src/views/ChartPage.tsx +2 -0
  439. package/packages/visualizer/src/views/ChartTests.tsx +43 -0
  440. package/packages/visualizer/src/views/SidePanel.tsx +2 -0
  441. package/packages/visualizer/vite/atlas-files.test.ts +4 -4
  442. package/packages/visualizer/vite/atlas-files.ts +1 -5
  443. package/packages/visualizer/vite/atlas-server-proxy.ts +37 -0
  444. package/packages/visualizer/vite.config.ts +11 -3
@@ -1,20 +1,32 @@
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, useLiveStatus } from '../live/live-stream';
14
+ import { liveInvalidator } from '../live/live-query';
15
+ import { useLiveTests } from '../live/live-runs';
16
+ import { withLiveProgress } from '../live/report-progress';
11
17
  import { fileName, folderOf, kindOfFile, outlinePath } from '../spec-kind';
12
18
  import { useUiStore } from '../state/ui-store';
13
- import { reportJson, staticEvidence } from './api';
19
+ import { effectiveEvidence, reportJson, staticEvidence } from './api';
14
20
 
15
21
  /** How often the layer list is asked for while a spec run is writing results, and while nothing is. */
16
22
  const LIVE_POLL_MS = 2_000;
17
23
  const IDLE_POLL_MS = 15_000;
24
+ /** The effective report is revalidated with its ETag, so asking often costs one small answer. */
25
+ const EFFECTIVE_POLL_MS = 5_000;
26
+ /** While the server's stream is open it says when to read again; this is only the safety net behind it. */
27
+ const LIVE_SAFETY_POLL_MS = 60_000;
28
+ /** At most this often is the report read again because the server said it changed. */
29
+ const LIVE_REREAD_MS = 250;
18
30
 
19
31
  export type EvidenceState = {
20
32
  /** The layer list has been read at least once. */
@@ -63,26 +75,77 @@ function outlineOf(files: readonly string[]): ReportOutline {
63
75
  return { specs, sha: null, changedFiles: [] };
64
76
  }
65
77
 
78
+ const NONE: readonly LayerEvidence[] = [];
79
+
80
+ function hasExclusions(excluded: SourceSelection): boolean {
81
+ return Object.values(excluded).some((list) => list.length > 0);
82
+ }
83
+
66
84
  export function EvidenceProvider({ children }: { children: ReactNode }) {
67
85
  const adapter = useAdapter();
68
86
  const source = specKey(adapter);
69
87
  const excluded = useUiStore((s) => s.excluded);
70
88
  const setExcluded = useUiStore((s) => s.setExcluded);
71
89
 
90
+ const client = useQueryClient();
91
+ const stream = useLiveStatus();
92
+ const streaming = stream === 'live';
93
+ // The server says when results arrive: read the report again then, not on a timer.
94
+ useEffect(() => {
95
+ const url = eventsAddress();
96
+ if (!url) return;
97
+ // A burst of results (a CI run finishing, many shards at once) is read once, not once for each.
98
+ const reads = [liveInvalidator(client, ['evidence-effective'], LIVE_REREAD_MS), liveInvalidator(client, ['evidence-index'], LIVE_REREAD_MS)];
99
+ const outlines = [liveInvalidator(client, ['evidence-outline'], LIVE_REREAD_MS), liveInvalidator(client, ['spec', source], LIVE_REREAD_MS)];
100
+ const reread = () => reads.forEach(refresh => refresh());
101
+ const reoutline = () => { outlines.forEach(refresh => refresh()); reread(); };
102
+ const stop = listenLive(url, (event) => {
103
+ if (event.type === 'change' && event.change.scope === 'specs') return reoutline();
104
+ if (event.type === 'change' && event.change.scope === 'runs' && !event.change.liveRunId) return reread();
105
+ if (event.type !== 'report' && event.type !== 'open') return;
106
+ reread();
107
+ if (event.type === 'open') reoutline();
108
+ });
109
+ return () => { stop(); [...reads, ...outlines].forEach(refresh => refresh.cancel()); };
110
+ }, [client, source]);
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
+ refetchOnWindowFocus: 'always',
133
+ refetchOnReconnect: 'always',
134
+ retry: false,
135
+ refetchInterval: streaming ? LIVE_SAFETY_POLL_MS : EFFECTIVE_POLL_MS
136
+ });
137
+
78
138
  const index = useInfiniteQuery({
79
139
  queryKey: ['evidence-index'],
140
+ enabled: !effective,
80
141
  initialPageParam: null as string | null,
81
142
  queryFn: ({ pageParam, signal }) => reportJson<EvidencePage>(`/evidence${pageParam ? `?cursor=${encodeURIComponent(pageParam)}` : ''}`, signal),
82
143
  getNextPageParam: (page) => page.cursor,
83
144
  staleTime: 0,
145
+ refetchOnWindowFocus: 'always',
146
+ refetchOnReconnect: 'always',
84
147
  retry: false,
85
- refetchInterval: (query) => (staticEvidence ? false : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
148
+ refetchInterval: (query) => (staticEvidence ? false : streaming ? LIVE_SAFETY_POLL_MS : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
86
149
  });
87
150
  // Show the newest layers at once, then read the older pages behind them.
88
151
  const { hasNextPage, isFetching, isError, fetchNextPage } = index;
@@ -98,7 +161,7 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
98
161
  const serverOutline = useQuery({ queryKey: ['evidence-outline'], queryFn: ({ signal }) => reportJson<ReportOutline>('/outline', signal), staleTime: 30_000, retry: false });
99
162
  const outline = useMemo(() => serverOutline.data ?? outlineOf(files), [serverOutline.data, files]);
100
163
 
101
- const wanted = useMemo(() => layers.filter((layer) => contributes(layer, excluded)), [layers, excluded]);
164
+ const wanted = useMemo(() => (effective ? [] : layers.filter((layer) => contributes(layer, excluded))), [layers, excluded, effective]);
102
165
  const rows = useQueries({
103
166
  queries: wanted.map((layer) => ({
104
167
  queryKey: rowsKey(layer),
@@ -126,23 +189,27 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
126
189
  // `stamp` changes exactly when a layer's rows do.
127
190
  }, [wanted, rows.stamp]);
128
191
 
129
- const view = useMemo(() => evidenceView(combineEvidence(outline, evidence)), [outline, evidence]);
192
+ const shown = effective ? overview.data?.layers ?? NONE : evidence;
193
+ const { tests: liveTests } = useLiveTests();
194
+ const view = useMemo(() => withLiveProgress(evidenceView(combineEvidence(outline, shown)), liveTests), [outline, shown, liveTests]);
130
195
  const titles = useMemo(() => new Map(outline.specs.map((spec) => [spec.path, spec.title])), [outline]);
196
+ const effectiveHeaders = useMemo(() => (overview.data?.layers ?? NONE).map((entry) => entry.layer), [overview.data]);
131
197
 
132
198
  const state: EvidenceState = {
133
- ready: pages !== undefined,
134
- unavailable: pages === undefined && index.error ? index.error.message : null,
199
+ ready: effective ? overview.data !== undefined : pages !== undefined,
200
+ unavailable: effective ? (overview.data === undefined && overview.error ? overview.error.message : null) : pages === undefined && index.error ? index.error.message : null,
135
201
  retry: () => {
202
+ if (effective) { void Promise.all([overview.refetch(), serverOutline.refetch()]); return; }
136
203
  void reportJson<EvidencePage>('/evidence?retry=1')
137
204
  .catch(() => null)
138
205
  .then(() => Promise.all([index.refetch(), serverOutline.refetch()]));
139
206
  },
140
207
  pending: wanted.length - evidence.length,
141
- layers,
142
- evidence,
143
- backends: first?.backends ?? [],
144
- active: first?.active ?? [],
145
- notices,
208
+ layers: effective ? effectiveHeaders : layers,
209
+ evidence: shown,
210
+ backends: effective ? overview.data?.backends ?? [] : first?.backends ?? [],
211
+ active: effective ? [] : first?.active ?? [],
212
+ notices: effective ? overview.data?.notices ?? [] : notices,
146
213
  view,
147
214
  files,
148
215
  filesPending: specIndex.isPending,
@@ -15,7 +15,8 @@
15
15
 
16
16
  .mark[data-status='running'] {
17
17
  color: var(--place);
18
- animation: turn 0.9s linear infinite;
18
+ animation: turn 1.8s linear infinite;
19
+ opacity: .8;
19
20
  }
20
21
 
21
22
  @keyframes turn {
@@ -509,6 +510,11 @@
509
510
  transform: rotate(-135deg);
510
511
  }
511
512
 
513
+ .excerpt[data-progress] {
514
+ color: var(--ink-3);
515
+ font-family: inherit;
516
+ }
517
+
512
518
  .excerpt {
513
519
  margin: -4px 14px 10px 40px;
514
520
  overflow: hidden;
@@ -895,3 +901,7 @@ a.listRow:hover {
895
901
  display: none;
896
902
  }
897
903
  }
904
+
905
+ @media (prefers-reduced-motion: reduce) {
906
+ .mark[data-status='running'] { animation: none; }
907
+ }
@@ -8,6 +8,7 @@ import { featureModel, scenarioLabel, slotsByScenario, type FeatureModel, type F
8
8
  import { Bounded } from './marks';
9
9
  import { SourceText, SpecHeader } from './spec-header';
10
10
  import { SpecSnapshot } from './spec-snapshot';
11
+ import { TestFilter, useRunningFilter } from './test-filter';
11
12
 
12
13
  type Props = { file: string; text: string; kind: SpecKind; entry: SpecEvidence | undefined; selection: CheckSelection; select: SelectCheck };
13
14
 
@@ -23,6 +24,7 @@ function parse(text: string): { feature: FeatureModel | null; error: string | nu
23
24
  export function FeaturePage({ file, text, kind, entry, selection, select }: Props) {
24
25
  const { feature, error } = useMemo(() => parse(text), [text]);
25
26
  const slots = entry?.slots ?? [];
27
+ const runningOnly = useRunningFilter();
26
28
  const filed = useMemo(() => (feature ? slotsByScenario(feature, slots) : { byScenario: new Map<string, CheckSlot[]>(), leftOver: slots }), [feature, slots]);
27
29
  const scenarios = feature?.rules.flatMap((rule) => rule.scenarios.filter((scenario) => !scenario.background)) ?? [];
28
30
  const planned = scenarios.reduce((count, scenario) => count + Math.max(1, filed.byScenario.get(scenario.name)?.length ?? 0), 0) + filed.leftOver.length;
@@ -30,8 +32,9 @@ export function FeaturePage({ file, text, kind, entry, selection, select }: Prop
30
32
 
31
33
  const rows = (scenario: FeatureScenario) => {
32
34
  const written = <PlainSteps steps={scenario.steps} examples={scenario.examples} />;
33
- const recorded = filed.byScenario.get(scenario.name) ?? [];
35
+ const recorded = (filed.byScenario.get(scenario.name) ?? []).filter(slot => !runningOnly || slot.status === 'running');
34
36
  if (!recorded.length) {
37
+ if (runningOnly) return null;
35
38
  const id = `scenario:${scenario.name}`;
36
39
  return (
37
40
  <ResultRow key={id} label={scenario.name} slot={null} open={selection.check === id} onToggle={() => toggle(id)}>
@@ -53,6 +56,7 @@ export function FeaturePage({ file, text, kind, entry, selection, select }: Prop
53
56
  {feature?.description && <p className={styles.lead}>{feature.description}</p>}
54
57
  </SpecHeader>
55
58
  <SpecSnapshot title={feature?.name || file.split('/').at(-1)!} text={text} kind={kind.one} />
59
+ <TestFilter slots={slots} />
56
60
  {error && <p role="alert" className={styles.notice}>This feature could not be parsed: {error}</p>}
57
61
  {feature?.rules.map((rule, index) => (
58
62
  <section key={index} className={styles.block}>
@@ -67,12 +71,12 @@ export function FeaturePage({ file, text, kind, entry, selection, select }: Prop
67
71
  <ul className={styles.results}>{rule.scenarios.filter((scenario) => !scenario.background).map(rows)}</ul>
68
72
  </section>
69
73
  ))}
70
- {filed.leftOver.length > 0 && (
74
+ {filed.leftOver.some(slot => !runningOnly || slot.status === 'running') && (
71
75
  <section className={styles.block}>
72
76
  <h2 className={styles.blockTitle}>{feature ? 'Other recorded results' : 'Recorded results'}</h2>
73
77
  {feature && <p className={styles.blockLead}>Recorded against this file under a name no scenario has now.</p>}
74
78
  <ul className={styles.results}>
75
- <Bounded items={filed.leftOver} size={40} more="More results">
79
+ <Bounded items={filed.leftOver.filter(slot => !runningOnly || slot.status === 'running')} size={40} more="More results">
76
80
  {(slot) => (
77
81
  <ResultRow key={slot.key} label={feature ? scenarioLabel(feature.name, slot.title) : slot.title} slot={slot} open={selection.check === slot.id} onToggle={() => toggle(slot.id)}>
78
82
  <CheckDetail slot={slot} layerId={selection.check === slot.id ? selection.layer : undefined} select={(patch) => select({ check: slot.id, ...patch })} reads="steps" />
@@ -8,6 +8,7 @@ import { Bounded } from './marks';
8
8
  import { QUINT_GROUPS, quintOutline } from './quint';
9
9
  import { SourceText, SpecHeader } from './spec-header';
10
10
  import { SpecSnapshot } from './spec-snapshot';
11
+ import { TestFilter, useRunningFilter } from './test-filter';
11
12
 
12
13
  type Props = { file: string; text: string; kind: SpecKind; entry: SpecEvidence | undefined; selection: CheckSelection; select: SelectCheck };
13
14
 
@@ -16,17 +17,20 @@ export function ModelPage({ file, text, kind, entry, selection, select }: Props)
16
17
  const items = useMemo(() => quintOutline(text), [text]);
17
18
  const moduleName = items.find((item) => item.kind === 'module')?.name;
18
19
  const slots = useMemo(() => [...(entry?.slots ?? [])].sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true })), [entry]);
20
+ const runningOnly = useRunningFilter();
21
+ const visible = slots.filter(slot => !runningOnly || slot.status === 'running');
19
22
  const toggle = (id: string) => select({ check: selection.check === id ? null : id, layer: null });
20
23
  const label = (title: string) => (moduleName && title.startsWith(`${moduleName} · `) ? title.slice(moduleName.length + 3) : title);
21
24
  return (
22
25
  <article className={styles.page}>
23
26
  <SpecHeader title={moduleName ?? file.split('/').at(-1)!} file={file} kind={kind} entry={entry} />
24
27
  <SpecSnapshot title={moduleName ?? file.split('/').at(-1)!} text={text} kind={kind.one} />
25
- {slots.length > 0 && (
28
+ <TestFilter slots={slots} />
29
+ {visible.length > 0 && (
26
30
  <section className={styles.block}>
27
31
  <h2 className={styles.blockTitle}>Traces</h2>
28
32
  <ul className={styles.results}>
29
- <Bounded items={slots} size={40} more="More traces">
33
+ <Bounded items={visible} size={40} more="More traces">
30
34
  {(slot) => (
31
35
  <ResultRow key={slot.key} label={label(slot.title)} slot={slot} open={selection.check === slot.id} onToggle={() => toggle(slot.id)}>
32
36
  <CheckDetail slot={slot} layerId={selection.check === slot.id ? selection.layer : undefined} select={(patch) => select({ check: slot.id, ...patch })} reads="trace" />
@@ -0,0 +1,45 @@
1
+ import { Link } from '@tanstack/react-router';
2
+ import { lazy, Suspense, useState } from 'react';
3
+ import { isStatechartFile } from '@crvouga/atlas/schema';
4
+ import { useEvidence } from './evidence-context';
5
+ import { CheckDetail, ResultRow, type CheckSelection } from './check-detail';
6
+ import { TestFilter, useRunningFilter } from './test-filter';
7
+ import { Bounded } from './marks';
8
+ import { SPEC_KINDS, kindOfFile } from '../spec-kind';
9
+ import { currentProject } from '../runtime';
10
+ import styles from './evidence.module.css';
11
+ const RecordingPage = lazy(() => import('./recording-page').then(module => ({ default: module.RecordingPage })));
12
+
13
+ /** A project can contain any test runner or spec notation; statecharts are optional. */
14
+ export function ProjectHome() {
15
+ const evidence = useEvidence();
16
+ const runningOnly = useRunningFilter();
17
+ const [selection, setSelection] = useState<CheckSelection>({});
18
+ const [selectedKey, setSelectedKey] = useState<string | null>(null);
19
+ const [recording, setRecording] = useState(false);
20
+ const [moment, setMoment] = useState<number | null>(null);
21
+ const kinds = SPEC_KINDS.map(kind => ({ kind, files: evidence.files.filter(kind.accepts) })).filter(entry => entry.files.length);
22
+ const checks = [...evidence.view.unassigned, ...[...evidence.view.specs].filter(([file]) => !kindOfFile(file) && !isStatechartFile(file)).flatMap(([, entry]) => entry.slots)];
23
+ const visible = checks.filter(slot => !runningOnly || slot.status === 'running');
24
+ return <article className={styles.page}>
25
+ <header className={styles.specHead}><h1 className={styles.title}>{currentProject()?.split('-').map(word => word[0]?.toUpperCase() + word.slice(1)).join(' ') || 'Project reports'}</h1></header>
26
+ {evidence.unavailable && <p role="alert" className={styles.notice}>{evidence.unavailable}</p>}
27
+ {kinds.length > 0 && <section className={styles.block}><h2 className={styles.blockTitle}>Specs</h2><ul className={styles.list}>{kinds.map(({ kind, files }) => <li key={kind.id}>
28
+ <Link className={styles.listRow} to="/specs/$kind" params={{ kind: kind.id }} search={{}}><strong>{kind.label}</strong><span>{files.length} {files.length === 1 ? kind.one : kind.many}</span></Link>
29
+ </li>)}</ul></section>}
30
+ {checks.length > 0 && <section className={styles.block}><h2 className={styles.blockTitle}>Tests</h2><TestFilter slots={checks} /><ul className={styles.results}>
31
+ <Bounded items={visible} size={40} more="More tests">{slot => <ResultRow key={slot.key} label={slot.title} slot={slot} open={selectedKey === slot.key} onToggle={() => { setSelectedKey(selectedKey === slot.key ? null : slot.key); setSelection({ check: slot.id }); setRecording(false); }}>
32
+ {recording ? <Suspense fallback={<p role="status">Loading the recording…</p>}><RecordingPage slot={slot} layerId={selection.layer} label={slot.title} back="Test report" moment={moment} onMoment={setMoment} onClose={() => setRecording(false)} /></Suspense>
33
+ : <CheckDetail slot={slot} layerId={selection.layer} reads="steps" select={patch => { setSelection({ check: patch.check ?? slot.id, layer: patch.layer ?? undefined }); if (patch.view === 'recording') { setRecording(true); setMoment(null); } }} />}
34
+ </ResultRow>}</Bounded>
35
+ </ul></section>}
36
+ {!kinds.length && !checks.length && <p className={styles.aside}>No reports yet. Publish tests and specs to this project to populate its report.</p>}
37
+ </article>;
38
+ }
39
+
40
+ export function ProjectSpecIndex() {
41
+ const { files } = useEvidence();
42
+ return <nav className={styles.index} aria-label="Project specs"><h2>Specs</h2><ul>{SPEC_KINDS.filter(kind => files.some(kind.accepts)).map(kind => <li key={kind.id}>
43
+ <Link className={styles.indexRow} to="/specs/$kind" params={{ kind: kind.id }} search={{}}>{kind.label}</Link>
44
+ </li>)}</ul></nav>;
45
+ }
@@ -10,6 +10,7 @@ import styles from './evidence.module.css';
10
10
  import { duration, statusWord, useNow } from './format';
11
11
  import { Origin, StatusMark } from './marks';
12
12
  import { Timeline } from './recording';
13
+ import { TestProgress, useTestProgress } from '../live/test-progress';
13
14
 
14
15
  type Props = {
15
16
  /** The check whose recording is open, once the combined report holds it. */
@@ -39,6 +40,7 @@ function needsStylesheet(report: RunReport | undefined): boolean {
39
40
 
40
41
  /** One recorded test, opened in place of its spec. */
41
42
  export function RecordingPage({ slot, layerId, label, back, moment, onMoment, onClose }: Props) {
43
+ const progress = useTestProgress(slot ?? null);
42
44
  const { evidence, backends, pending, ready, unavailable } = useEvidence();
43
45
  const now = useNow();
44
46
  const history = useMemo(() => (slot ? checkHistory(evidence, slot.specPath, slot.id) : []), [evidence, slot]);
@@ -64,8 +66,9 @@ export function RecordingPage({ slot, layerId, label, back, moment, onMoment, on
64
66
  </p>
65
67
  )}
66
68
  </header>
69
+ <TestProgress {...progress} />
67
70
  {waiting && <p className={styles.aside} role="status">Loading the recording…</p>}
68
- {!chosen && !waiting && <p className={styles.notice}>{unavailable ?? 'This result is not in the combined report any more. A newer run may have replaced it, or its source is switched off.'}</p>}
71
+ {!chosen && !waiting && !progress.tests.length && <p className={styles.notice}>{unavailable ?? 'This result is not in the combined report any more. A newer run may have replaced it, or its source is switched off.'}</p>}
69
72
  {chosen && !paths && <p className={styles.notice}>This result has an outcome and no recorded timeline.</p>}
70
73
  {report.isLoading && <p className={styles.aside} role="status">Loading the recording…</p>}
71
74
  {report.isError && (
@@ -11,6 +11,7 @@ import { StatusMark, Tally } from './marks';
11
11
  const FILTERS: readonly { readonly id: ResultFilter; readonly label: string; readonly keeps: (summary: SpecSummary) => boolean }[] = [
12
12
  { id: 'all', label: 'All', keeps: () => true },
13
13
  { id: 'results', label: 'Results', keeps: (summary) => summary.status !== 'none' },
14
+ { id: 'running', label: 'Running', keeps: (summary) => summary.counts.running > 0 },
14
15
  { id: 'failed', label: 'Failing', keeps: (summary) => summary.counts.failed > 0 },
15
16
  { id: 'none', label: 'None', keeps: (summary) => summary.status === 'none' }
16
17
  ];
@@ -47,7 +48,7 @@ export function SpecIndex({ kind }: { kind: SpecKind }) {
47
48
  {evidence.filesError && <p className={styles.indexNote}>The file list could not be loaded.</p>}
48
49
  {!evidence.filesPending && !evidence.filesError && visible.length === 0 && (
49
50
  <p className={styles.indexNote}>
50
- {rows.length === 0 ? `No ${kind.many} in this spec set.` : filter === 'failed' ? `No failing ${kind.many}.` : filter === 'results' ? `No ${kind.many} with results yet.` : `Every ${kind.one} has results.`}
51
+ {rows.length === 0 ? `No ${kind.many} in this spec set.` : filter === 'running' ? `No running ${kind.many}.` : filter === 'failed' ? `No failing ${kind.many}.` : filter === 'results' ? `No ${kind.many} with results yet.` : `Every ${kind.one} has results.`}
51
52
  </p>
52
53
  )}
53
54
  {groups.map(([folder, entries]) => (
@@ -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
  }
@@ -0,0 +1,21 @@
1
+ import type { CheckSlot } from '@crvouga/atlas/reports/evidence-view';
2
+ import { useUiStore } from '../state/ui-store';
3
+ import styles from './evidence.module.css';
4
+
5
+ /** Filter actual test rows as well as the spec index. Counts always describe the whole document. */
6
+ export function TestFilter({ slots }: { slots: readonly CheckSlot[] }) {
7
+ const filter = useUiStore(s => s.resultFilter);
8
+ const setFilter = useUiStore(s => s.setResultFilter);
9
+ const running = slots.filter(slot => slot.status === 'running').length;
10
+ return <>
11
+ <div className={styles.filters} role="group" aria-label="Test status">
12
+ <button type="button" aria-pressed={filter !== 'running'} onClick={() => setFilter('all')}>All tests <span>{slots.length}</span></button>
13
+ <button type="button" aria-pressed={filter === 'running'} onClick={() => setFilter('running')}>Running <span>{running}</span></button>
14
+ </div>
15
+ {filter === 'running' && running === 0 && <p className={styles.aside} role="status">No running tests.</p>}
16
+ </>;
17
+ }
18
+
19
+ export function useRunningFilter() {
20
+ return useUiStore(s => s.resultFilter === 'running');
21
+ }
@@ -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
+ }
@@ -0,0 +1,21 @@
1
+ import type { QueryClient, QueryKey } from '@tanstack/react-query';
2
+ import { throttled } from './live-stream';
3
+
4
+ /** Coalesce bursts without cancelling useful work; an event during a fetch guarantees one trailing read. */
5
+ export function liveInvalidator(client: QueryClient, queryKey: QueryKey, ms = 150) {
6
+ let dirty = false;
7
+ let reading = false;
8
+ let stopped = false;
9
+ const drain = throttled(() => {
10
+ if (stopped || reading || !dirty) return;
11
+ dirty = false;
12
+ reading = true;
13
+ void client.invalidateQueries({ queryKey }, { cancelRefetch: false }).finally(() => {
14
+ reading = false;
15
+ if (dirty && !stopped) drain();
16
+ });
17
+ }, ms);
18
+ const refresh = () => { if (!stopped) { dirty = true; drain(); } };
19
+ refresh.cancel = () => { stopped = true; dirty = false; drain.cancel(); };
20
+ return refresh;
21
+ }
@@ -0,0 +1,69 @@
1
+ import { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react';
2
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
3
+ import type { LiveRun } from '@crvouga/atlas/schema';
4
+ import { runtimeConfig, runtimeUrl } from '../runtime';
5
+ import { eventsAddress, listenLive, useLiveStatus } from './live-stream';
6
+ import { liveInvalidator } from './live-query';
7
+ import type { LiveCheck } from '@crvouga/atlas/schema';
8
+ import { useUiStore } from '../state/ui-store';
9
+ import { indexLiveTests } from './report-progress';
10
+
11
+ export type LiveTest = { run: LiveRun; check: LiveCheck };
12
+ type LiveState = { tests: readonly LiveTest[]; index: ReturnType<typeof indexLiveTests>; error: string | null };
13
+ const EMPTY: LiveState = { tests: [], index: new Map(), error: null };
14
+ const LiveContext = createContext<LiveState>(EMPTY);
15
+
16
+ /** One subscription shared by report rows; leases never rewrite historical report results. */
17
+ export function LiveRunsProvider({ children, enabled = true }: { children: ReactNode; enabled?: boolean }) {
18
+ const client = useQueryClient();
19
+ const streaming = useLiveStatus() === 'live';
20
+ const excluded = useUiStore(s => s.excluded);
21
+ const api = runtimeUrl(runtimeConfig()?.api);
22
+ const url = api && enabled ? `${api.replace(/\/$/, '')}/live` : undefined;
23
+ const key = ['live-runs', url] as const;
24
+ const query = useQuery({ queryKey: key, enabled: !!url, retry: false, staleTime: 0, refetchOnWindowFocus: 'always', refetchOnReconnect: 'always',
25
+ queryFn: async ({ signal }) => {
26
+ const response = await fetch(url!, { signal, cache: 'no-store' });
27
+ if (!response.ok) throw new Error(`Live feed unavailable (${response.status}). Last received results may be stale.`);
28
+ const fetched = (await response.json()) as { runs: LiveRun[] };
29
+ // An event can arrive while this HTTP snapshot is in flight. Keep its newer progress,
30
+ // while still loading the other runs from the snapshot rather than cancelling it.
31
+ const held = client.getQueryData<{ runs: LiveRun[] }>(key);
32
+ const runs = new Map(fetched.runs.map(run => [run.id, run]));
33
+ for (const run of held?.runs ?? []) {
34
+ const read = runs.get(run.id);
35
+ if (!read || run.sequence > read.sequence || (run.sequence === read.sequence &&
36
+ ((run.status !== 'running' && read.status === 'running') || run.lastHeartbeatAtMs > read.lastHeartbeatAtMs))) runs.set(run.id, run);
37
+ }
38
+ return { runs: [...runs.values()].sort((a, b) => b.startedAtMs - a.startedAtMs || a.id.localeCompare(b.id)).slice(0, 50) };
39
+ }, refetchInterval: streaming ? 60_000 : 2000, refetchIntervalInBackground: true });
40
+ useEffect(() => {
41
+ const address = eventsAddress();
42
+ if (!address || !url) return;
43
+ const refresh = liveInvalidator(client, ['live-runs', url]);
44
+ const stop = listenLive(address, event => {
45
+ if (event.type === 'run') {
46
+ client.setQueryData<{ runs: LiveRun[] }>(['live-runs', url], held => ({ runs: [event.run, ...(held?.runs ?? []).filter(run => run.id !== event.run.id)]
47
+ .sort((a, b) => b.startedAtMs - a.startedAtMs || a.id.localeCompare(b.id)).slice(0, 50) }));
48
+ }
49
+ if (event.type === 'open' || (event.type === 'change' && event.change.scope === 'runs' && !event.change.liveRunId)) refresh();
50
+ });
51
+ return () => { stop(); refresh.cancel(); };
52
+ }, [client, url]);
53
+ const value = useMemo(() => {
54
+ const tests: LiveTest[] = [];
55
+ for (const run of [...(query.data?.runs ?? [])].sort((a, b) => b.startedAtMs - a.startedAtMs)) {
56
+ if (excluded.branches.includes(run.branch ?? 'Unknown branch') || excluded.versions.includes(run.sha ?? 'unknown') ||
57
+ excluded.environments.includes(run.environment || 'unspecified') || excluded.backends.length > 0) continue;
58
+ const checks = new Map([...run.recent, ...run.current].map(check => [check.id, check]));
59
+ for (const check of checks.values()) tests.push({ run, check: run.status !== 'running' && check.status === 'running'
60
+ ? { ...check, status: 'not-run', detail: run.reason ?? 'Run ended before this test completed.' } : check });
61
+ }
62
+ return { tests, index: indexLiveTests(tests), error: query.error?.message ?? null };
63
+ }, [query.data, query.error, excluded]);
64
+ return <LiveContext.Provider value={url ? value : EMPTY}>{children}</LiveContext.Provider>;
65
+ }
66
+
67
+ export function useLiveTests() {
68
+ return useContext(LiveContext);
69
+ }