@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,5 +1,11 @@
1
+ import { summarizeEvidence } from './evidence-detail.js';
1
2
  import { layerPlace } from './layer-axes.js';
2
- /** Advisory identity, never a cryptographic integrity check. */
3
+ /**
4
+ * A short, deterministic fingerprint of a spec's text, shaped `<length>:<hash>:<hash>` (the length in UTF-16 code
5
+ * units and two 32-bit hashes in hex). A layer records it per spec so a reader can warn when the spec changed
6
+ * since ("Potentially outdated: spec changed"). Advisory identity only: it is not collision resistant and never an
7
+ * integrity check.
8
+ */
3
9
  export function textFingerprint(text) {
4
10
  let a = 2166136261, b = 5381;
5
11
  for (let i = 0; i < text.length; i++) {
@@ -8,20 +14,37 @@ export function textFingerprint(text) {
8
14
  }
9
15
  return `${text.length}:${(a >>> 0).toString(16)}:${(b >>> 0).toString(16)}`;
10
16
  }
17
+ /**
18
+ * Normalises a check's or spec's path to its `specs/<path inside the specs directory>` form, so the same spec
19
+ * recorded from different checkouts is one spec.
20
+ *
21
+ * Converts `\` to `/`, drops a leading `./`, and drops everything before the first path segment named exactly
22
+ * `specs`. A path with no such segment is returned unchanged (apart from the first two steps).
23
+ *
24
+ * @example
25
+ * specPath('packages/app/specs/join.feature'); // 'specs/join.feature'
26
+ * specPath('myspecs/x.feature'); // 'myspecs/x.feature'
27
+ */
11
28
  export function specPath(path) {
12
- return path.replaceAll('\\', '/').replace(/^\.\//, '').replace(/^.*?(?=specs\/)/, '');
29
+ // Up to the first folder named exactly `specs`: `myspecs/x` is not in one.
30
+ return path.replaceAll('\\', '/').replace(/^\.\//, '').replace(/^(?:.*?\/)?(?=specs\/)/, '');
13
31
  }
32
+ /** Serialises parameters as JSON key/value pairs sorted by key, so equal parameter sets compare equal as strings. */
14
33
  export function parameterKey(parameters) {
15
34
  return JSON.stringify(Object.entries(parameters).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
16
35
  }
17
- /** Recorded timings qualify an outcome. They are a result of their own only when nothing else checked the same thing. */
36
+ /** Whether a part `kind` only qualifies an outcome (today only `performance`) and so is not counted as a check when anything else is recorded. */
18
37
  export function isDiagnosticKind(kind) {
19
38
  return kind === 'performance';
20
39
  }
40
+ /** A fresh status tally with every status at zero. */
21
41
  export function emptyCounts() {
22
42
  return { passed: 0, failed: 0, running: 0, 'not-run': 0 };
23
43
  }
24
- /** Outcomes of a layer: its checks, without the timings that only qualify them. */
44
+ /**
45
+ * Counts rows by status, leaving out the diagnostic rows (see `isDiagnosticKind`) that only qualify an outcome.
46
+ * When every row is diagnostic they are counted, so a layer of timings alone is not empty.
47
+ */
25
48
  export function outcomeCounts(rows) {
26
49
  const outcomes = rows.some(row => !isDiagnosticKind(row.kind)) ? rows.filter(row => !isDiagnosticKind(row.kind)) : rows;
27
50
  const counts = emptyCounts();
@@ -29,28 +52,49 @@ export function outcomeCounts(rows) {
29
52
  counts[row.status] += 1;
30
53
  return counts;
31
54
  }
32
- /** A layer as the explorer reads it: its origin, and each check as one row. */
55
+ /**
56
+ * Flattens a layer into what composition reads: a header (origin, normalised environment and parameters, counts,
57
+ * evidence summary), one row per check of every part, and the spec fingerprints it recorded.
58
+ *
59
+ * `backend` is the id of the backend the layer was read from; it only breaks ties between copies. Paths in rows and
60
+ * fingerprints go through `specPath`. When the layer has a recorded `source`, a row whose check id matches one of its
61
+ * tests also gets that test's `testId`, `variant` and duration. The result is `live: false`: a layer read from a
62
+ * store is never a run still in progress.
63
+ */
33
64
  export function layerEvidence(summary, backend) {
34
65
  const place = layerPlace(summary);
35
66
  const source = summary.source;
36
67
  const tests = new Map(source?.tests.map(test => [`${source.source}/${test.testId}`, test]) ?? []);
37
- const rows = summary.parts.flatMap(part => part.checks.map(check => {
38
- const test = tests.get(check.id);
39
- const variant = test ? { system: test.system, runtime: test.runtime, seed: test.seed } : {};
40
- return {
41
- kind: part.kind,
42
- id: check.id,
43
- title: check.title,
44
- status: check.status,
45
- specPath: check.specPath ? specPath(check.specPath) : null,
46
- detail: check.detail,
47
- measurements: check.measurements,
48
- testId: test?.testId ?? null,
49
- variant,
50
- durationMs: test?.durationMs ?? null,
51
- };
52
- }));
68
+ // A layer's checks repeat a handful of spec paths; each is normalized once.
69
+ const normalized = new Map();
70
+ const specOf = (path) => {
71
+ let found = normalized.get(path);
72
+ if (found === undefined)
73
+ normalized.set(path, found = specPath(path));
74
+ return found;
75
+ };
76
+ const rows = [];
77
+ for (const part of summary.parts) {
78
+ for (const check of part.checks) {
79
+ const test = tests.get(check.id);
80
+ const evidence = summarizeEvidence(check);
81
+ rows.push({
82
+ kind: part.kind,
83
+ id: check.id,
84
+ title: check.title,
85
+ status: check.status,
86
+ specPath: check.specPath ? specOf(check.specPath) : null,
87
+ detail: check.detail,
88
+ measurements: check.measurements,
89
+ testId: test?.testId ?? null,
90
+ variant: test ? { system: test.system, runtime: test.runtime, seed: test.seed } : {},
91
+ durationMs: test?.durationMs ?? check.durationMs ?? null,
92
+ ...(evidence ? { evidence } : {}),
93
+ });
94
+ }
95
+ }
53
96
  const recording = summary.recording;
97
+ const layerSummaryEvidence = summarizeEvidence(summary, ...summary.parts);
54
98
  const layer = {
55
99
  ...(summary.freshness ? { freshness: summary.freshness } : {}),
56
100
  id: summary.id,
@@ -67,6 +111,7 @@ export function layerEvidence(summary, backend) {
67
111
  live: false,
68
112
  counts: outcomeCounts(rows),
69
113
  execution: textFingerprint(layerExecutionKey(summary)),
114
+ ...(layerSummaryEvidence ? { evidence: layerSummaryEvidence } : {}),
70
115
  };
71
116
  const fingerprints = {};
72
117
  for (const spec of summary.specs)
@@ -74,6 +119,12 @@ export function layerEvidence(summary, backend) {
74
119
  fingerprints[specPath(spec.path)] = spec.fingerprint;
75
120
  return { id: summary.id, layer, rows, fingerprints };
76
121
  }
122
+ /**
123
+ * The freshness warning for one result, or `null` when it is current. Checked in this order: a live run has none;
124
+ * a `modified` or `unknown` layer; a spec that is not in the outline; a missing fingerprint on either side; a
125
+ * fingerprint that differs from the current spec; related files that changed; a layer sha that differs from the
126
+ * outline's (or either absent). Advisory only: a warning never changes a status.
127
+ */
77
128
  function warning(layer, path, current, outline, recorded, relatedChanged) {
78
129
  if (layer.live)
79
130
  return null;
@@ -97,8 +148,15 @@ const RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 };
97
148
  function completed(status) {
98
149
  return status === 'passed' || status === 'failed';
99
150
  }
100
- /** A newer completed result takes its slot. An unfinished one never replaces a completed one, and a tie fails. */
101
- function replaces(next, prior) {
151
+ /**
152
+ * Whether `next` should take the slot currently held by `prior`: the winner rule of the report contract.
153
+ *
154
+ * In order: a completed result (`passed`, `failed`) beats an unfinished one (`running`, `not-run`) whatever their
155
+ * times; otherwise the greater `recordedAtMs` wins; at the same time `failed` beats `passed` and `running` beats
156
+ * `not-run`; at the same status the greater `layerId|backendId` wins. The last step makes the order total, so the
157
+ * answer never depends on the order results are offered in. Returns `false` for two identical results.
158
+ */
159
+ export function replaces(next, prior) {
102
160
  const done = completed(next.row.status);
103
161
  const priorDone = completed(prior.row.status);
104
162
  if (done !== priorDone)
@@ -107,9 +165,22 @@ function replaces(next, prior) {
107
165
  return next.layer.recordedAtMs > prior.layer.recordedAtMs;
108
166
  if (RANK[next.row.status] !== RANK[prior.row.status])
109
167
  return RANK[next.row.status] > RANK[prior.row.status];
110
- return next.layer.id > prior.layer.id;
168
+ // The last tie-break is total, so no input order can change the answer.
169
+ return layerOrder(next.layer) > layerOrder(prior.layer);
170
+ }
171
+ function layerOrder(layer) {
172
+ return `${layer.id}|${layer.backend}`;
111
173
  }
112
- /** One linear pass. The newest completed result wins only its own check, environment and parameter slot. */
174
+ /**
175
+ * Lays layers over the outline of current specs and returns the winner of every slot.
176
+ *
177
+ * One linear pass over the rows of the layers (after `distinctEvidence`). The slot is producer, kind, spec path,
178
+ * check id, environment and parameters; `replaces` picks the winner, so the result does not depend on the order of
179
+ * `layers`. Branch, sha, backend and run never split a slot. Each result carries a freshness `warning`, computed
180
+ * against `outline`, that does not affect which result wins.
181
+ *
182
+ * @see docs/report-contract.md#how-layers-compose
183
+ */
113
184
  export function combineEvidence(outline, layers) {
114
185
  const specs = new Map(outline.specs.map(spec => [specPath(spec.path), { spec, results: [] }]));
115
186
  const changed = new Set(outline.changedFiles.map(specPath));
@@ -117,12 +188,13 @@ export function combineEvidence(outline, layers) {
117
188
  for (const { layer, rows, fingerprints } of distinctEvidence(layers)) {
118
189
  const scope = layer.packageDir;
119
190
  const related = outline.changedFiles.some(file => scope ? file.startsWith(`${scope}/`) : !specPath(file).startsWith('specs/'));
191
+ const keyOf = layerResultKeys(layer);
120
192
  for (const row of rows) {
121
193
  // Environment and the layer's other choices split the slot. A harness
122
194
  // seed does not: a focused rerun replaces the check it repeated.
123
195
  const parameters = layer.parameters;
124
196
  const path = row.specPath ?? '';
125
- const key = JSON.stringify([layer.producer, row.kind, path, row.id, layer.environment ?? '', parameterKey(parameters)]);
197
+ const key = keyOf(row);
126
198
  const result = { key, slot: slotKey(path, row.id), layer, row, parameters, warning: warning(layer, path, specs.get(path)?.spec, outline, fingerprints[path], related || changed.has(path)) };
127
199
  const prior = winners.get(key);
128
200
  if (!prior || replaces(result, prior))
@@ -134,26 +206,70 @@ export function combineEvidence(outline, layers) {
134
206
  (specs.get(result.row.specPath ?? '')?.results ?? unassigned).push(result);
135
207
  return { specs: [...specs.values()], unassigned };
136
208
  }
209
+ /**
210
+ * The key of the slot a row fills: a JSON array of producer, kind, spec path (`''` when none), check id, environment
211
+ * (`''` when none) and the sorted parameters. Two results with the same key compete; the winner is `replaces`.
212
+ */
213
+ export function resultKey(layer, row) {
214
+ return JSON.stringify([layer.producer, row.kind, row.specPath ?? '', row.id, layer.environment ?? '', parameterKey(layer.parameters)]);
215
+ }
216
+ /**
217
+ * `resultKey` for every row of one layer, which share all but three parts of it. The strings are exactly
218
+ * those `resultKey` makes (they are keys that persist in no store, but two ways of making one must agree),
219
+ * without sorting and writing the layer's parameters once per check.
220
+ */
221
+ export function layerResultKeys(layer) {
222
+ const producer = JSON.stringify(layer.producer);
223
+ const tail = `,${JSON.stringify(layer.environment ?? '')},${JSON.stringify(parameterKey(layer.parameters))}]`;
224
+ return row => `[${producer},${JSON.stringify(row.kind)},${JSON.stringify(row.specPath ?? '')},${JSON.stringify(row.id)}${tail}`;
225
+ }
226
+ /** The key of a check regardless of producer, environment and parameters: a JSON array of its spec path and check id. */
137
227
  export function slotKey(path, id) {
138
228
  return JSON.stringify([path, id]);
139
229
  }
230
+ /** Whether a layer id is a content digest (64 lowercase hex digits), that is an immutable layer rather than a mutable address. */
140
231
  function archived(id) {
141
232
  return /^[0-9a-f]{64}$/.test(id);
142
233
  }
143
- /** Suppress the mutable address of an execution already archived as an immutable layer. */
234
+ /**
235
+ * De-duplicates layer headers: the same layer held by several backends is one layer.
236
+ *
237
+ * Suppresses the mutable address of an execution already archived as an immutable layer. An
238
+ * immutable layer (64-hex id) held by several backends is one layer, reported by the backend whose id sorts
239
+ * first, whatever order they were read in. A mutable one is its backend's own and is kept once per backend.
240
+ * Returns the survivors, not necessarily in input order.
241
+ */
144
242
  export function distinctHeaders(layers) {
145
243
  const kept = new Set(layers.filter(layer => archived(layer.id)).map(layer => layer.execution));
146
- return [...new Map(layers.filter(layer => archived(layer.id) || !kept.has(layer.execution)).map(layer => [layer.id, layer])).values()];
244
+ const chosen = new Map();
245
+ for (const layer of layers) {
246
+ if (!archived(layer.id) && kept.has(layer.execution))
247
+ continue;
248
+ const key = archived(layer.id) ? layer.id : `${layer.backend ?? ''}|${layer.id}`;
249
+ const prior = chosen.get(key);
250
+ if (!prior || (layer.backend ?? '') < (prior.backend ?? ''))
251
+ chosen.set(key, layer);
252
+ }
253
+ return [...chosen.values()];
147
254
  }
255
+ /** Keeps, in input order, only the layers `distinctHeaders` keeps, and each `backend|id` once. */
148
256
  export function distinctEvidence(layers) {
149
- const kept = new Set(distinctHeaders(layers.map(entry => entry.layer)).map(layer => layer.id));
150
- return [...new Map(layers.filter(entry => kept.has(entry.id)).map(entry => [entry.id, entry])).values()];
257
+ const kept = new Set(distinctHeaders(layers.map(entry => entry.layer)).map(layer => `${layer.backend}|${layer.id}`));
258
+ const emitted = new Set();
259
+ return layers.filter(entry => {
260
+ const key = `${entry.layer.backend}|${entry.id}`;
261
+ if (!kept.has(key) || emitted.has(key))
262
+ return false;
263
+ emitted.add(key);
264
+ return true;
265
+ });
151
266
  }
152
- /** Suppress the mutable address of an execution already archived as an immutable layer. */
267
+ /** Drops each mutable layer whose execution is already archived as an immutable one, and keeps one summary per id. */
153
268
  export function distinctLayers(layers) {
154
269
  const kept = new Set(layers.filter(layer => archived(layer.id)).map(layerExecutionKey));
155
270
  return [...new Map(layers.filter(layer => archived(layer.id) || !kept.has(layerExecutionKey(layer))).map(layer => [layer.id, layer])).values()];
156
271
  }
272
+ /** A string equal for the mutable and the archived address of one execution: its producer, sha, time and parts. */
157
273
  export function layerExecutionKey(layer) {
158
274
  return JSON.stringify([layer.producer, layer.sha, layer.recordedAtMs, layer.parts]);
159
275
  }
@@ -1,6 +1,24 @@
1
+ /**
2
+ * @module Archiving a recorded run as a layer
3
+ *
4
+ * Turns one published source (a package bucket of recorded statechart or test runs with timelines and screenshots)
5
+ * into an immutable layer, so it composes with every other layer. It is the bridge from the commit-keyed recordings
6
+ * to the layer store and is called only by `publishSource`.
7
+ */
1
8
  import type { HtmlReportTest } from './recording-bundle.js';
2
9
  import { type BundleIndex } from './bundle.js';
3
10
  import type { ObjectStore } from './object-store.js';
4
11
  import type { SourceRecord } from './protocol.js';
5
12
  import type { PublishSourceArgs } from './publish.js';
13
+ /**
14
+ * Writes `source` and its recorded files as an immutable layer and returns it (see `publishLayerBundle`).
15
+ *
16
+ * The layer is recorded at the later of the commit's start and the source's `updatedAtMs`, with the commit's branch and
17
+ * run URL, the commit's spec inventory (empty when none), the source's parts, `parameters` from `args`, the environment
18
+ * normalised from `args.environment` or the `environment` parameter, and `recording: { type: 'layer' }`. Its bundle
19
+ * holds the commit's `frame.css` and each test's `tests/<testId>/report.json` and `tests/<testId>/files/<file>`.
20
+ * `bundle`, when given, is an index of those files already written for this source (keys under
21
+ * `commits/<sha>/tests/<source>/`); then only `frame.css` is written here and the index is re-keyed to the layer's
22
+ * layout. The same inputs give the same layer, so publishing twice is harmless. Store errors reject.
23
+ */
6
24
  export declare function archiveSourceLayer(store: ObjectStore, args: PublishSourceArgs, source: SourceRecord, tests: readonly HtmlReportTest[], bundle?: BundleIndex): Promise<import('./layer-types.js').ReportLayer>;
@@ -5,6 +5,17 @@ import { readInventory } from './spec-inventory.js';
5
5
  import { sourceParts } from './source-part.js';
6
6
  import { sourceEntries } from './source-entries.js';
7
7
  import { publishLayerBundle } from './layer-store.js';
8
+ /**
9
+ * Writes `source` and its recorded files as an immutable layer and returns it (see `publishLayerBundle`).
10
+ *
11
+ * The layer is recorded at the later of the commit's start and the source's `updatedAtMs`, with the commit's branch and
12
+ * run URL, the commit's spec inventory (empty when none), the source's parts, `parameters` from `args`, the environment
13
+ * normalised from `args.environment` or the `environment` parameter, and `recording: { type: 'layer' }`. Its bundle
14
+ * holds the commit's `frame.css` and each test's `tests/<testId>/report.json` and `tests/<testId>/files/<file>`.
15
+ * `bundle`, when given, is an index of those files already written for this source (keys under
16
+ * `commits/<sha>/tests/<source>/`); then only `frame.css` is written here and the index is re-keyed to the layer's
17
+ * layout. The same inputs give the same layer, so publishing twice is harmless. Store errors reject.
18
+ */
8
19
  export async function archiveSourceLayer(store, args, source, tests, bundle) {
9
20
  const [commit, inventory, css] = await Promise.all([readCommit(store, args.sha), readInventory(store, args.sha), readFrameCss(store, args.sha)]);
10
21
  const recordedAtMs = Math.max(commit?.startedAtMs ?? 0, source.updatedAtMs);
@@ -1,10 +1,52 @@
1
+ /**
2
+ * @module Layer storage
3
+ *
4
+ * Reads and writes layers in an `ObjectStore`: the immutable manifest at `layers/manifests/<id>.json`, the catalog
5
+ * entry at `layers/index/<inverted-time>-<id>.json`, and the captured files under `layers/blobs/`. A layer is written
6
+ * bottom up (files, then manifest, then catalog entry) so a reader never meets an entry whose content has not arrived,
7
+ * and every key is derived from content so two producers never write different bytes to one key.
8
+ *
9
+ * @see docs/report-contract.md#where-bytes-go
10
+ */
1
11
  import { type BundleIndex, type BundleEntry } from './bundle.js';
2
12
  import type { ObjectStore } from './object-store.js';
3
13
  import type { LayerSummary, ReportLayer } from './layer-types.js';
14
+ /** The key prefix under which every layer's catalog entry is written. */
4
15
  export declare const LAYER_INDEX = "layers/index/";
16
+ /** The store key of a layer's manifest. Throws `Invalid layer id` unless `id` is 64 lowercase hex digits, so an id can never address another key. */
5
17
  export declare function layerKey(id: string): string;
18
+ /**
19
+ * Writes a layer whose captured files arrive as a stream, then its manifest and catalog entry, and returns it.
20
+ *
21
+ * The files are packed into content-addressed blobs under `layers/blobs/` first (`writeBundle`), then
22
+ * `publishLayerBundle` does the rest. The id is computed from the content, so publishing the same layer again
23
+ * writes the same keys.
24
+ */
6
25
  export declare function publishLayer(store: ObjectStore, summary: Omit<LayerSummary, 'id'>, entries: AsyncIterable<BundleEntry>): Promise<ReportLayer>;
26
+ /**
27
+ * The key of a layer's catalog entry: `layers/index/<16 digits>-<id>.json`, where the digits are
28
+ * `Number.MAX_SAFE_INTEGER - floor(max(0, recordedAtMs))` zero-padded. The time is inverted so a listing in key order
29
+ * is newest first. The entry makes a layer visible, so it is written last, after the layer and its files.
30
+ */
31
+ export declare function layerCatalogKey(recordedAtMs: number, id: string): string;
32
+ /**
33
+ * Writes a layer whose captured files are already stored (`bundle` locates them) and returns it with its id.
34
+ *
35
+ * The id is the SHA-256 of the JSON of `{ schemaVersion, ...summary, bundle }`, so recording the same thing the same
36
+ * way gives the same id. Writes the manifest only if `head` says it is absent, then always writes the catalog entry
37
+ * (same key, same bytes on a repeat), so a retry is harmless and a crash between the two leaves a manifest no catalog
38
+ * entry names, never the reverse. Store errors reject; nothing is validated here (the publishers do that).
39
+ */
7
40
  export declare function publishLayerBundle(store: ObjectStore, summary: Omit<LayerSummary, 'id'>, bundle: BundleIndex): Promise<ReportLayer>;
41
+ /**
42
+ * Reads and validates a layer manifest, returning it without `schemaVersion`, or `null` when no manifest exists.
43
+ *
44
+ * A manifest that is present but cannot be read is an error naming why, never a half-read layer: it throws when the
45
+ * bytes are not JSON, fail the layer schema, or are a newer major schema version. Callers turn that into a notice,
46
+ * which fails the gate. Throws `Invalid layer id` for a malformed `id`. The id is not recomputed from the content.
47
+ */
8
48
  export declare function readLayer(store: ObjectStore, id: string): Promise<ReportLayer | null>;
49
+ /** Reads one captured file (a screenshot, a frame, a recorded report) of a layer, or `null` when the layer or the file is missing. Throws like `readLayer`. */
9
50
  export declare function readLayerFile(store: ObjectStore, id: string, file: string): Promise<BundleEntry | null>;
51
+ /** Drops a layer's bundle index, leaving the summary composition reads. */
10
52
  export declare function layerSummary(layer: ReportLayer): LayerSummary;
@@ -1,35 +1,93 @@
1
+ /**
2
+ * @module Layer storage
3
+ *
4
+ * Reads and writes layers in an `ObjectStore`: the immutable manifest at `layers/manifests/<id>.json`, the catalog
5
+ * entry at `layers/index/<inverted-time>-<id>.json`, and the captured files under `layers/blobs/`. A layer is written
6
+ * bottom up (files, then manifest, then catalog entry) so a reader never meets an entry whose content has not arrived,
7
+ * and every key is derived from content so two producers never write different bytes to one key.
8
+ *
9
+ * @see docs/report-contract.md#where-bytes-go
10
+ */
1
11
  import { createBundleReader, writeBundle } from './bundle.js';
2
12
  import { readUtf8, writeUtf8 } from './object-io.js';
13
+ import { LAYER_SCHEMA_VERSION, parseLayerDocument } from '../../schema/src/layer.js';
14
+ /** The key prefix under which every layer's catalog entry is written. */
3
15
  export const LAYER_INDEX = 'layers/index/';
16
+ /** The store key of a layer's manifest. Throws `Invalid layer id` unless `id` is 64 lowercase hex digits, so an id can never address another key. */
4
17
  export function layerKey(id) {
5
18
  if (!/^[0-9a-f]{64}$/.test(id))
6
19
  throw new Error('Invalid layer id');
7
20
  return `layers/manifests/${id}.json`;
8
21
  }
22
+ /**
23
+ * Writes a layer whose captured files arrive as a stream, then its manifest and catalog entry, and returns it.
24
+ *
25
+ * The files are packed into content-addressed blobs under `layers/blobs/` first (`writeBundle`), then
26
+ * `publishLayerBundle` does the rest. The id is computed from the content, so publishing the same layer again
27
+ * writes the same keys.
28
+ */
9
29
  export async function publishLayer(store, summary, entries) {
10
30
  const bundle = await writeBundle(store, entries, 'layers/blobs/');
11
31
  return publishLayerBundle(store, summary, bundle);
12
32
  }
33
+ /**
34
+ * The key of a layer's catalog entry: `layers/index/<16 digits>-<id>.json`, where the digits are
35
+ * `Number.MAX_SAFE_INTEGER - floor(max(0, recordedAtMs))` zero-padded. The time is inverted so a listing in key order
36
+ * is newest first. The entry makes a layer visible, so it is written last, after the layer and its files.
37
+ */
38
+ export function layerCatalogKey(recordedAtMs, id) {
39
+ return `${LAYER_INDEX}${String(Number.MAX_SAFE_INTEGER - Math.max(0, Math.floor(recordedAtMs))).padStart(16, '0')}-${id}.json`;
40
+ }
41
+ /**
42
+ * Writes a layer whose captured files are already stored (`bundle` locates them) and returns it with its id.
43
+ *
44
+ * The id is the SHA-256 of the JSON of `{ schemaVersion, ...summary, bundle }`, so recording the same thing the same
45
+ * way gives the same id. Writes the manifest only if `head` says it is absent, then always writes the catalog entry
46
+ * (same key, same bytes on a repeat), so a retry is harmless and a crash between the two leaves a manifest no catalog
47
+ * entry names, never the reverse. Store errors reject; nothing is validated here (the publishers do that).
48
+ */
13
49
  export async function publishLayerBundle(store, summary, bundle) {
14
- const body = JSON.stringify({ ...summary, bundle });
50
+ // The id is the digest of exactly this document without it, so every producer that records the same thing writes the same layer.
51
+ const body = JSON.stringify({ schemaVersion: LAYER_SCHEMA_VERSION, ...summary, bundle });
15
52
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body));
16
53
  const id = [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join('');
17
54
  const layer = { ...summary, id, bundle };
18
55
  // Immutable manifest first, independent catalog entry last: concurrent producers never overwrite one another.
19
56
  if (!(await store.head(layerKey(id))))
20
- await writeUtf8(store, layerKey(id), JSON.stringify(layer), 'application/json');
21
- const inverse = String(Number.MAX_SAFE_INTEGER - Math.max(0, Math.floor(summary.recordedAtMs))).padStart(16, '0');
22
- await writeUtf8(store, `${LAYER_INDEX}${inverse}-${id}.json`, JSON.stringify({ id }), 'application/json');
57
+ await writeUtf8(store, layerKey(id), JSON.stringify({ schemaVersion: LAYER_SCHEMA_VERSION, ...layer }), 'application/json');
58
+ await writeUtf8(store, layerCatalogKey(summary.recordedAtMs, id), JSON.stringify({ id }), 'application/json');
23
59
  return layer;
24
60
  }
61
+ /**
62
+ * Reads and validates a layer manifest, returning it without `schemaVersion`, or `null` when no manifest exists.
63
+ *
64
+ * A manifest that is present but cannot be read is an error naming why, never a half-read layer: it throws when the
65
+ * bytes are not JSON, fail the layer schema, or are a newer major schema version. Callers turn that into a notice,
66
+ * which fails the gate. Throws `Invalid layer id` for a malformed `id`. The id is not recomputed from the content.
67
+ */
25
68
  export async function readLayer(store, id) {
26
69
  const raw = await readUtf8(store, layerKey(id));
27
- return raw === null ? null : JSON.parse(raw);
70
+ if (raw === null)
71
+ return null;
72
+ let json;
73
+ try {
74
+ json = JSON.parse(raw);
75
+ }
76
+ catch {
77
+ throw new Error(`Layer ${id.slice(0, 12)} is not JSON`);
78
+ }
79
+ const parsed = parseLayerDocument(json);
80
+ if (!parsed.ok)
81
+ throw new Error(`Layer ${id.slice(0, 12)}: ${parsed.reason}`);
82
+ const { schemaVersion: _version, ...layer } = parsed.layer;
83
+ return layer;
28
84
  }
85
+ /** Reads one captured file (a screenshot, a frame, a recorded report) of a layer, or `null` when the layer or the file is missing. Throws like `readLayer`. */
29
86
  export async function readLayerFile(store, id, file) {
30
87
  const layer = await readLayer(store, id);
31
88
  return layer ? createBundleReader(store, layer.bundle)(file) : null;
32
89
  }
90
+ /** Drops a layer's bundle index, leaving the summary composition reads. */
33
91
  export function layerSummary(layer) {
34
92
  const { bundle: _, ...summary } = layer;
35
93
  return summary;
@@ -1,21 +1,47 @@
1
+ /**
2
+ * @module Layer types
3
+ *
4
+ * The in-memory shapes of a layer: `LayerSummary` (everything but its captured files), `ReportLayer` (the summary plus
5
+ * the index of those files) and `ReportOutline` (the specs a report is laid over). The stored form is the layer
6
+ * document of `packages/schema/src/layer.ts`; these types are what the reader and publisher code hands around after
7
+ * parsing it. Fields follow the layer document, so adding a field here means adding an optional field there.
8
+ *
9
+ * @see docs/report-contract.md#the-layer-document
10
+ */
1
11
  import type { BundleIndex } from './bundle.js';
2
12
  import type { ReportPart, SpecEntry } from './explorer-types.js';
3
13
  import type { SourceRecord } from './protocol.js';
14
+ /** String choices (such as `{ device: 'iphone-15' }`) that make a result its own slot. Compared as sorted key/value pairs. */
4
15
  export type LayerParameters = Readonly<Record<string, string>>;
5
- export type LayerSummary = {
16
+ /** What a layer may carry as a whole. All optional and open: see docs/extending.md. */
17
+ export type LayerExtras = Partial<Pick<import('../../schema/src/layer.js').LayerDocument, 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>>;
18
+ /** A layer without its captured files: what composition and the viewer's lists read. */
19
+ export type LayerSummary = LayerExtras & {
20
+ /** `modified`: recorded from a checkout with uncommitted changes. `unknown`: origin not captured. Absent for a clean checkout. Advisory; never changes a status. */
6
21
  readonly freshness?: 'unknown' | 'modified';
22
+ /** The layer's content digest: 64 lowercase hex digits. Opaque to readers. Legacy commit-keyed recordings use `part-<sha>-<id>` or `source-<sha>-<id>` instead. */
7
23
  readonly id: string;
24
+ /** When the checks ran, epoch milliseconds. The newest completed result wins its slot. */
8
25
  readonly recordedAtMs: number;
26
+ /** The stable name of the suite or tool that made the layer. Part of the slot. */
9
27
  readonly producer: string;
28
+ /** Provenance: the commit recorded, or `null`. Never part of the slot. */
10
29
  readonly sha: string | null;
30
+ /** Provenance: the branch recorded, or `null`. Never part of the slot. */
11
31
  readonly branch: string | null;
12
32
  /** The client this layer checked, when it said. Normalized when the layer is read. */
13
33
  readonly environment?: string | null;
34
+ /** Provenance: the CI run that produced the layer, or `null`. Never part of the slot. */
14
35
  readonly runUrl: string | null;
36
+ /** Free choices that make a result its own slot. The `environment` parameter is read as the environment (see `layerPlace`). */
15
37
  readonly parameters: LayerParameters;
38
+ /** The specs the layer was recorded against, with fingerprints of their text. */
16
39
  readonly specs: readonly SpecEntry[];
40
+ /** The checks, grouped by producer-defined kind. */
17
41
  readonly parts: readonly ReportPart[];
42
+ /** The recorded statechart run behind the layer (timelines and tests), or `null` for a layer of plain checks. */
18
43
  readonly source: SourceRecord | null;
44
+ /** Where the layer's timelines and captured files are read from: its own bundle (`layer`), an older commit-keyed recording (`commit`), or nowhere (`null`). */
19
45
  readonly recording: {
20
46
  readonly type: 'layer';
21
47
  } | {
@@ -24,11 +50,16 @@ export type LayerSummary = {
24
50
  readonly source: string;
25
51
  } | null;
26
52
  };
53
+ /** A layer with the index that locates its captured files (screenshots, frames) in the store. */
27
54
  export type ReportLayer = LayerSummary & {
28
55
  readonly bundle: BundleIndex;
29
56
  };
57
+ /** The current specs a report is laid over, and the commit and changes freshness warnings are judged against. */
30
58
  export type ReportOutline = {
59
+ /** The specs that make the outline of the report. A spec here with no layer shows as having no results. */
31
60
  readonly specs: readonly SpecEntry[];
61
+ /** The commit the specs are from, or `null` when unknown. */
32
62
  readonly sha: string | null;
63
+ /** Paths changed since the layers were recorded; a result touching one is warned as potentially outdated. */
33
64
  readonly changedFiles: readonly string[];
34
65
  };
@@ -1,20 +1,63 @@
1
+ /**
2
+ * @module Layers HTTP routes
3
+ *
4
+ * The read-only HTTP API the viewer uses to see layers: a paged list of layer headers merged from every backend, the
5
+ * rows of one layer, its attachments and recorded timelines, the live runs still writing on this machine, and the
6
+ * spec outline. It is mounted under `/api/reports` by `reportsHost` and by the Atlas server. Immutable layers (64-hex
7
+ * ids) are read once and cached for good and answered with long-lived `immutable` cache headers; mutable commit-keyed
8
+ * recordings are re-read after a short wait. A backend that cannot be listed or a layer that cannot be read becomes a
9
+ * notice in the page, never a silent omission and never a pass. Layers the host's `include` filter rejects are absent
10
+ * from every route here, so a filter cannot be undone by asking for history.
11
+ *
12
+ * @see docs/server.md
13
+ * @see docs/report-contract.md#failure-behaviour
14
+ */
1
15
  import { Hono } from 'hono';
2
16
  import type { ObjectStore } from './object-store.js';
3
17
  import type { ReportsAppDeps } from './reports-route.js';
4
- /** A further store whose layers join the combined report, beside the local and the shared one. */
18
+ /** A further store whose layers join the combined report, beside the local and the shared one. Passed as `backends` in `ReportsAppDeps`. */
5
19
  export type ReportBackend = {
6
- /** Stable and unique: letters, digits and hyphens. `local` and `shared` are taken. */
20
+ /** Stable and unique: lowercase letters, digits and hyphens. `local` and `shared` are taken. Appears in cursors and URLs. */
7
21
  readonly id: string;
22
+ /** Human name shown for the backend in the viewer. */
8
23
  readonly label: string;
24
+ /** Where the backend's layers are read from. */
9
25
  readonly store: ObjectStore;
10
26
  };
11
27
  type Backend = ReportBackend & {
12
28
  readonly live: boolean;
13
29
  };
30
+ /** The id of the backend for this machine's own store (`localStore`), where runs may still be writing. */
14
31
  export declare const LOCAL_BACKEND = "local";
32
+ /** The id of the backend for the shared reports store (`store`). */
15
33
  export declare const SHARED_BACKEND = "shared";
16
- /** The stores that contribute layers, in the order they answer for a recording. */
34
+ /**
35
+ * The stores that contribute layers, in the order they answer for a recording: `localStore` (id `local`, live), then
36
+ * `store` (id `shared`) when it is a different store, then each of `backends`.
37
+ *
38
+ * Throws `Invalid report backend id: "<id>"` when an extra backend's id is not lowercase letters, digits and hyphens
39
+ * or repeats an earlier id (including `local` and `shared`).
40
+ */
17
41
  export declare function reportBackends(deps: ReportsAppDeps): Backend[];
18
- /** Bounded pages of layer origins, each layer's checks read once, and the runs still writing. */
42
+ /**
43
+ * Builds the Hono app that serves layers: bounded pages of layer origins, each layer's checks read once, and the
44
+ * runs still writing. Throws (as `reportBackends`) when a backend id is invalid.
45
+ *
46
+ * Routes, relative to where it is mounted:
47
+ * - `GET /outline` the current specs as `{ specs, sha, changedFiles }` (503 when the host has no outline), cached 30 s.
48
+ * - `GET /outline/spec?path=` one outline spec's text; 404 for a path not in the outline.
49
+ * - `GET /evidence?cursor=&retry=` one page of layer headers (`EvidencePage`) from 40 catalog entries across all
50
+ * backends: immutable layers newest first, then commit-keyed recordings. Without a cursor it also carries the live
51
+ * runs of this machine. `cursor` is the previous page's; 409 when it no longer matches the listing (refresh).
52
+ * Any `retry` value re-lists backends that were unavailable now instead of after the wait.
53
+ * - `GET /evidence/rows/:backend/:id` the checks and spec fingerprints of one layer (`LayerRows`).
54
+ * - `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer, or one check's detail.
55
+ * - `GET /layers/:id/blobs/*` an attachment of an immutable layer by its bundle key, served by `attachmentResponse`.
56
+ * - `GET /layers/:id/*` the recorded `frame.css`, `tests/<test>/report.json` and `tests/<test>/files/<frame>` of a layer.
57
+ * - `GET /live/:source/frame.css` and `GET /live/:source/tests/:testId/*` the same for a run still writing (uncached).
58
+ *
59
+ * Directory listings are cached for 30 s (1.5 s for the local store). A listing that fails keeps the previous keys and
60
+ * marks the backend unavailable; the page then carries a notice.
61
+ */
19
62
  export declare function createLayersApp(deps: ReportsAppDeps): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
20
63
  export {};