@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
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @module Store protocol routes
3
+ *
4
+ * The `ObjectStore` interface over HTTP at `/api/store`: list keys under `layers/`, read and probe an object, upload one
5
+ * (checked by `validateUpload`, immutable once stored) and remove a scratch object. This is what `httpStore` talks to. An
6
+ * upload is told to the report only after it is stored, and a layer's manifest, parsed while it was checked, is handed on so
7
+ * it is not read again. Everything read is served as untrusted bytes (never sniffed, never run).
8
+ *
9
+ * @see docs/server.md#the-store-protocol
10
+ */
11
+ import { Hono } from 'hono';
12
+ import { isPaged } from '../../reports/src/object-store.js';
13
+ import { readBytes } from '../../reports/src/object-io.js';
14
+ import { UNTRUSTED_HEADERS } from '../../reports/src/attachment-response.js';
15
+ import { classify, isCatalog, Refusal, sameBytes, validateUpload, validKey } from './validate.js';
16
+ /** Read a request body, refusing as soon as it is over the limit rather than after holding it all. */
17
+ export async function readLimited(request, limit) {
18
+ const declared = Number(request.headers.get('content-length') ?? NaN);
19
+ if (Number.isFinite(declared) && declared > limit)
20
+ throw new Refusal(413, `That is ${declared} bytes; the limit is ${limit}`);
21
+ if (!request.body)
22
+ return new Uint8Array();
23
+ const reader = request.body.getReader();
24
+ const chunks = [];
25
+ let size = 0;
26
+ try {
27
+ for (;;) {
28
+ const { done, value } = await reader.read();
29
+ if (done)
30
+ break;
31
+ size += value.byteLength;
32
+ if (size > limit) {
33
+ await reader.cancel();
34
+ throw new Refusal(413, `The body is over the limit of ${limit} bytes`);
35
+ }
36
+ chunks.push(value);
37
+ }
38
+ }
39
+ finally {
40
+ reader.releaseLock();
41
+ }
42
+ const out = new Uint8Array(size);
43
+ let at = 0;
44
+ for (const chunk of chunks) {
45
+ out.set(chunk, at);
46
+ at += chunk.byteLength;
47
+ }
48
+ return out;
49
+ }
50
+ const decode = (c, prefix) => {
51
+ try {
52
+ return decodeURIComponent(new URL(c.req.url).pathname.slice(prefix.length));
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ };
58
+ /**
59
+ * The store protocol: the seven operations of an object store over HTTP, for what the server keeps. A client
60
+ * that publishes to it uses the same code that publishes to a folder or a bucket. It differs from a bucket in
61
+ * that the server checks what it is given, keeps layers immutable, and tells every viewer when something lands.
62
+ *
63
+ * GET /api/store?prefix=&after=&limit= a page of keys, in order
64
+ * GET /api/store/<key> the object
65
+ * HEAD /api/store/<key> does it exist
66
+ * PUT /api/store/<key> upload it (publish token, Bearer)
67
+ * DELETE /api/store/<key> remove a scratch object (publish token, Bearer)
68
+ */
69
+ /** A plain relative path: no traversal, no backslash, no control characters. */
70
+ const safePath = (value) => value.length > 0 && value.length <= 1024 && !value.startsWith('/') && !value.includes('\\') && !/[\u0000-\u001f]/.test(value) && value.split('/').every(part => part !== '' && part !== '.' && part !== '..');
71
+ /** The Hono app of the store protocol, to be mounted at `/api/store`. Reads need role `read`; writes need `publish` through a Bearer token. */
72
+ export function storeApi(options) {
73
+ const app = new Hono();
74
+ const { store, auth } = options;
75
+ // A layer's manifest is validated when it arrives and its catalog entry follows: what was read the first time serves the second.
76
+ const recent = new Map();
77
+ // Serialize only competing writes to the same key. Other uploads retain their concurrency.
78
+ const writing = new Map();
79
+ async function exclusive(key, action) {
80
+ const previous = writing.get(key) ?? Promise.resolve();
81
+ const next = previous.catch(() => undefined).then(action);
82
+ writing.set(key, next);
83
+ try {
84
+ return await next;
85
+ }
86
+ finally {
87
+ if (writing.get(key) === next)
88
+ writing.delete(key);
89
+ }
90
+ }
91
+ const refuse = (c, status, message) => c.json({ error: message }, status);
92
+ app.get('/', async (c) => {
93
+ const verdict = auth.check(c.req.raw, 'read');
94
+ if (!verdict.ok)
95
+ return refuse(c, verdict.status, verdict.message);
96
+ // Only layers/ is listed: the whole store may hold other things.
97
+ const prefix = c.req.query('prefix') || 'layers/';
98
+ const after = c.req.query('after');
99
+ const limit = Math.max(1, Math.min(Number(c.req.query('limit') ?? 1000) || 1000, 1000));
100
+ const named = prefix.replace(/\/$/, '');
101
+ if (prefix !== 'layers/' && !validKey(named)) {
102
+ // Older layouts keep things outside layers/. A server holds none of them: nothing there, not an error.
103
+ if (safePath(named))
104
+ return c.json({ keys: [], next: null });
105
+ return refuse(c, 422, 'Not a prefix Atlas holds');
106
+ }
107
+ if (isPaged(store))
108
+ return c.json(await store.listPage(prefix, { ...(after === undefined ? {} : { after }), limit }));
109
+ const all = (await store.listKeys(prefix)).filter(key => after === undefined || key > after);
110
+ return c.json({ keys: all.slice(0, limit), next: all.length > limit ? all[limit - 1] : null });
111
+ });
112
+ const key = (c) => decode(c, '/api/store/');
113
+ app.on(['GET', 'HEAD'], '/*', async (c) => {
114
+ const verdict = auth.check(c.req.raw, 'read');
115
+ if (!verdict.ok)
116
+ return refuse(c, verdict.status, verdict.message);
117
+ const name = key(c);
118
+ if (name === null || !validKey(name)) {
119
+ if (name !== null && safePath(name))
120
+ return new Response(c.req.method === 'HEAD' ? null : JSON.stringify({ error: 'Not found' }), { status: 404, headers: { 'content-type': 'application/json' } });
121
+ return refuse(c, 422, 'Not a key Atlas holds');
122
+ }
123
+ const stored = await store.get(name);
124
+ if (!stored)
125
+ return new Response(c.req.method === 'HEAD' ? null : JSON.stringify({ error: 'Not found' }), { status: 404, headers: { 'content-type': 'application/json' } });
126
+ // Everything under layers/ is immutable, so it may be kept for good.
127
+ // What is stored came from a test run: it is never run, sniffed or embedded, whatever type its uploader gave it.
128
+ const type = /^[\w.+-]+\/[\w.+-]+(?:\s*;\s*[\w.-]+=[\w.\-"]+)*$/.test(stored.contentType) ? stored.contentType : 'application/octet-stream';
129
+ const headers = { 'content-type': type, ...UNTRUSTED_HEADERS, 'cache-control': 'public, max-age=31536000, immutable', ...(stored.size ? { 'content-length': String(stored.size) } : {}) };
130
+ if (c.req.method === 'HEAD') {
131
+ await stored.body.cancel();
132
+ return new Response(null, { headers });
133
+ }
134
+ return new Response(stored.body, { headers });
135
+ });
136
+ app.put('/*', async (c) => {
137
+ const verdict = auth.check(c.req.raw, 'publish');
138
+ if (!verdict.ok)
139
+ return refuse(c, verdict.status, verdict.message);
140
+ const name = key(c);
141
+ if (name === null || !validKey(name))
142
+ return refuse(c, 422, 'Not a key Atlas holds');
143
+ try {
144
+ const bytes = await readLimited(c.req.raw, options.maxObjectBytes);
145
+ return await exclusive(name, async () => {
146
+ const kind = await validateUpload(name, bytes, store, layer => { recent.set(layer.id, layer); if (recent.size > 8)
147
+ recent.delete(recent.keys().next().value); });
148
+ // A blob's name is the digest of what is in it, which was just checked: one that is there is this one, however it was compressed.
149
+ const present = kind === 'blob' && (await store.head(name)) ? 'same' : await sameBytes(store, name, bytes);
150
+ // Scratch objects may be overwritten (an object store's contract says writing a key twice is fine); layers may not.
151
+ if (present === 'different' && kind !== 'scratch')
152
+ throw new Refusal(409, `${name} already exists with other content. Layers are immutable: publish a new one.`);
153
+ if (present !== 'same')
154
+ await store.put(name, bytes, c.req.header('content-type') ?? 'application/octet-stream');
155
+ // Catalog entries make a layer, spec set or run visible, so everyone is told only once they are in.
156
+ if (present === 'absent' || isCatalog(kind))
157
+ await options.onStored(name, kind, kind === 'layer-catalog' ? recent.get(classify(name)?.id ?? '') : undefined);
158
+ return c.json({ ok: true, key: name, kind, created: present === 'absent' }, present === 'absent' ? 201 : 200);
159
+ });
160
+ }
161
+ catch (error) {
162
+ if (error instanceof Refusal)
163
+ return refuse(c, error.status, error.message);
164
+ throw error;
165
+ }
166
+ });
167
+ app.delete('/*', async (c) => {
168
+ const verdict = auth.check(c.req.raw, 'publish');
169
+ if (!verdict.ok)
170
+ return refuse(c, verdict.status, verdict.message);
171
+ const name = key(c);
172
+ // Nothing a report is made of can be taken away. Only what `atlas report doctor` writes to try the store.
173
+ if (name === null || classify(name)?.kind !== 'scratch')
174
+ return refuse(c, 403, 'Layers, specs and runs are never removed through the API');
175
+ await store.delete(name);
176
+ return c.body(null, 204);
177
+ });
178
+ return app;
179
+ }
180
+ export { readBytes };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @module Upload validation
3
+ *
4
+ * What the server checks before it stores an object, so one bad upload cannot corrupt the report for everyone: the key is
5
+ * one Atlas writes (`classify`), a layer's content hashes to its id, blobs and spec files are what their names say, a
6
+ * document's files and a catalog entry's document are already there (so nothing half-uploaded is visible), and a run
7
+ * passed its privacy scan. Refusals are `Refusal`s carrying the HTTP status to answer. Layers are immutable: the same
8
+ * bytes again are fine, other bytes under the same key are not.
9
+ *
10
+ * @see docs/server.md#what-the-server-refuses
11
+ */
12
+ import { readUtf8 } from '../../reports/src/object-io.js';
13
+ import type { ObjectStore } from '../../reports/src/object-store.js';
14
+ import type { ReportLayer } from '../../reports/src/layer-types.js';
15
+ /** Why an upload was not accepted. The status is what to answer. */
16
+ export declare class Refusal extends Error {
17
+ readonly status: 403 | 409 | 413 | 422;
18
+ constructor(status: 403 | 409 | 413 | 422, message: string);
19
+ }
20
+ /** The kinds of object Atlas writes, by their keys. Anything else is not for a server to hold. */
21
+ declare const KINDS: readonly [{
22
+ readonly kind: "layer";
23
+ readonly pattern: RegExp;
24
+ }, {
25
+ readonly kind: "layer-catalog";
26
+ readonly pattern: RegExp;
27
+ }, {
28
+ readonly kind: "blob";
29
+ readonly pattern: RegExp;
30
+ }, {
31
+ readonly kind: "spec-body";
32
+ readonly pattern: RegExp;
33
+ }, {
34
+ readonly kind: "spec-set";
35
+ readonly pattern: RegExp;
36
+ }, {
37
+ readonly kind: "spec-catalog";
38
+ readonly pattern: RegExp;
39
+ }, {
40
+ readonly kind: "run-manifest";
41
+ readonly pattern: RegExp;
42
+ }, {
43
+ readonly kind: "run-catalog";
44
+ readonly pattern: RegExp;
45
+ }, {
46
+ readonly kind: "run-file";
47
+ readonly pattern: RegExp;
48
+ }, {
49
+ readonly kind: "scratch";
50
+ readonly pattern: RegExp;
51
+ }];
52
+ /** What a key names: `layer`, `layer-catalog`, `blob`, `spec-body`, `spec-set`, `spec-catalog`, `run-manifest`, `run-catalog`, `run-file` or `scratch` (what `doctor` writes to try the store). */
53
+ export type UploadKind = (typeof KINDS)[number]['kind'];
54
+ /** Is this a key a client may name? No traversal, no odd characters, and under `layers/`. */
55
+ export declare function validKey(key: string): boolean;
56
+ /** What kind of object a key names and the id inside it (the layer id, the digest…), or null when it is not a key Atlas writes. */
57
+ export declare function classify(key: string): {
58
+ kind: UploadKind;
59
+ id: string | null;
60
+ } | null;
61
+ /** Catalog entries are what makes something visible, so they are the last thing a client writes. */
62
+ export declare const isCatalog: (kind: UploadKind) => kind is "layer-catalog" | "spec-catalog" | "run-catalog";
63
+ /**
64
+ * Check an object before it is stored. What is checked is what would otherwise corrupt the report for everyone:
65
+ * a layer whose content is not its name, a catalog entry for something not there yet, a run that failed its
66
+ * privacy scan. Everything that is checked is checked cheaply, from the bytes in hand.
67
+ */
68
+ export declare function validateUpload(key: string, bytes: Uint8Array, store: ObjectStore, parsedLayer?: (layer: ReportLayer) => void): Promise<UploadKind>;
69
+ /** Is this the same object already? Layers are immutable: the same bytes again is fine, other bytes under the same key is not. */
70
+ export declare function sameBytes(store: ObjectStore, key: string, bytes: Uint8Array): Promise<'absent' | 'same' | 'different'>;
71
+ export { readUtf8 };
@@ -0,0 +1,197 @@
1
+ /**
2
+ * @module Upload validation
3
+ *
4
+ * What the server checks before it stores an object, so one bad upload cannot corrupt the report for everyone: the key is
5
+ * one Atlas writes (`classify`), a layer's content hashes to its id, blobs and spec files are what their names say, a
6
+ * document's files and a catalog entry's document are already there (so nothing half-uploaded is visible), and a run
7
+ * passed its privacy scan. Refusals are `Refusal`s carrying the HTTP status to answer. Layers are immutable: the same
8
+ * bytes again are fine, other bytes under the same key are not.
9
+ *
10
+ * @see docs/server.md#what-the-server-refuses
11
+ */
12
+ import { promisify } from 'node:util';
13
+ import { gunzip } from 'node:zlib';
14
+ import { parseLayerDocument, SpecSetSchema } from '../../schema/src/layer.js';
15
+ import { readUtf8 } from '../../reports/src/object-io.js';
16
+ import { eachLimited } from '../../reports/src/completion-order.js';
17
+ import { sha256Hex } from '../../reports/src/sigv4.js';
18
+ /** Why an upload was not accepted. The status is what to answer. */
19
+ export class Refusal extends Error {
20
+ status;
21
+ constructor(status, message) {
22
+ super(message);
23
+ this.status = status;
24
+ }
25
+ }
26
+ const HEX = '[0-9a-f]{64}';
27
+ const KEY_LENGTH = 1024;
28
+ /** The kinds of object Atlas writes, by their keys. Anything else is not for a server to hold. */
29
+ const KINDS = [
30
+ { kind: 'layer', pattern: new RegExp(`^layers/manifests/(${HEX})\\.json$`) },
31
+ { kind: 'layer-catalog', pattern: new RegExp(`^layers/index/\\d{16}-(${HEX})\\.json$`) },
32
+ { kind: 'blob', pattern: new RegExp(`^layers/blobs/(${HEX})\\.gz$`) },
33
+ { kind: 'spec-body', pattern: new RegExp(`^layers/specs/blobs/(${HEX})\\.txt$`) },
34
+ { kind: 'spec-set', pattern: new RegExp(`^layers/specs/sets/(${HEX})\\.json$`) },
35
+ { kind: 'spec-catalog', pattern: new RegExp(`^layers/specs/index/\\d{16}-.+-[cmu]-(${HEX})\\.json$`) },
36
+ { kind: 'run-manifest', pattern: /^layers\/runs\/([\w.-]+)\/manifest\.json$/ },
37
+ { kind: 'run-catalog', pattern: /^layers\/runs\/index\/\d{16}-([\w.-]+)\.json$/ },
38
+ { kind: 'run-file', pattern: /^layers\/runs\/([\w.-]+)\/.+$/ },
39
+ { kind: 'scratch', pattern: /^layers\/doctor-[\w-]+\/.+$/ }
40
+ ];
41
+ /** Is this a key a client may name? No traversal, no odd characters, and under `layers/`. */
42
+ export function validKey(key) {
43
+ if (!key || key.length > KEY_LENGTH || !key.startsWith('layers/') || key.includes('\\') || /[\u0000-\u001f]/.test(key))
44
+ return false;
45
+ return key.split('/').every(part => part !== '' && part !== '.' && part !== '..');
46
+ }
47
+ /** What kind of object a key names and the id inside it (the layer id, the digest…), or null when it is not a key Atlas writes. */
48
+ export function classify(key) {
49
+ for (const { kind, pattern } of KINDS) {
50
+ const match = pattern.exec(key);
51
+ if (match)
52
+ return { kind, id: match[1] ?? null };
53
+ }
54
+ return null;
55
+ }
56
+ /** Catalog entries are what makes something visible, so they are the last thing a client writes. */
57
+ export const isCatalog = (kind) => kind === 'layer-catalog' || kind === 'spec-catalog' || kind === 'run-catalog';
58
+ const gunzipAsync = promisify(gunzip);
59
+ /** The most a blob may expand to: what a layer may carry in total. A small file that expands to more is refused, not inflated. */
60
+ const MAX_BLOB_BYTES = 2 * 1024 * 1024 * 1024;
61
+ /** Parsed JSON that is not an object (a `null`, a number) names nothing: a refusal, not a crash. */
62
+ function objectOf(value) {
63
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
64
+ throw new Refusal(422, 'A catalog entry is a JSON object with the id of what it makes visible');
65
+ return value;
66
+ }
67
+ async function exists(store, key) {
68
+ return store.head(key);
69
+ }
70
+ /** Which of `keys` the store does not have, asked 16 at a time (a store far away answers each in its own time). */
71
+ async function missingOf(store, keys) {
72
+ const absent = new Set();
73
+ await eachLimited(keys, 16, async (key) => { if (!(await exists(store, key)))
74
+ absent.add(key); });
75
+ return keys.filter(key => absent.has(key));
76
+ }
77
+ /**
78
+ * Check an object before it is stored. What is checked is what would otherwise corrupt the report for everyone:
79
+ * a layer whose content is not its name, a catalog entry for something not there yet, a run that failed its
80
+ * privacy scan. Everything that is checked is checked cheaply, from the bytes in hand.
81
+ */
82
+ export async function validateUpload(key, bytes, store, parsedLayer) {
83
+ const known = classify(key);
84
+ if (!known)
85
+ throw new Refusal(422, `"${key}" is not a key Atlas writes`);
86
+ // Parsed once, however many of the checks below look at it.
87
+ let parsed;
88
+ const json = () => {
89
+ if (!parsed) {
90
+ try {
91
+ parsed = { value: JSON.parse(new TextDecoder().decode(bytes)) };
92
+ }
93
+ catch {
94
+ throw new Refusal(422, `${key} is not JSON`);
95
+ }
96
+ }
97
+ return parsed.value;
98
+ };
99
+ switch (known.kind) {
100
+ case 'layer': {
101
+ const document = parseLayerDocument(json());
102
+ if (!document.ok)
103
+ throw new Refusal(422, document.reason);
104
+ const raw = json();
105
+ if (raw.id !== known.id)
106
+ throw new Refusal(422, 'The layer\'s id is not the one in its key');
107
+ // The id is the digest of the document without it: a layer cannot be named for something it is not.
108
+ const { id: _id, ...rest } = raw;
109
+ if ((await sha256Hex(JSON.stringify(rest))) !== known.id)
110
+ throw new Refusal(422, 'The layer\'s content does not match its id (it was changed, or written by something that does not follow the format)');
111
+ const blobs = [...new Set(Object.values(document.layer.bundle).flatMap(entry => entry.parts.map(part => part.blob)))];
112
+ const missing = await missingOf(store, blobs);
113
+ if (missing.length)
114
+ throw new Refusal(409, `The layer needs files that have not been uploaded yet: ${missing.slice(0, 3).join(', ')}. Upload blobs before the manifest.`);
115
+ // What was just read is what the report folds in: handed on so it is not read from the store again.
116
+ const { schemaVersion: _version, ...layer } = document.layer;
117
+ parsedLayer?.(layer);
118
+ break;
119
+ }
120
+ case 'layer-catalog': {
121
+ const entry = objectOf(json());
122
+ if (entry.id !== known.id)
123
+ throw new Refusal(422, 'The catalog entry names another layer than its key does');
124
+ if (!(await exists(store, `layers/manifests/${known.id}.json`)))
125
+ throw new Refusal(409, 'That layer has not been uploaded yet. Upload its manifest before its catalog entry.');
126
+ break;
127
+ }
128
+ case 'spec-body':
129
+ if ((await sha256Hex(bytes)) !== known.id)
130
+ throw new Refusal(422, 'The spec\'s text does not match the digest in its key');
131
+ break;
132
+ case 'spec-set': {
133
+ const set = SpecSetSchema.safeParse(json());
134
+ if (!set.success)
135
+ throw new Refusal(422, `Not a valid spec set: ${set.error.issues[0]?.message ?? 'unknown'}`);
136
+ if (set.data.id !== known.id)
137
+ throw new Refusal(422, 'The spec set\'s id is not the one in its key');
138
+ const lacking = new Set(await missingOf(store, [...new Set(set.data.files.map(file => `layers/specs/blobs/${file.digest}.txt`))]));
139
+ const first = set.data.files.find(file => lacking.has(`layers/specs/blobs/${file.digest}.txt`));
140
+ if (first)
141
+ throw new Refusal(409, `The spec set needs ${first.path}, which has not been uploaded yet`);
142
+ break;
143
+ }
144
+ case 'spec-catalog': {
145
+ const entry = objectOf(json());
146
+ if (entry.id !== known.id)
147
+ throw new Refusal(422, 'The catalog entry names another spec set than its key does');
148
+ if (!(await exists(store, `layers/specs/sets/${known.id}.json`)))
149
+ throw new Refusal(409, 'That spec set has not been uploaded yet');
150
+ break;
151
+ }
152
+ case 'run-manifest': {
153
+ const manifest = json();
154
+ // The rule the static viewer always had: a run that did not pass its privacy scan is never shared.
155
+ if (manifest?.privacy?.passed !== true)
156
+ throw new Refusal(422, `Run ${known.id} is not shared: its privacy scan did not pass (or never ran)`);
157
+ break;
158
+ }
159
+ case 'run-catalog': {
160
+ if (!(await exists(store, `layers/runs/${known.id}/manifest.json`)))
161
+ throw new Refusal(409, 'That run has not been uploaded yet. Upload its manifest before its catalog entry.');
162
+ json();
163
+ break;
164
+ }
165
+ case 'blob': {
166
+ // The key is the digest of the uncompressed bytes. Checked the way a layer is, so a blob cannot be named for something it is not.
167
+ let raw;
168
+ try {
169
+ raw = new Uint8Array(await gunzipAsync(bytes, { maxOutputLength: MAX_BLOB_BYTES }));
170
+ }
171
+ catch {
172
+ throw new Refusal(422, 'A blob is gzip; this one could not be read as such');
173
+ }
174
+ if ((await sha256Hex(raw)) !== known.id)
175
+ throw new Refusal(422, 'The blob\'s content does not match the digest in its key');
176
+ break;
177
+ }
178
+ case 'run-file':
179
+ case 'scratch':
180
+ break;
181
+ }
182
+ return known.kind;
183
+ }
184
+ /** Is this the same object already? Layers are immutable: the same bytes again is fine, other bytes under the same key is not. */
185
+ export async function sameBytes(store, key, bytes) {
186
+ const existing = await store.get(key);
187
+ if (!existing)
188
+ return 'absent';
189
+ const stored = new Uint8Array(await new Response(existing.body).arrayBuffer());
190
+ if (stored.length !== bytes.length)
191
+ return 'different';
192
+ for (let i = 0; i < stored.length; i++)
193
+ if (stored[i] !== bytes[i])
194
+ return 'different';
195
+ return 'same';
196
+ }
197
+ export { readUtf8 };
@@ -1,4 +1,13 @@
1
+ /**
2
+ * @module Test-runner adapter
3
+ *
4
+ * Runs an Atlas config as a test suite in Vitest, `bun test` or any Jest-style runner. The runner's own
5
+ * `describe`, `it`, `beforeAll` and `expect` are passed in, so this module imports no test framework
6
+ * and never touches global test state. It owns the mapping from planned paths to tests; the run itself
7
+ * is `runConfig` from the core.
8
+ */
1
9
  import type { AtlasConfig, RunCommandOptions } from '../../core/src/index.js';
10
+ /** The subset of a Vitest/Jest-style runner's API that `describeAtlas` calls. */
2
11
  type Suite = {
3
12
  describe: (name: string, fn: () => void) => void;
4
13
  it: (name: string, fn: () => void | Promise<void>, timeout?: number) => void;
@@ -8,12 +17,25 @@ type Suite = {
8
17
  };
9
18
  };
10
19
  /**
11
- * Turn an Atlas config into a test suite: one test per planned path, so any Vitest/Jest-style
12
- * runner reports each journey and generated path on its own. The whole plan runs once in
13
- * `beforeAll` (paths share one manifest); each test then reads its path's outcome.
20
+ * Registers a test suite for an Atlas config: one test that the charts are executable (no structural
21
+ * lint finding), then one test per planned path, so any Vitest/Jest-style runner reports each journey
22
+ * and generated path on its own. The whole plan runs once in `beforeAll` (paths share one manifest);
23
+ * each test then reads its path's outcome. A failed path fails its test with the last attempt's error;
24
+ * `passed` and `flaky` paths pass; a path that was `not-reached` (blocked by an event with no
25
+ * implementation) or was not part of the run passes quietly.
26
+ *
27
+ * `prepare` runs synchronously when `describeAtlas` is called, so specs that cannot be loaded throw at
28
+ * collection time. The run writes its output like `atlas run` does.
29
+ *
30
+ * @param config the Atlas config to run.
31
+ * @param suite the runner's `describe`, `it`, `beforeAll` and `expect`.
32
+ * @param options the `atlas run` options (`mode`, `paths`, `retry`, `output`, `log`, ...);
33
+ * `baseDirectory` resolves relative config paths (the current directory when omitted) and `timeoutMs`
34
+ * bounds the `beforeAll` (30 minutes by default). Progress lines are discarded unless `log` is given.
14
35
  *
15
- * import { describe, it, beforeAll, expect } from 'bun:test';
16
- * describeAtlas(config, { describe, it, beforeAll, expect });
36
+ * @example
37
+ * import { beforeAll, describe, expect, it } from 'bun:test';
38
+ * describeAtlas(config, { describe, it, beforeAll, expect });
17
39
  */
18
40
  export declare function describeAtlas<C>(config: AtlasConfig<C>, suite: Suite, options?: RunCommandOptions & {
19
41
  baseDirectory?: string;
@@ -1,11 +1,24 @@
1
1
  import { prepare, runConfig } from '../../core/src/index.js';
2
2
  /**
3
- * Turn an Atlas config into a test suite: one test per planned path, so any Vitest/Jest-style
4
- * runner reports each journey and generated path on its own. The whole plan runs once in
5
- * `beforeAll` (paths share one manifest); each test then reads its path's outcome.
3
+ * Registers a test suite for an Atlas config: one test that the charts are executable (no structural
4
+ * lint finding), then one test per planned path, so any Vitest/Jest-style runner reports each journey
5
+ * and generated path on its own. The whole plan runs once in `beforeAll` (paths share one manifest);
6
+ * each test then reads its path's outcome. A failed path fails its test with the last attempt's error;
7
+ * `passed` and `flaky` paths pass; a path that was `not-reached` (blocked by an event with no
8
+ * implementation) or was not part of the run passes quietly.
6
9
  *
7
- * import { describe, it, beforeAll, expect } from 'bun:test';
8
- * describeAtlas(config, { describe, it, beforeAll, expect });
10
+ * `prepare` runs synchronously when `describeAtlas` is called, so specs that cannot be loaded throw at
11
+ * collection time. The run writes its output like `atlas run` does.
12
+ *
13
+ * @param config the Atlas config to run.
14
+ * @param suite the runner's `describe`, `it`, `beforeAll` and `expect`.
15
+ * @param options the `atlas run` options (`mode`, `paths`, `retry`, `output`, `log`, ...);
16
+ * `baseDirectory` resolves relative config paths (the current directory when omitted) and `timeoutMs`
17
+ * bounds the `beforeAll` (30 minutes by default). Progress lines are discarded unless `log` is given.
18
+ *
19
+ * @example
20
+ * import { beforeAll, describe, expect, it } from 'bun:test';
21
+ * describeAtlas(config, { describe, it, beforeAll, expect });
9
22
  */
10
23
  export function describeAtlas(config, suite, options = {}) {
11
24
  const prepared = prepare(config, options.baseDirectory);
@@ -4,33 +4,50 @@ import type { Locator } from './protocol.js';
4
4
  import { WebDriverSession } from './protocol.js';
5
5
  /** What a step points at: a signal (resolved per platform) or a raw WebDriver locator. */
6
6
  export type Target = Signal | Locator;
7
+ /** How signals are resolved: `web` evaluates a DOM probe in the page, `native` queries the accessibility tree (an Appium session). */
7
8
  export type Mode = 'web' | 'native';
8
- /** What every event and recogniser receives: the session and user actions logged to the timeline. */
9
+ /**
10
+ * What every event and recogniser receives from `webdriverDriver`: the open session plus a
11
+ * `SignalContext` whose targets may also be raw locators. It is created per attempt (or once per run
12
+ * with `session: 'per-run'`) and carries the actions that are logged to the step timeline.
13
+ */
9
14
  export type WebDriverContext = SignalContext & {
15
+ /** The open WebDriver session, for protocol calls the context does not wrap (`session.execute`, `session.command`). */
10
16
  session: WebDriverSession;
17
+ /** Whether signals resolve in the DOM (`web`) or the accessibility tree (`native`). */
11
18
  mode: Mode;
19
+ /** What `goto` resolves paths against; undefined when the options set no `baseUrl`. */
12
20
  baseUrl: string | undefined;
13
- /** Navigate, relative to `baseUrl` (a deep link in a native session). */
21
+ /** Navigate, relative to `baseUrl` (a deep link in a native session). Without a `baseUrl` the URL is sent as given. */
14
22
  goto(url: string): Promise<void>;
15
- /** The first visible element a target resolves to, or null. */
23
+ /** The id of the first visible element a target resolves to, or null. Does not wait. */
16
24
  find(target: Target): Promise<string | null>;
25
+ /** Whether the target resolves to a visible element right now. Does not wait; resolution errors count as not visible. */
17
26
  isVisible(target: Target): Promise<boolean>;
27
+ /** Actions a person does; each is added to the step timeline (with the element's centre as the tap position) and then performed. */
18
28
  user: {
29
+ /** Clicks the target's element, polling up to `actionTimeoutMs` for it to appear; throws `<target> is not on screen` otherwise. `label` is the caption. */
19
30
  tap(target: Target, label: string): Promise<void>;
31
+ /** Clicks the target's element to focus it, then sends `text` as keys (it does not clear existing text); waits and throws like `tap`. */
20
32
  type(target: Target, text: string, label: string): Promise<void>;
21
33
  };
22
34
  };
35
+ /** A WebDriver server binary the driver starts for the run and kills when the driver is disposed. */
23
36
  export type WebDriverServer = {
24
37
  /** The server binary: `safaridriver`, `chromedriver`, `geckodriver`, `appium`. */
25
38
  command: string;
26
- /** `{port}` is replaced with the port. Defaults: `--port {port}` (`--port={port}` for chromedriver). */
39
+ /** `{port}` is replaced with the port. Defaults: `--port {port}` (`--port={port}` for chromedriver and msedgedriver). */
27
40
  args?: string[];
41
+ /** The port to listen on; a free port on 127.0.0.1 is picked when omitted. */
28
42
  port?: number;
43
+ /** How long to wait for the server to answer `GET /status` as ready, in milliseconds (default 30 000). */
29
44
  readyTimeoutMs?: number;
30
45
  };
46
+ /** Options of `webdriverDriver`. `capabilities` and one of `url` or `server` are required. */
31
47
  export type WebDriverDriverOptions = {
32
- /** A running WebDriver server; or set `server` to start one for the run. */
48
+ /** A running WebDriver server (for example `http://127.0.0.1:4723` for Appium); or set `server` to start one for the run. */
33
49
  url?: string;
50
+ /** A server to start for the run; used when `url` is not set. The driver throws on first use if neither is set. */
34
51
  server?: WebDriverServer;
35
52
  /** W3C capabilities (`alwaysMatch`), e.g. `{ browserName: 'safari' }` or Appium's `platformName`, `appium:app`. */
36
53
  capabilities: Record<string, unknown>;
@@ -44,7 +61,9 @@ export type WebDriverDriverOptions = {
44
61
  recording?: 'frames' | 'appium' | 'none';
45
62
  /** How long a tap waits for its target, in milliseconds (default 15 000). */
46
63
  actionTimeoutMs?: number;
64
+ /** The driver's name in reports; `webdriver:<platformName>` for native sessions, else `webdriver:<browserName>`, when omitted. */
47
65
  name?: string;
66
+ /** Pauses around actions in showcase mode, in milliseconds; 400 before and 900 after when omitted. */
48
67
  pacing?: {
49
68
  before: number;
50
69
  after: number;
@@ -55,15 +74,48 @@ export type WebDriverDriverOptions = {
55
74
  */
56
75
  concurrency?: number;
57
76
  };
58
- /** A signal as a native locator: test IDs are accessibility identifiers, text is any label, value or text. */
77
+ /**
78
+ * Translates a signal into an Appium locator: a test ID becomes an `accessibility id`; text, labels
79
+ * and roles become an XPath over the label, name, value, text and content-desc attributes (a
80
+ * substring match unless `exact`), with a role narrowed to the matching XCUITest and UiAutomator2
81
+ * element types.
82
+ *
83
+ * @returns the locator, or null when the signal matches by regular expression, which XPath 1.0
84
+ * cannot express (the driver then matches it against the page source instead).
85
+ * @throws Error for a `css` signal, which only works in web sessions.
86
+ */
59
87
  export declare function nativeLocator(signal: Signal): Locator | null;
60
88
  /**
61
- * Drive anything that speaks W3C WebDriver: Safari (safaridriver ships with macOS), Chrome,
62
- * Firefox and Edge, or a native iOS or Android app through Appium. Signals resolve in the DOM
63
- * for web sessions and in the accessibility tree for native ones (test IDs are accessibility
89
+ * Creates a driver for anything that speaks W3C WebDriver: Safari (safaridriver ships with macOS),
90
+ * Chrome, Firefox and Edge, or a native iOS or Android app through Appium. Signals resolve in the
91
+ * DOM for web sessions and in the accessibility tree for native ones (test IDs are accessibility
64
92
  * identifiers), so one implementation style covers both. Screenshots and the page source are
65
- * standard; clips are encoded from screenshots, or come from Appium's screen recorder.
93
+ * standard; clips are encoded from screenshots (FFmpeg on the `PATH`), or come from Appium's screen
94
+ * recorder.
95
+ *
96
+ * The WebDriver server is started lazily on the first attempt (`options.server`) or used as is
97
+ * (`options.url`); `dispose()` deletes the shared session and kills a server this driver started.
98
+ * With `session: 'per-attempt'` each attempt opens and deletes its own session. A server that exits
99
+ * or is not ready in time, and a session the server refuses, reject the attempt that needed them.
100
+ *
101
+ * @param options the server, capabilities and behaviour; see `WebDriverDriverOptions`.
102
+ * @returns a driver whose context is a `WebDriverContext`.
103
+ * @throws Error on the first attempt when neither `options.url` nor `options.server` is set.
104
+ *
105
+ * @example
106
+ * const driver = webdriverDriver({
107
+ * server: { command: 'chromedriver' },
108
+ * capabilities: { browserName: 'chrome', 'goog:chromeOptions': { args: ['--headless=new'] } },
109
+ * baseUrl: 'http://localhost:3000'
110
+ * });
66
111
  */
67
112
  export declare function webdriverDriver(options: WebDriverDriverOptions): Driver<WebDriverContext>;
68
- /** A state recognised from signals or locators: every `all` visible and no `none` visible. */
113
+ /**
114
+ * Builds a state implementation recognised from signals or raw locators: every `all` target visible,
115
+ * one of `any` (when given) and none of `none`. `checks` pair a statement from the state's
116
+ * `meta.checks` with the target that proves it. A target whose lookup throws counts as not visible.
117
+ *
118
+ * @example
119
+ * states: { 'Visit booked': screen({ all: [text('Booked for')], none: [accessibilityId('book-button')] }) }
120
+ */
69
121
  export declare const screen: (spec: import('../../core/src/signals.js').ScreenSpec<WebDriverContext, Target>) => import('../../core/src/index.js').StateImplementation<WebDriverContext>;