@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,11 +1,27 @@
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 { createBundleReader } from './bundle.js';
3
17
  import { eachLimited } from './completion-order.js';
4
18
  import { createLiveEvidence, liveRunRecord } from './evidence-live.js';
5
19
  import { isFramePath, isKeySegment } from './keys.js';
6
20
  import { catalogEntries, catalogLayers } from './layer-catalog.js';
21
+ import { attachmentResponse } from './attachment-response.js';
22
+ import { checkDetailOf, runEvidence } from './evidence-detail.js';
7
23
  import { layerEvidence } from './layer-merge.js';
8
- import { readLayer } from './layer-store.js';
24
+ import { layerSummary, readLayer } from './layer-store.js';
9
25
  import { readBytes, readUtf8 } from './object-io.js';
10
26
  import { SPEC_RUN_ID } from './publish.js';
11
27
  import { testFrameKey, testReportKey } from './recording-keys.js';
@@ -13,9 +29,17 @@ const TTL = 30_000;
13
29
  const LOCAL_TTL = 1_500;
14
30
  const PAGE = 40;
15
31
  const CACHED_ENTRIES = 4_096;
32
+ /** The id of the backend for this machine's own store (`localStore`), where runs may still be writing. */
16
33
  export const LOCAL_BACKEND = 'local';
34
+ /** The id of the backend for the shared reports store (`store`). */
17
35
  export const SHARED_BACKEND = 'shared';
18
- /** The stores that contribute layers, in the order they answer for a recording. */
36
+ /**
37
+ * The stores that contribute layers, in the order they answer for a recording: `localStore` (id `local`, live), then
38
+ * `store` (id `shared`) when it is a different store, then each of `backends`.
39
+ *
40
+ * Throws `Invalid report backend id: "<id>"` when an extra backend's id is not lowercase letters, digits and hyphens
41
+ * or repeats an earlier id (including `local` and `shared`).
42
+ */
19
43
  export function reportBackends(deps) {
20
44
  const backends = [];
21
45
  if (deps.localStore)
@@ -29,12 +53,34 @@ export function reportBackends(deps) {
29
53
  }
30
54
  return backends;
31
55
  }
56
+ /** Whether a layer id is a content digest (64 lowercase hex digits), that is an immutable layer. */
32
57
  function archived(id) {
33
58
  return /^[0-9a-f]{64}$/.test(id);
34
59
  }
35
- /** Bounded pages of layer origins, each layer's checks read once, and the runs still writing. */
60
+ /**
61
+ * Builds the Hono app that serves layers: bounded pages of layer origins, each layer's checks read once, and the
62
+ * runs still writing. Throws (as `reportBackends`) when a backend id is invalid.
63
+ *
64
+ * Routes, relative to where it is mounted:
65
+ * - `GET /outline` the current specs as `{ specs, sha, changedFiles }` (503 when the host has no outline), cached 30 s.
66
+ * - `GET /outline/spec?path=` one outline spec's text; 404 for a path not in the outline.
67
+ * - `GET /evidence?cursor=&retry=` one page of layer headers (`EvidencePage`) from 40 catalog entries across all
68
+ * backends: immutable layers newest first, then commit-keyed recordings. Without a cursor it also carries the live
69
+ * runs of this machine. `cursor` is the previous page's; 409 when it no longer matches the listing (refresh).
70
+ * Any `retry` value re-lists backends that were unavailable now instead of after the wait.
71
+ * - `GET /evidence/rows/:backend/:id` the checks and spec fingerprints of one layer (`LayerRows`).
72
+ * - `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer, or one check's detail.
73
+ * - `GET /layers/:id/blobs/*` an attachment of an immutable layer by its bundle key, served by `attachmentResponse`.
74
+ * - `GET /layers/:id/*` the recorded `frame.css`, `tests/<test>/report.json` and `tests/<test>/files/<frame>` of a layer.
75
+ * - `GET /live/:source/frame.css` and `GET /live/:source/tests/:testId/*` the same for a run still writing (uncached).
76
+ *
77
+ * Directory listings are cached for 30 s (1.5 s for the local store). A listing that fails keeps the previous keys and
78
+ * marks the backend unavailable; the page then carries a notice.
79
+ */
36
80
  export function createLayersApp(deps) {
37
81
  const backends = reportBackends(deps);
82
+ // Layers the host leaves out are left out of every route, so a filter cannot be undone by asking for history.
83
+ const include = deps.include ?? (() => true);
38
84
  const listings = backends.map(() => undefined);
39
85
  let pending = null;
40
86
  const loaded = new Map();
@@ -109,8 +155,9 @@ export function createLayersApp(deps) {
109
155
  let reader = readers.get(id);
110
156
  if (!reader) {
111
157
  for (const backend of backends) {
112
- const layer = await readLayer(backend.store, id);
113
- if (!layer)
158
+ const layer = await readLayer(backend.store, id).catch(() => null);
159
+ // A layer the host leaves out has no files to give either.
160
+ if (!layer || !include(layerEvidence(layerSummary(layer), backend.id).layer))
114
161
  continue;
115
162
  reader = createBundleReader(backend.store, layer.bundle);
116
163
  if (readers.size >= 8)
@@ -185,7 +232,8 @@ export function createLayersApp(deps) {
185
232
  await eachLimited(selected, 8, async (entry) => {
186
233
  try {
187
234
  for (const evidence of await entryEvidence(entry))
188
- layers.push(evidence.layer);
235
+ if (include(evidence.layer))
236
+ layers.push(evidence.layer);
189
237
  }
190
238
  catch {
191
239
  notices.push('A report layer could not be loaded. Its outcome is unknown.');
@@ -207,11 +255,42 @@ export function createLayersApp(deps) {
207
255
  const backend = backends.findIndex(known => known.id === c.req.param('backend'));
208
256
  const id = c.req.param('id');
209
257
  const evidence = backend < 0 ? null : await findEvidence(backend, id);
210
- if (!evidence)
258
+ if (!evidence || !include(evidence.layer))
211
259
  return c.notFound();
212
260
  const rows = { id: evidence.id, rows: evidence.rows, fingerprints: evidence.fingerprints };
213
261
  return c.json(rows, 200, { 'cache-control': archived(id) ? 'public, max-age=31536000, immutable' : 'no-cache' });
214
262
  });
263
+ // The files a layer carries for its checks: screenshots, videos, traces, logs, snapshots, anything.
264
+ app.get('/layers/:id/blobs/*', async (c) => {
265
+ const id = c.req.param('id');
266
+ let key;
267
+ try {
268
+ key = decodeURIComponent(new URL(c.req.url).pathname.split(`/layers/${id}/blobs/`)[1] ?? '');
269
+ }
270
+ catch {
271
+ return c.json({ error: 'Invalid attachment path' }, 400);
272
+ }
273
+ if (!archived(id) || !key || key.split('/').some(part => !part || part === '.' || part === '..'))
274
+ return c.notFound();
275
+ const entry = await file(id, key);
276
+ return entry ? attachmentResponse(c.req.raw, entry, `"${id.slice(0, 16)}-${key.replace(/[^\w.-]/g, '_')}"`) : c.notFound();
277
+ });
278
+ // Everything one result carries: its timeline, attachments, annotations, retries and extensions.
279
+ app.get('/evidence/detail/:backend/:id', async (c) => {
280
+ const backend = backends.find(known => known.id === c.req.param('backend'));
281
+ const id = c.req.param('id');
282
+ if (!backend || !archived(id))
283
+ return c.notFound();
284
+ const layer = await readLayer(backend.store, id).catch(() => null);
285
+ if (!layer || !include(layerEvidence(layerSummary(layer), backend.id).layer))
286
+ return c.notFound();
287
+ const checkId = c.req.query('check');
288
+ const headers = { 'cache-control': 'public, max-age=31536000, immutable' };
289
+ if (checkId === undefined)
290
+ return c.json({ layer: id, run: runEvidence(layer) }, 200, headers);
291
+ const detail = checkDetailOf(layer, checkId, c.req.query('kind'));
292
+ return detail ? c.json(detail, 200, headers) : c.notFound();
293
+ });
215
294
  app.get('/layers/:id/*', async (c) => {
216
295
  const id = c.req.param('id');
217
296
  let path;
@@ -0,0 +1,34 @@
1
+ import type { z } from 'zod/v4';
2
+ import type { LiveCheck, LiveRunStartSchema } from '../../schema/src/live-run.js';
3
+ /** Fixed identity of a live invocation; a UUID is generated when omitted. */
4
+ export type LiveIdentity = Omit<z.input<typeof LiveRunStartSchema>, 'id'> & {
5
+ id?: string;
6
+ };
7
+ /** Transport and batching configuration. Defaults: 250 ms batches, 5 s heartbeats and 10 s request timeouts. */
8
+ export type LiveReporterOptions = {
9
+ url: string;
10
+ project?: string;
11
+ token?: string;
12
+ flushMs?: number;
13
+ heartbeatMs?: number;
14
+ timeoutMs?: number;
15
+ onError?: (error: Error) => void;
16
+ };
17
+ /** Producer handle. check never throws transport errors into a test; finish flushes and rejects if publication failed. */
18
+ export type LiveReporter = {
19
+ readonly id: string;
20
+ readonly finishedAtMs?: number;
21
+ check(check: LiveCheck): void;
22
+ flush(): Promise<void>;
23
+ finish(status?: 'completed' | 'interrupted', reason?: string): Promise<void>;
24
+ };
25
+ /**
26
+ * Start publishing immediately, batch changing checks, and send independent heartbeats while the producer is alive.
27
+ * The same failed batch is retried with the same sequence and content; never silently replace a lost update.
28
+ * @example
29
+ * const run = createLiveReporter({ producer: 'unit', plannedChecks: 10 }, { url: 'http://localhost:5180' });
30
+ * run.check({ id: 'a', title: 'Adds a todo', status: 'running' });
31
+ * run.check({ id: 'a', title: 'Adds a todo', status: 'passed' });
32
+ * await run.finish();
33
+ */
34
+ export declare function createLiveReporter(identity: LiveIdentity, options: LiveReporterOptions): LiveReporter;
@@ -0,0 +1,122 @@
1
+ /** @module Reporting a live run to Atlas
2
+ * Streams bounded check deltas while tests execute. Heartbeats are independent of progress; finished results are
3
+ * published by the server as ordinary immutable layers. A final flush is awaited before a runner exits.
4
+ */
5
+ import { randomUUID } from 'node:crypto';
6
+ /**
7
+ * Start publishing immediately, batch changing checks, and send independent heartbeats while the producer is alive.
8
+ * The same failed batch is retried with the same sequence and content; never silently replace a lost update.
9
+ * @example
10
+ * const run = createLiveReporter({ producer: 'unit', plannedChecks: 10 }, { url: 'http://localhost:5180' });
11
+ * run.check({ id: 'a', title: 'Adds a todo', status: 'running' });
12
+ * run.check({ id: 'a', title: 'Adds a todo', status: 'passed' });
13
+ * await run.finish();
14
+ */
15
+ export function createLiveReporter(identity, options) {
16
+ const base = new URL(options.url);
17
+ if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password || base.search || base.hash)
18
+ throw new Error('Atlas live reporter needs an HTTP(S) server URL without credentials, query or fragment.');
19
+ const id = identity.id ?? randomUUID();
20
+ const endpoint = `${base.href.replace(/\/$/, '')}/api/live/${id}`;
21
+ let sequence = 0;
22
+ let finishedAtMs;
23
+ let closed = false;
24
+ let timer;
25
+ let error;
26
+ const pending = new Map();
27
+ const known = new Map();
28
+ let failed;
29
+ const reportError = (caught) => { error = caught instanceof Error ? caught : new Error(String(caught)); options.onError?.(error); };
30
+ async function request(method, payload, suffix = '') {
31
+ const text = payload === undefined ? undefined : JSON.stringify(payload);
32
+ for (let attempt = 0;; attempt++) {
33
+ try {
34
+ const response = await fetch(endpoint + suffix, { method, redirect: 'error', signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),
35
+ headers: { 'content-type': 'application/json', ...(options.project ? { 'x-atlas-project': options.project } : {}), ...(options.token ? { authorization: `Bearer ${options.token}` } : {}) }, body: text });
36
+ const status = response.status;
37
+ const json = response.headers.get('content-type')?.includes('application/json');
38
+ if (response.ok && !json) {
39
+ await response.body?.cancel();
40
+ throw Object.assign(new Error('Atlas live reporter: the endpoint is not an Atlas server. Start or restart `bun atlas`.'), { permanent: true });
41
+ }
42
+ if (!response.ok) {
43
+ await response.body?.cancel();
44
+ throw Object.assign(new Error(`Atlas live reporter: HTTP ${status}`), { permanent: status < 500 && status !== 429 });
45
+ }
46
+ return await response.json();
47
+ }
48
+ catch (caught) {
49
+ if (attempt >= 2 || caught.permanent)
50
+ throw caught;
51
+ await new Promise(resolve => setTimeout(resolve, 250 * 2 ** attempt));
52
+ }
53
+ }
54
+ }
55
+ const ready = request('POST', { ...identity, id });
56
+ void ready.catch(reportError);
57
+ let queue = Promise.resolve();
58
+ let draining;
59
+ const flush = () => {
60
+ if (draining)
61
+ return draining;
62
+ queue = queue.then(async () => {
63
+ await ready;
64
+ while (failed || pending.size) {
65
+ if (!failed) {
66
+ const checks = [...pending.values()].slice(0, 1000);
67
+ checks.forEach(check => pending.delete(check.id));
68
+ failed = { sequence: sequence + 1, checks };
69
+ }
70
+ await request('PATCH', failed);
71
+ sequence = failed.sequence;
72
+ failed = undefined;
73
+ }
74
+ error = undefined;
75
+ }).catch(reportError);
76
+ draining = queue.finally(() => { draining = undefined; });
77
+ return draining;
78
+ };
79
+ const heartbeat = setInterval(() => { if (!closed)
80
+ void ready.then(async () => { await request('POST', undefined, '/heartbeat'); if (!closed && (failed || pending.size))
81
+ void flush(); }).catch(caught => { if (!closed)
82
+ reportError(caught); }); }, options.heartbeatMs ?? 5000);
83
+ heartbeat.unref?.();
84
+ const check = (given) => {
85
+ if (closed)
86
+ throw new Error('This live reporter is closed.');
87
+ const check = { id: given.id, title: given.title.slice(0, 2048), status: given.status, detail: (given.detail ?? '').slice(0, 8000),
88
+ ...(given.specPath ? { specPath: given.specPath } : {}), ...(given.durationMs === undefined ? {} : { durationMs: given.durationMs }),
89
+ ...(given.steps?.length ? { steps: given.steps.slice(-20).map(step => ({ name: step.name.slice(0, 2048), ...(step.status ? { status: step.status.slice(0, 100) } : {}) })) } : {}) };
90
+ if (JSON.stringify(known.get(check.id)) === JSON.stringify(check))
91
+ return;
92
+ known.set(check.id, check);
93
+ pending.set(check.id, check);
94
+ if (!timer) {
95
+ timer = setTimeout(() => { timer = undefined; void flush(); }, options.flushMs ?? 250);
96
+ timer.unref?.();
97
+ }
98
+ };
99
+ return { id, get finishedAtMs() { return finishedAtMs; }, check, flush, async finish(status = 'completed', reason) {
100
+ if (closed) {
101
+ await queue;
102
+ if (error)
103
+ throw error;
104
+ return;
105
+ }
106
+ const unfinished = [...known.values()].filter(check => check.status === 'running');
107
+ if (unfinished.length) {
108
+ status = 'interrupted';
109
+ reason ??= 'The producer ended without a final outcome for running checks.';
110
+ unfinished.forEach(value => check({ ...value, status: 'not-run', detail: reason }));
111
+ }
112
+ closed = true;
113
+ clearInterval(heartbeat);
114
+ clearTimeout(timer);
115
+ await flush();
116
+ if (error)
117
+ throw error;
118
+ const result = await request('PATCH', { sequence: sequence + 1, checks: [], finish: status, ...(reason ? { reason } : {}) });
119
+ finishedAtMs = result.finishedAtMs;
120
+ sequence++;
121
+ } };
122
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @module Media types by file extension
3
+ *
4
+ * A small fixed table from file extension to media type, used when a captured file arrives without a type. It is a
5
+ * fallback guess from the name only: the bytes are never sniffed, and a type the producer gave always wins.
6
+ */
7
+ /**
8
+ * The media type for a file name, from its last extension, case-insensitively. Anything not in the table, and a name
9
+ * with no extension, is `application/octet-stream`. Never reads the content.
10
+ */
11
+ export declare function contentTypeOf(file: string): string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @module Media types by file extension
3
+ *
4
+ * A small fixed table from file extension to media type, used when a captured file arrives without a type. It is a
5
+ * fallback guess from the name only: the bytes are never sniffed, and a type the producer gave always wins.
6
+ */
7
+ /** Media types by file extension (lowercase, no dot), for attachments whose producer did not say. */
8
+ const TYPES = {
9
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif', svg: 'image/svg+xml', ico: 'image/x-icon', bmp: 'image/bmp',
10
+ mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', mkv: 'video/x-matroska', ogv: 'video/ogg',
11
+ mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4', flac: 'audio/flac',
12
+ vtt: 'text/vtt', srt: 'application/x-subrip', txt: 'text/plain; charset=utf-8', log: 'text/plain; charset=utf-8', md: 'text/markdown; charset=utf-8', csv: 'text/csv', html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8', css: 'text/css',
13
+ json: 'application/json', ndjson: 'application/x-ndjson', har: 'application/json', xml: 'application/xml', yaml: 'application/yaml', yml: 'application/yaml', pdf: 'application/pdf',
14
+ zip: 'application/zip', gz: 'application/gzip', tgz: 'application/gzip', tar: 'application/x-tar', mmd: 'text/plain; charset=utf-8', diff: 'text/x-diff', patch: 'text/x-diff'
15
+ };
16
+ /**
17
+ * The media type for a file name, from its last extension, case-insensitively. Anything not in the table, and a name
18
+ * with no extension, is `application/octet-stream`. Never reads the content.
19
+ */
20
+ export function contentTypeOf(file) {
21
+ return TYPES[file.split('.').at(-1)?.toLowerCase() ?? ''] ?? 'application/octet-stream';
22
+ }
@@ -1,11 +1,20 @@
1
+ /**
2
+ * @module Object key guards
3
+ *
4
+ * Throws on object-store keys that could escape their prefix. The checks are lexical only (empty, leading `/`, `..`,
5
+ * missing namespace); they do not normalise a key or decide whether an object exists.
6
+ */
1
7
  /**
2
8
  * Validates a wire key before any remote object-store I/O. Every S3 command
3
9
  * for the shared bucket must pass this gate so projects cannot clobber
4
10
  * each other's prefixes.
11
+ *
12
+ * Throws `Error` when `key` is empty, starts with `/`, contains `..` anywhere, is exactly `namespace` (with or without
13
+ * a trailing `/`), or does not start with `namespace`. Returns nothing when the key is acceptable.
5
14
  */
6
15
  export declare function assertRemoteObjectKey(key: string, namespace: string): void;
7
16
  /**
8
17
  * Validates a logical key before {@link ObjectStoreWithPrefix} scopes it under
9
- * a namespace or feature prefix.
18
+ * a namespace or feature prefix. Throws `Error` when `key` is empty, starts with `/`, or contains `..` anywhere.
10
19
  */
11
20
  export declare function assertLogicalObjectKey(key: string): void;
@@ -1,7 +1,16 @@
1
+ /**
2
+ * @module Object key guards
3
+ *
4
+ * Throws on object-store keys that could escape their prefix. The checks are lexical only (empty, leading `/`, `..`,
5
+ * missing namespace); they do not normalise a key or decide whether an object exists.
6
+ */
1
7
  /**
2
8
  * Validates a wire key before any remote object-store I/O. Every S3 command
3
9
  * for the shared bucket must pass this gate so projects cannot clobber
4
10
  * each other's prefixes.
11
+ *
12
+ * Throws `Error` when `key` is empty, starts with `/`, contains `..` anywhere, is exactly `namespace` (with or without
13
+ * a trailing `/`), or does not start with `namespace`. Returns nothing when the key is acceptable.
5
14
  */
6
15
  export function assertRemoteObjectKey(key, namespace) {
7
16
  if (key.length === 0) {
@@ -25,7 +34,7 @@ export function assertRemoteObjectKey(key, namespace) {
25
34
  }
26
35
  /**
27
36
  * Validates a logical key before {@link ObjectStoreWithPrefix} scopes it under
28
- * a namespace or feature prefix.
37
+ * a namespace or feature prefix. Throws `Error` when `key` is empty, starts with `/`, or contains `..` anywhere.
29
38
  */
30
39
  export function assertLogicalObjectKey(key) {
31
40
  if (key.length === 0) {
@@ -1,8 +1,20 @@
1
+ /**
2
+ * @module Explorer navigation host
3
+ *
4
+ * The seam between a reports explorer and the page it is embedded in: where the address (hash), keyboard and title
5
+ * live. It is a type only; a host (usually the browser) supplies the implementation, which keeps the explorer free of
6
+ * `window` and testable.
7
+ */
1
8
  /** Host navigation and keyboard events, scoped to one reports explorer. */
2
9
  export type ReportNavigation = {
10
+ /** The current location hash, as the host holds it, with or without the leading `#` (the route parser accepts both). */
3
11
  readonly readHash: () => string;
12
+ /** Sets the hash (given with its leading `#`, like `#/test/<id>`); `replace` true replaces the current history entry instead of adding one. */
4
13
  readonly writeHash: (hash: string, replace: boolean) => void;
14
+ /** Calls `listener` whenever the hash changes; returns the function that stops listening. */
5
15
  readonly subscribeHash: (listener: () => void) => () => void;
16
+ /** Calls `listener` for keyboard events scoped to this explorer; returns the function that stops listening. */
6
17
  readonly subscribeKeys: (listener: (event: KeyboardEvent) => void) => () => void;
18
+ /** Sets the page title. */
7
19
  readonly setTitle: (title: string) => void;
8
20
  };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @module Explorer navigation host
3
+ *
4
+ * The seam between a reports explorer and the page it is embedded in: where the address (hash), keyboard and title
5
+ * live. It is a type only; a host (usually the browser) supplies the implementation, which keeps the explorer free of
6
+ * `window` and testable.
7
+ */
@@ -1,9 +1,16 @@
1
+ /**
2
+ * @module Reading and writing whole objects
3
+ *
4
+ * Small helpers over `ObjectStore` for the common case of an object read or written whole: draining a stream,
5
+ * decoding UTF-8, and encoding text for `put`. They hold the object in memory, so they are for manifests, catalog
6
+ * entries and bounded blobs, not for large files. Errors from the store are not caught.
7
+ */
1
8
  import type { ObjectStore } from './object-store.js';
2
- /** Drain a {@link ReadableStream} into a single byte array. */
9
+ /** Drains a stream into one byte array (it reads the stream to its end; it holds every chunk in memory). */
3
10
  export declare function readBytes(stream: ReadableStream<Uint8Array>): Promise<Uint8Array>;
4
- /** Read object bytes as UTF-8 text, or `null` when the key is missing. */
11
+ /** Reads an object as UTF-8 text, or `null` when the key is missing. Invalid UTF-8 is decoded leniently (replacement characters), not rejected. Transport errors throw. */
5
12
  export declare function readUtf8(store: ObjectStore, key: string): Promise<string | null>;
6
- /** Write UTF-8 text under `key` with the given content type (default text/plain). */
13
+ /** Writes UTF-8 text under `key` with the given content type (default `text/plain; charset=utf-8`). Overwrites; transport errors throw. */
7
14
  export declare function writeUtf8(store: ObjectStore, key: string, text: string, contentType?: string): Promise<void>;
8
- /** Read object bytes, or `null` when the key is missing. */
15
+ /** Reads an object's bytes whole, or `null` when the key is missing. Transport errors throw. */
9
16
  export declare function readObjectBytes(store: ObjectStore, key: string): Promise<Uint8Array | null>;
@@ -1,4 +1,4 @@
1
- /** Drain a {@link ReadableStream} into a single byte array. */
1
+ /** Drains a stream into one byte array (it reads the stream to its end; it holds every chunk in memory). */
2
2
  export async function readBytes(stream) {
3
3
  const reader = stream.getReader();
4
4
  const chunks = [];
@@ -20,7 +20,7 @@ export async function readBytes(stream) {
20
20
  }
21
21
  return out;
22
22
  }
23
- /** Read object bytes as UTF-8 text, or `null` when the key is missing. */
23
+ /** Reads an object as UTF-8 text, or `null` when the key is missing. Invalid UTF-8 is decoded leniently (replacement characters), not rejected. Transport errors throw. */
24
24
  export async function readUtf8(store, key) {
25
25
  const obj = await store.get(key);
26
26
  if (obj === null)
@@ -28,11 +28,11 @@ export async function readUtf8(store, key) {
28
28
  const bytes = await readBytes(obj.body);
29
29
  return new TextDecoder().decode(bytes);
30
30
  }
31
- /** Write UTF-8 text under `key` with the given content type (default text/plain). */
31
+ /** Writes UTF-8 text under `key` with the given content type (default `text/plain; charset=utf-8`). Overwrites; transport errors throw. */
32
32
  export async function writeUtf8(store, key, text, contentType = 'text/plain; charset=utf-8') {
33
33
  await store.put(key, new TextEncoder().encode(text), contentType);
34
34
  }
35
- /** Read object bytes, or `null` when the key is missing. */
35
+ /** Reads an object's bytes whole, or `null` when the key is missing. Transport errors throw. */
36
36
  export async function readObjectBytes(store, key) {
37
37
  const obj = await store.get(key);
38
38
  if (obj === null)
@@ -0,0 +1,27 @@
1
+ import type { ObjectStore } from './object-store.js';
2
+ /** The outcome of `verifyObjectStore`: `ok` is true exactly when `failures` is empty. */
3
+ export type StoreVerification = {
4
+ readonly ok: boolean;
5
+ readonly failures: readonly string[];
6
+ };
7
+ /**
8
+ * Checks a backend against the {@link ObjectStore} contract before trusting it with evidence. It
9
+ * writes a few objects under a scratch prefix and removes them. Use it in the test suite of an S3,
10
+ * GCS, R2 or HTTP adapter; an adapter that passes composes with every other backend.
11
+ *
12
+ * It is a plain async function, so it runs under any test runner and from a script. It checks: a missing key reads
13
+ * `null` (`get`, `getUri`) and `false` (`head`); `put` overwrites and round-trips bytes, size and content type;
14
+ * `listKeys` is in code-unit order, filters by any string prefix and is empty for an unused one; `withPrefix` scopes
15
+ * reads and writes; `delete` and `deleteMany` accept missing keys. A failed expectation is reported in `failures`;
16
+ * an exception from the store is reported as one `threw: ...` failure rather than thrown. It does not check
17
+ * `listPage`.
18
+ *
19
+ * @param store the backend to verify.
20
+ * @param scratch the key prefix to write under; defaults to a random `atlas-conformance-<id>/`. If you pass one,
21
+ * nothing else may live under it: the check asserts that `listKeys(scratch)` returns exactly the four objects it wrote.
22
+ *
23
+ * @example
24
+ * const { ok, failures } = await verifyObjectStore(myStore);
25
+ * if (!ok) throw new Error(failures.join('\n'));
26
+ */
27
+ export declare function verifyObjectStore(store: ObjectStore, scratch?: string): Promise<StoreVerification>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @module ObjectStore conformance check
3
+ *
4
+ * Runs the `ObjectStore` contract against any implementation, so a new backend (GCS, an HTTP service, a device lab) can
5
+ * prove it composes with the others before it holds evidence. It is a plain async function with no test-runner
6
+ * dependency. It writes only under a scratch prefix and removes what it wrote.
7
+ *
8
+ * @see docs/report-contract.md#backends
9
+ */
10
+ import { readBytes } from './object-io.js';
11
+ const text = (value) => new TextEncoder().encode(value);
12
+ /**
13
+ * Checks a backend against the {@link ObjectStore} contract before trusting it with evidence. It
14
+ * writes a few objects under a scratch prefix and removes them. Use it in the test suite of an S3,
15
+ * GCS, R2 or HTTP adapter; an adapter that passes composes with every other backend.
16
+ *
17
+ * It is a plain async function, so it runs under any test runner and from a script. It checks: a missing key reads
18
+ * `null` (`get`, `getUri`) and `false` (`head`); `put` overwrites and round-trips bytes, size and content type;
19
+ * `listKeys` is in code-unit order, filters by any string prefix and is empty for an unused one; `withPrefix` scopes
20
+ * reads and writes; `delete` and `deleteMany` accept missing keys. A failed expectation is reported in `failures`;
21
+ * an exception from the store is reported as one `threw: ...` failure rather than thrown. It does not check
22
+ * `listPage`.
23
+ *
24
+ * @param store the backend to verify.
25
+ * @param scratch the key prefix to write under; defaults to a random `atlas-conformance-<id>/`. If you pass one,
26
+ * nothing else may live under it: the check asserts that `listKeys(scratch)` returns exactly the four objects it wrote.
27
+ *
28
+ * @example
29
+ * const { ok, failures } = await verifyObjectStore(myStore);
30
+ * if (!ok) throw new Error(failures.join('\n'));
31
+ */
32
+ export async function verifyObjectStore(store, scratch = `atlas-conformance-${Math.random().toString(36).slice(2, 10)}/`) {
33
+ const failures = [];
34
+ const expect = (ok, message) => { if (!ok)
35
+ failures.push(message); };
36
+ const key = (name) => `${scratch}${name}`;
37
+ const created = [];
38
+ try {
39
+ expect((await store.get(key('missing'))) === null, 'get returns null for a missing key');
40
+ expect((await store.head(key('missing'))) === false, 'head returns false for a missing key');
41
+ expect((await store.getUri(key('missing'))) === null, 'getUri returns null for a missing key');
42
+ await store.delete(key('missing'));
43
+ const payload = new Uint8Array([0, 1, 2, 254, 255]);
44
+ created.push(key('a/b.json'));
45
+ await store.put(key('a/b.json'), payload, 'application/json');
46
+ await store.put(key('a/b.json'), payload, 'application/json');
47
+ const stored = await store.get(key('a/b.json'));
48
+ expect(stored !== null, 'a written key can be read');
49
+ if (stored) {
50
+ expect(stored.contentType.startsWith('application/json'), 'the content type round-trips');
51
+ const bytes = await readBytes(stored.body);
52
+ expect(bytes.length === payload.length && payload.every((byte, i) => bytes[i] === byte), 'bytes round-trip exactly');
53
+ expect(stored.size === payload.length, 'size is the byte length');
54
+ }
55
+ expect(await store.head(key('a/b.json')), 'head is true for a written key');
56
+ await store.put(key('a/b.json'), text('{}'), 'application/json');
57
+ const replaced = await store.get(key('a/b.json'));
58
+ expect(replaced !== null && (await readBytes(replaced.body)).length === 2, 'put overwrites the same key');
59
+ for (const name of ['a/c.json', 'a/d/e.json', 'z.json']) {
60
+ created.push(key(name));
61
+ await store.put(key(name), text(name), 'text/plain');
62
+ }
63
+ const all = await store.listKeys(scratch);
64
+ expect(JSON.stringify(all) === JSON.stringify([...all].sort()), 'listKeys is sorted by code unit');
65
+ expect(all.length === 4, 'listKeys lists every key under the prefix, nothing else');
66
+ const nested = await store.listKeys(key('a/'));
67
+ expect(nested.length === 3 && nested.every((name) => name.startsWith(key('a/'))), 'listKeys filters by prefix, including across directories');
68
+ expect((await store.listKeys(key('a/c'))).length === 1, 'a prefix need not end at a separator');
69
+ expect((await store.listKeys(key('nothing/'))).length === 0, 'listKeys is empty for an unused prefix');
70
+ const scoped = store.withPrefix(scratch);
71
+ expect((await scoped.listKeys()).length === 4 && (await scoped.listKeys())[0] === 'a/b.json', 'withPrefix scopes keys, which are relative to it');
72
+ await scoped.put('scoped.json', text('s'), 'text/plain');
73
+ created.push(key('scoped.json'));
74
+ expect(await store.head(key('scoped.json')), 'a write through withPrefix lands under the prefix');
75
+ await store.delete(key('z.json'));
76
+ expect(!(await store.head(key('z.json'))), 'delete removes a key');
77
+ await store.deleteMany([key('a/c.json'), key('a/d/e.json'), key('missing-too')]);
78
+ expect(!(await store.head(key('a/c.json'))) && !(await store.head(key('a/d/e.json'))), 'deleteMany removes keys, missing ones included');
79
+ }
80
+ catch (error) {
81
+ failures.push(`threw: ${error instanceof Error ? error.message : String(error)}`);
82
+ }
83
+ finally {
84
+ await store.deleteMany(created).catch(() => { });
85
+ }
86
+ return { ok: failures.length === 0, failures };
87
+ }