@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,63 @@
1
+ /**
2
+ * @module Spec sets
3
+ *
4
+ * Publishes and finds spec sets: the specs of one commit, stored in the store itself so a viewer needs no checkout. Bodies
5
+ * are content-addressed in `layers/specs/blobs/<sha256>.txt`, the set is `layers/specs/sets/<id>.json`, and the catalog
6
+ * entry `layers/specs/index/<inverted-time>-<branch>-<flag>-<id>.json` is written last. `latestSpecSet` picks the outline
7
+ * a report is laid over: the newest set of `main` (then `master`, then any branch), preferring a set from a clean
8
+ * commit over one from a modified checkout.
9
+ *
10
+ * @see docs/report-contract.md#where-bytes-go
11
+ */
12
+ import { type SpecSet } from '../../schema/src/layer.js';
13
+ import { collectSpecFiles, isSpecFile, type SpecFileText } from './directory-outline.js';
14
+ import { type ObjectStore } from './object-store.js';
15
+ export { collectSpecFiles, isSpecFile, type SpecFileText };
16
+ /** What to publish as one spec set. */
17
+ export type PublishSpecSetInput = {
18
+ /** The spec files and their text; see `collectSpecFiles`. */
19
+ readonly files: readonly SpecFileText[];
20
+ /** The branch the specs are from, or `null`. Part of the set's identity and of its catalog key. */
21
+ readonly branch: string | null;
22
+ /** The commit the specs are from, or `null`. Part of the set's identity. */
23
+ readonly sha: string | null;
24
+ /** When the specs were recorded, epoch milliseconds; orders the catalog (newest first). */
25
+ readonly recordedAtMs: number;
26
+ /** `modified` for a checkout with uncommitted edits, `unknown` outside git; absent for a clean checkout. Part of the set's identity. */
27
+ readonly freshness?: 'unknown' | 'modified';
28
+ /** The group shown in the viewer for every file. Default `product`. */
29
+ readonly domain?: string;
30
+ };
31
+ /**
32
+ * Publishes the specs of one version and returns the set. Bodies are stored by content, so a spec that did not change
33
+ * costs nothing, and the set is named by what it holds: publishing the same specs from the same commit twice
34
+ * writes the same set. The catalog entry is written last.
35
+ *
36
+ * The set's `id` is the SHA-256 of the branch, sha, freshness and each file's path and digest, so the same specs from a
37
+ * developer's edited worktree and from CI are different sets. Store errors reject; a failure part-way leaves bodies
38
+ * no set names, never a set whose bodies are missing.
39
+ */
40
+ export declare function publishSpecSet(store: ObjectStore, input: PublishSpecSetInput): Promise<SpecSet>;
41
+ /** Reads and validates a spec set by id, or `null` when it is missing. Throws `Spec set <id> is not valid: ...` when it fails the schema, and a `SyntaxError` when it is not JSON. */
42
+ export declare function readSpecSet(store: ObjectStore, id: string): Promise<SpecSet | null>;
43
+ /** Reads the text of a spec body by its SHA-256 digest, or `null` when it is missing or `digest` is not 64 lowercase hex digits. */
44
+ export declare function readSpecBody(store: ObjectStore, digest: string): Promise<string | null>;
45
+ type Found = {
46
+ readonly set: SpecSet;
47
+ readonly store: ObjectStore;
48
+ };
49
+ /**
50
+ * The spec set the report is laid over: the newest one of `branch` in any of the stores. Without a
51
+ * branch, the newest of `main` or `master`, and failing that the newest of all, so a project that
52
+ * only ever publishes from other branches still has an outline.
53
+ *
54
+ * A set from a modified checkout (a developer's uncommitted edits) is used only when no set from a
55
+ * commit exists: the shared view does not follow anyone's work in progress. The branch and cleanliness
56
+ * are read from the catalog's keys, so choosing costs a listing and a read or two, however many sets
57
+ * other branches have published. `cache` keeps sets already read: they never change.
58
+ *
59
+ * Resolves to the set and the store it was found in, or `null` when no store has a fitting set (with an explicit
60
+ * `branch`, only that branch counts). A store that fails is skipped; the first error is thrown only when every store
61
+ * failed. A set that cannot be read is skipped.
62
+ */
63
+ export declare function latestSpecSet(stores: readonly ObjectStore[], branch?: string, cache?: Map<string, SpecSet>): Promise<Found | null>;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * @module Spec sets
3
+ *
4
+ * Publishes and finds spec sets: the specs of one commit, stored in the store itself so a viewer needs no checkout. Bodies
5
+ * are content-addressed in `layers/specs/blobs/<sha256>.txt`, the set is `layers/specs/sets/<id>.json`, and the catalog
6
+ * entry `layers/specs/index/<inverted-time>-<branch>-<flag>-<id>.json` is written last. `latestSpecSet` picks the outline
7
+ * a report is laid over: the newest set of `main` (then `master`, then any branch), preferring a set from a clean
8
+ * commit over one from a modified checkout.
9
+ *
10
+ * @see docs/report-contract.md#where-bytes-go
11
+ */
12
+ import { SpecSetSchema } from '../../schema/src/layer.js';
13
+ import { eachLimited } from './completion-order.js';
14
+ import { collectSpecFiles, isSpecFile, specKindOf, specTitleOf } from './directory-outline.js';
15
+ import { textFingerprint } from './layer-merge.js';
16
+ import { readUtf8, writeUtf8 } from './object-io.js';
17
+ import { isPaged } from './object-store.js';
18
+ import { sha256Hex } from './sigv4.js';
19
+ export { collectSpecFiles, isSpecFile };
20
+ const SET_PREFIX = 'layers/specs/sets/';
21
+ const INDEX_PREFIX = 'layers/specs/index/';
22
+ const BLOB_PREFIX = 'layers/specs/blobs/';
23
+ /** A branch as it appears in a catalog key: lowercase letters and digits. `_` for none. */
24
+ const branchSlug = (branch) => branch?.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || '_';
25
+ const flagOf = (freshness) => (freshness === undefined ? 'c' : freshness === 'modified' ? 'm' : 'u');
26
+ /**
27
+ * Publishes the specs of one version and returns the set. Bodies are stored by content, so a spec that did not change
28
+ * costs nothing, and the set is named by what it holds: publishing the same specs from the same commit twice
29
+ * writes the same set. The catalog entry is written last.
30
+ *
31
+ * The set's `id` is the SHA-256 of the branch, sha, freshness and each file's path and digest, so the same specs from a
32
+ * developer's edited worktree and from CI are different sets. Store errors reject; a failure part-way leaves bodies
33
+ * no set names, never a set whose bodies are missing.
34
+ */
35
+ export async function publishSpecSet(store, input) {
36
+ const files = await Promise.all([...input.files].sort((a, b) => (a.path < b.path ? -1 : 1)).map(async (file) => {
37
+ const bytes = new TextEncoder().encode(file.text);
38
+ return { path: file.path, digest: await sha256Hex(bytes), fingerprint: textFingerprint(file.text), kind: specKindOf(file.path), title: specTitleOf(file.path, file.text), domain: input.domain ?? 'product', bytes: bytes.length, text: file.text };
39
+ }));
40
+ await eachLimited(files, 16, async (file) => {
41
+ const key = `${BLOB_PREFIX}${file.digest}.txt`;
42
+ if (!(await store.head(key)))
43
+ await writeUtf8(store, key, file.text, 'text/plain; charset=utf-8');
44
+ });
45
+ // Freshness is part of the identity: the same specs from a developer's edited worktree and from CI are different sets.
46
+ const id = await sha256Hex(JSON.stringify([input.branch, input.sha, input.freshness ?? null, files.map(file => [file.path, file.digest])]));
47
+ const set = {
48
+ schemaVersion: 1, id, branch: input.branch, sha: input.sha, recordedAtMs: input.recordedAtMs,
49
+ ...(input.freshness ? { freshness: input.freshness } : {}),
50
+ files: files.map(({ text: _text, ...file }) => file)
51
+ };
52
+ if (!(await store.head(`${SET_PREFIX}${id}.json`)))
53
+ await writeUtf8(store, `${SET_PREFIX}${id}.json`, JSON.stringify(set), 'application/json');
54
+ const inverse = String(Number.MAX_SAFE_INTEGER - Math.max(0, Math.floor(input.recordedAtMs))).padStart(16, '0');
55
+ // The branch and whether the checkout was clean are in the key, so the outline is found without opening every set.
56
+ await writeUtf8(store, `${INDEX_PREFIX}${inverse}-${branchSlug(input.branch)}-${flagOf(input.freshness)}-${id}.json`, JSON.stringify({ id }), 'application/json');
57
+ return set;
58
+ }
59
+ /** Reads and validates a spec set by id, or `null` when it is missing. Throws `Spec set <id> is not valid: ...` when it fails the schema, and a `SyntaxError` when it is not JSON. */
60
+ export async function readSpecSet(store, id) {
61
+ const raw = await readUtf8(store, `${SET_PREFIX}${id}.json`);
62
+ if (raw === null)
63
+ return null;
64
+ const parsed = SpecSetSchema.safeParse(JSON.parse(raw));
65
+ if (!parsed.success)
66
+ throw new Error(`Spec set ${id.slice(0, 12)} is not valid: ${parsed.error.issues[0]?.message ?? 'unknown'}`);
67
+ return parsed.data;
68
+ }
69
+ /** Reads the text of a spec body by its SHA-256 digest, or `null` when it is missing or `digest` is not 64 lowercase hex digits. */
70
+ export async function readSpecBody(store, digest) {
71
+ return /^[0-9a-f]{64}$/.test(digest) ? readUtf8(store, `${BLOB_PREFIX}${digest}.txt`) : null;
72
+ }
73
+ const DEFAULT_BRANCHES = ['main', 'master'];
74
+ const MAX_PAGES = 20;
75
+ const newer = (a, b) => b.set.recordedAtMs - a.set.recordedAtMs || (a.set.id < b.set.id ? -1 : 1);
76
+ const clean = (found) => found.set.freshness === undefined;
77
+ const ENTRY = /(?:^|\/)(\d{16})-(.+)-([cmu])-([0-9a-f]{64})\.json$/;
78
+ /** The catalog, newest first, a page at a time where the store can. */
79
+ async function* catalog(store) {
80
+ const parse = (key) => { const match = ENTRY.exec(key); return match ? { slug: match[2], flag: match[3], id: match[4] } : null; };
81
+ if (!isPaged(store)) {
82
+ for (const key of await store.listKeys(INDEX_PREFIX)) {
83
+ const entry = parse(key);
84
+ if (entry)
85
+ yield entry;
86
+ }
87
+ return;
88
+ }
89
+ let after;
90
+ for (let page = 0; page < MAX_PAGES; page++) {
91
+ const result = await store.listPage(INDEX_PREFIX, { ...(after === undefined ? {} : { after }), limit: 1000 });
92
+ for (const key of result.keys) {
93
+ const entry = parse(key);
94
+ if (entry)
95
+ yield entry;
96
+ }
97
+ if (result.next === null)
98
+ return;
99
+ after = result.next;
100
+ }
101
+ }
102
+ /**
103
+ * The spec set the report is laid over: the newest one of `branch` in any of the stores. Without a
104
+ * branch, the newest of `main` or `master`, and failing that the newest of all, so a project that
105
+ * only ever publishes from other branches still has an outline.
106
+ *
107
+ * A set from a modified checkout (a developer's uncommitted edits) is used only when no set from a
108
+ * commit exists: the shared view does not follow anyone's work in progress. The branch and cleanliness
109
+ * are read from the catalog's keys, so choosing costs a listing and a read or two, however many sets
110
+ * other branches have published. `cache` keeps sets already read: they never change.
111
+ *
112
+ * Resolves to the set and the store it was found in, or `null` when no store has a fitting set (with an explicit
113
+ * `branch`, only that branch counts). A store that fails is skipped; the first error is thrown only when every store
114
+ * failed. A set that cannot be read is skipped.
115
+ */
116
+ export async function latestSpecSet(stores, branch, cache = new Map()) {
117
+ const wanted = new Set((branch ? [branch] : DEFAULT_BRANCHES).map(name => branchSlug(name)));
118
+ const read = async (store, id) => {
119
+ const known = cache.get(id);
120
+ if (known)
121
+ return known;
122
+ const set = await readSpecSet(store, id).catch(() => null);
123
+ if (set)
124
+ cache.set(id, set);
125
+ return set;
126
+ };
127
+ const preferred = [];
128
+ const newest = [];
129
+ const failures = [];
130
+ for (const store of stores) {
131
+ let firstAccepted = null;
132
+ let firstOverall = null;
133
+ let firstCleanOverall = null;
134
+ try {
135
+ for await (const entry of catalog(store)) {
136
+ // Entries are newest first. Stop at the first clean set of the branch; remember the others for the fallbacks.
137
+ if (!wanted.has(entry.slug)) {
138
+ if (!branch && entry.flag === 'c' && !firstCleanOverall) {
139
+ const set = await read(store, entry.id);
140
+ if (set)
141
+ firstCleanOverall = { set, store };
142
+ }
143
+ else if (!branch && !firstOverall && !firstCleanOverall) {
144
+ const set = await read(store, entry.id);
145
+ if (set)
146
+ firstOverall = { set, store };
147
+ }
148
+ continue;
149
+ }
150
+ const set = await read(store, entry.id);
151
+ if (!set || (branch && set.branch !== branch))
152
+ continue;
153
+ const found = { set, store };
154
+ firstAccepted ??= found;
155
+ if (entry.flag === 'c') {
156
+ firstAccepted = found;
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ catch (error) {
162
+ // One store that is down does not take the outline with it.
163
+ failures.push(error);
164
+ continue;
165
+ }
166
+ if (firstAccepted)
167
+ preferred.push(firstAccepted);
168
+ const fallback = firstCleanOverall ?? firstOverall;
169
+ if (fallback)
170
+ newest.push(fallback);
171
+ }
172
+ if (failures.length === stores.length && stores.length > 0)
173
+ throw failures[0];
174
+ const pool = preferred.length || branch ? preferred : newest;
175
+ const cleaned = pool.filter(clean);
176
+ return [...(cleaned.length ? cleaned : pool)].sort(newer)[0] ?? null;
177
+ }
@@ -1,4 +1,14 @@
1
+ /**
2
+ * @module Suite status
3
+ *
4
+ * The one rule for rolling test outcomes up into a suite or commit outcome, used wherever a recorded run is summarised.
5
+ */
1
6
  import type { ReportTestStatus } from './report-types.js';
7
+ /**
8
+ * The worst outcome of a set of tests or sources: `failed` if any failed, `skipped` if there is at least one and all
9
+ * were skipped, otherwise `passed`. An empty list is `passed`: callers that need "no evidence" to mean something else
10
+ * (for example `closeCommit`) check for emptiness themselves.
11
+ */
2
12
  export declare function suiteStatus(tests: readonly {
3
13
  readonly status: ReportTestStatus;
4
14
  }[]): ReportTestStatus;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * The worst outcome of a set of tests or sources: `failed` if any failed, `skipped` if there is at least one and all
3
+ * were skipped, otherwise `passed`. An empty list is `passed`: callers that need "no evidence" to mean something else
4
+ * (for example `closeCommit`) check for emptiness themselves.
5
+ */
1
6
  export function suiteStatus(tests) {
2
7
  if (tests.some((t) => t.status === 'failed'))
3
8
  return 'failed';
@@ -1,21 +1,49 @@
1
+ /**
2
+ * @module Prefix-scoped object store
3
+ *
4
+ * A decorator that makes a store look like it starts at a key prefix: callers use short keys, the shared bucket stays
5
+ * partitioned by prefix, and prefixes compose. It guards the keys it is given (no leading `/`, no `..`) so a scoped
6
+ * store cannot reach outside its prefix by key.
7
+ */
1
8
  import type { ObjectStore, StoredObject } from './object-store.js';
9
+ /** Returns `prefix` with exactly one trailing `/` added when missing. Throws `ObjectStoreWithPrefix: prefix must be non-empty` for an empty prefix. */
2
10
  export declare function normalizeObjectStorePrefix(prefix: string): string;
3
11
  /**
4
12
  * Decorator that scopes every {@link ObjectStore} operation to a fixed key
5
13
  * prefix. Used at composition roots (e.g. API bindings) so feature code uses
6
14
  * logical keys while a shared bucket stays partitioned by prefix.
15
+ *
16
+ * Keys passed to `get`, `put`, `head`, `delete`, `deleteMany`, `getUri` are checked (`assertLogicalObjectKey`: not
17
+ * empty, no leading `/`, no `..`) and throw otherwise. The `prefix` argument of `listKeys` and `listPage` is not
18
+ * checked here; the store underneath is responsible for it. Returned keys have the prefix removed.
7
19
  */
8
20
  export declare class ObjectStoreWithPrefix implements ObjectStore {
9
21
  private readonly inner;
10
22
  private readonly prefix;
11
23
  constructor(inner: ObjectStore, prefix: string);
12
24
  private scopedKey;
25
+ /** Reads `prefix + key` from the underlying store; `null` when missing. Throws for an invalid key. */
13
26
  get(key: string): Promise<StoredObject | null>;
27
+ /** Writes `prefix + key` in the underlying store. Throws for an invalid key. */
14
28
  put(key: string, bytes: Uint8Array, contentType: string): Promise<void>;
29
+ /** Whether `prefix + key` exists in the underlying store. Throws for an invalid key. */
15
30
  head(key: string): Promise<boolean>;
31
+ /** Removes `prefix + key`; a no-op when missing. Throws for an invalid key. */
16
32
  delete(key: string): Promise<void>;
33
+ /** Removes every `prefix + key`. Throws for an invalid key, before deleting any. */
17
34
  deleteMany(keys: readonly string[]): Promise<void>;
35
+ /** The underlying store's URL for `prefix + key`, or `null`. Throws for an invalid key. */
18
36
  getUri(key: string): Promise<string | null>;
37
+ /** Lists keys under `prefix` (relative to this store's prefix) in the underlying store's order, with this store's prefix removed. */
19
38
  listKeys(prefix?: string): Promise<readonly string[]>;
39
+ /** Paged listing with this store's prefix removed from keys and from `next`; present only when the store underneath has `listPage`. */
40
+ listPage?: (prefix: string, options?: {
41
+ readonly after?: string;
42
+ readonly limit?: number;
43
+ }) => Promise<{
44
+ readonly keys: readonly string[];
45
+ readonly next: string | null;
46
+ }>;
47
+ /** A store scoped to this prefix followed by `subPrefix`, over the same underlying store (`a/` then `b/` is `a/b/`). */
20
48
  withPrefix(subPrefix: string): ObjectStore;
21
49
  }
@@ -1,4 +1,5 @@
1
1
  import { assertLogicalObjectKey } from './namespace-guard.js';
2
+ /** Returns `prefix` with exactly one trailing `/` added when missing. Throws `ObjectStoreWithPrefix: prefix must be non-empty` for an empty prefix. */
2
3
  export function normalizeObjectStorePrefix(prefix) {
3
4
  if (prefix.length === 0) {
4
5
  throw new Error('ObjectStoreWithPrefix: prefix must be non-empty');
@@ -12,6 +13,10 @@ function joinObjectStorePrefixes(left, right) {
12
13
  * Decorator that scopes every {@link ObjectStore} operation to a fixed key
13
14
  * prefix. Used at composition roots (e.g. API bindings) so feature code uses
14
15
  * logical keys while a shared bucket stays partitioned by prefix.
16
+ *
17
+ * Keys passed to `get`, `put`, `head`, `delete`, `deleteMany`, `getUri` are checked (`assertLogicalObjectKey`: not
18
+ * empty, no leading `/`, no `..`) and throw otherwise. The `prefix` argument of `listKeys` and `listPage` is not
19
+ * checked here; the store underneath is responsible for it. Returned keys have the prefix removed.
15
20
  */
16
21
  export class ObjectStoreWithPrefix {
17
22
  inner;
@@ -19,34 +24,51 @@ export class ObjectStoreWithPrefix {
19
24
  constructor(inner, prefix) {
20
25
  this.inner = inner;
21
26
  this.prefix = normalizeObjectStorePrefix(prefix);
27
+ const paged = inner.listPage;
28
+ if (paged) {
29
+ this.listPage = async (listPrefix, options = {}) => {
30
+ const page = await paged.call(inner, `${this.prefix}${listPrefix}`, { ...options, ...(options.after === undefined ? {} : { after: `${this.prefix}${options.after}` }) });
31
+ return { keys: page.keys.map(key => key.slice(this.prefix.length)), next: page.next === null ? null : page.next.slice(this.prefix.length) };
32
+ };
33
+ }
22
34
  }
23
35
  scopedKey(key) {
24
36
  assertLogicalObjectKey(key);
25
37
  return `${this.prefix}${key}`;
26
38
  }
39
+ /** Reads `prefix + key` from the underlying store; `null` when missing. Throws for an invalid key. */
27
40
  get(key) {
28
41
  return this.inner.get(this.scopedKey(key));
29
42
  }
43
+ /** Writes `prefix + key` in the underlying store. Throws for an invalid key. */
30
44
  put(key, bytes, contentType) {
31
45
  return this.inner.put(this.scopedKey(key), bytes, contentType);
32
46
  }
47
+ /** Whether `prefix + key` exists in the underlying store. Throws for an invalid key. */
33
48
  head(key) {
34
49
  return this.inner.head(this.scopedKey(key));
35
50
  }
51
+ /** Removes `prefix + key`; a no-op when missing. Throws for an invalid key. */
36
52
  delete(key) {
37
53
  return this.inner.delete(this.scopedKey(key));
38
54
  }
55
+ /** Removes every `prefix + key`. Throws for an invalid key, before deleting any. */
39
56
  deleteMany(keys) {
40
57
  return this.inner.deleteMany(keys.map((key) => this.scopedKey(key)));
41
58
  }
59
+ /** The underlying store's URL for `prefix + key`, or `null`. Throws for an invalid key. */
42
60
  getUri(key) {
43
61
  return this.inner.getUri(this.scopedKey(key));
44
62
  }
63
+ /** Lists keys under `prefix` (relative to this store's prefix) in the underlying store's order, with this store's prefix removed. */
45
64
  async listKeys(prefix = '') {
46
65
  const scopedPrefix = `${this.prefix}${prefix}`;
47
66
  const keys = await this.inner.listKeys(scopedPrefix);
48
67
  return keys.map((k) => k.slice(this.prefix.length));
49
68
  }
69
+ /** Paged listing with this store's prefix removed from keys and from `next`; present only when the store underneath has `listPage`. */
70
+ listPage;
71
+ /** A store scoped to this prefix followed by `subPrefix`, over the same underlying store (`a/` then `b/` is `a/b/`). */
50
72
  withPrefix(subPrefix) {
51
73
  return new ObjectStoreWithPrefix(this.inner, joinObjectStorePrefixes(this.prefix, subPrefix));
52
74
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @module Stores named by URL
3
+ *
4
+ * Turns a store name (`./folder`, `s3://bucket/prefix`, `r2://…`, `gs://…`, `https://atlas.example.com`) into an opened
5
+ * `ObjectStore`, and a list of names (`ATLAS_STORE`, `--store`) into backends with unique ids. Secrets never travel in a
6
+ * URL: S3 credentials come from the environment, an Atlas server's token from `ATLAS_TOKEN` (or `ATLAS_TOKEN_<ID>` for a
7
+ * named store when several servers are used), and a token is never sent to a server over plain `http://` beyond this machine.
8
+ *
9
+ * @see docs/deploying.md#1-where-results-are-kept
10
+ */
11
+ import type { BackendInput } from './compose.js';
12
+ import type { ObjectStore } from './object-store.js';
13
+ import { type HttpStore } from './http-store.js';
14
+ import { type S3Store } from './s3-store.js';
15
+ type Env = Readonly<Record<string, string | undefined>>;
16
+ /** Where a store is, parsed: a `directory`, an S3-compatible `s3` bucket and prefix (with region, endpoint and addressing), or an Atlas `server` URL. */
17
+ export type StoreLocation = {
18
+ readonly kind: 'directory';
19
+ readonly directory: string;
20
+ } | {
21
+ readonly kind: 's3';
22
+ readonly bucket: string;
23
+ readonly prefix: string;
24
+ readonly region?: string;
25
+ readonly endpoint?: string;
26
+ readonly pathStyle?: boolean;
27
+ }
28
+ /** An Atlas server: uploads go to it, and it shows everyone the combined report. */
29
+ | {
30
+ readonly kind: 'server';
31
+ readonly url: string;
32
+ };
33
+ /** A store opened from a location: the `ObjectStore`, a `label` for people, the parsed `location`, and, by kind, the `s3` or `server` store with its extra methods. */
34
+ export type OpenedStore = {
35
+ readonly store: ObjectStore;
36
+ /** What to show people: no credentials, ever. */
37
+ readonly label: string;
38
+ readonly location: StoreLocation;
39
+ /** The S3 handle, for credential source and ping. */
40
+ readonly s3?: S3Store;
41
+ /** The server handle, for ping. */
42
+ readonly server?: HttpStore;
43
+ };
44
+ /**
45
+ * Where a store is, from one string:
46
+ *
47
+ * - `./atlas-reports`, `/srv/atlas`, `file:///srv/atlas`: a directory.
48
+ * - `s3://bucket/prefix`: S3. Add `?region=eu-west-1`, `?endpoint=https://…` for R2, MinIO and the
49
+ * rest, `?pathStyle=1`. Keys are never in the URL: they come from the environment (see
50
+ * `credentialChain`).
51
+ * - `r2://bucket/prefix?account=<id>` (Cloudflare R2) and `gs://bucket/prefix` (Google Cloud Storage with
52
+ * HMAC keys): the same store with the endpoint filled in. Atlas does not test against these two.
53
+ */
54
+ export declare function parseStoreLocation(spec: string, base: string, env?: Env): StoreLocation;
55
+ /** Which store this is among those opened together, so a token goes only to the server it is for. */
56
+ export type StoreRole = {
57
+ readonly id: string; /** The only Atlas server among them: the one `ATLAS_TOKEN` is for. */
58
+ readonly soleServer: boolean;
59
+ };
60
+ /**
61
+ * Opens one store from its name. A directory is not created here (commands that write create it). Throws for a name it cannot make sense of, and for a token that would be sent over plain `http://` beyond this machine.
62
+ *
63
+ * @example
64
+ * const { store } = openStore('s3://my-bucket/atlas/my-project?region=us-east-1', { base: process.cwd(), env: process.env });
65
+ */
66
+ export declare function openStore(spec: string, options: {
67
+ readonly base: string;
68
+ readonly env?: Env;
69
+ readonly fetch?: typeof fetch;
70
+ readonly role?: StoreRole;
71
+ }): OpenedStore;
72
+ /**
73
+ * The token a client sends to one Atlas server. `ATLAS_TOKEN_<ID>` (or the first line of the file `ATLAS_TOKEN_FILE_<ID>`
74
+ * names) is for the store called `<ID>`; `ATLAS_TOKEN` / `ATLAS_TOKEN_FILE` is for the only server there is. With
75
+ * several servers, `ATLAS_TOKEN` is for none of them: a token is a secret of one server, never sent to another.
76
+ */
77
+ export declare function serverToken(env: Env, role?: StoreRole, readFile?: (file: string) => string | null): string | null;
78
+ /** A short id for a store, unique among the given ones: `my-bucket-ci`, `reports`. */
79
+ export declare function storeId(location: StoreLocation, taken: ReadonlySet<string>): string;
80
+ /** `--store` values, or `ATLAS_STORE` when there are none: one store, or several separated by commas or new lines (a folder may have spaces in its name). */
81
+ export declare function storeSpecs(flags: readonly string[], env: Env): string[];
82
+ /** `id=url` or a bare url, opened and given a unique id. */
83
+ export declare function openBackends(specs: readonly string[], options: {
84
+ readonly base: string;
85
+ readonly env?: Env;
86
+ readonly fetch?: typeof fetch;
87
+ readonly mustExist?: boolean;
88
+ }): (BackendInput & OpenedStore)[];
89
+ export {};
@@ -0,0 +1,160 @@
1
+ /**
2
+ * @module Stores named by URL
3
+ *
4
+ * Turns a store name (`./folder`, `s3://bucket/prefix`, `r2://…`, `gs://…`, `https://atlas.example.com`) into an opened
5
+ * `ObjectStore`, and a list of names (`ATLAS_STORE`, `--store`) into backends with unique ids. Secrets never travel in a
6
+ * URL: S3 credentials come from the environment, an Atlas server's token from `ATLAS_TOKEN` (or `ATLAS_TOKEN_<ID>` for a
7
+ * named store when several servers are used), and a token is never sent to a server over plain `http://` beyond this machine.
8
+ *
9
+ * @see docs/deploying.md#1-where-results-are-kept
10
+ */
11
+ import { readFileSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { directoryStore } from './directory-store.js';
15
+ import { httpStore } from './http-store.js';
16
+ import { s3Store } from './s3-store.js';
17
+ /**
18
+ * Where a store is, from one string:
19
+ *
20
+ * - `./atlas-reports`, `/srv/atlas`, `file:///srv/atlas`: a directory.
21
+ * - `s3://bucket/prefix`: S3. Add `?region=eu-west-1`, `?endpoint=https://…` for R2, MinIO and the
22
+ * rest, `?pathStyle=1`. Keys are never in the URL: they come from the environment (see
23
+ * `credentialChain`).
24
+ * - `r2://bucket/prefix?account=<id>` (Cloudflare R2) and `gs://bucket/prefix` (Google Cloud Storage with
25
+ * HMAC keys): the same store with the endpoint filled in. Atlas does not test against these two.
26
+ */
27
+ export function parseStoreLocation(spec, base, env = {}) {
28
+ // Shorthands for two services that speak S3's protocol, so there is no endpoint to look up.
29
+ const shorthand = /^(r2|gs):\/\/(.*)$/i.exec(spec);
30
+ if (shorthand) {
31
+ const kind = shorthand[1].toLowerCase();
32
+ const url = new URL(`s3://${shorthand[2]}`);
33
+ const account = url.searchParams.get('account') ?? env.R2_ACCOUNT_ID ?? env.CLOUDFLARE_ACCOUNT_ID;
34
+ if (kind === 'r2' && !account)
35
+ throw new Error('An r2:// store needs the Cloudflare account id: r2://bucket/folder?account=<id>, or set R2_ACCOUNT_ID');
36
+ url.searchParams.delete('account');
37
+ url.searchParams.set('endpoint', kind === 'r2' ? `https://${account}.r2.cloudflarestorage.com` : 'https://storage.googleapis.com');
38
+ if (!url.searchParams.has('region'))
39
+ url.searchParams.set('region', 'auto');
40
+ return parseStoreLocation(url.toString(), base, env);
41
+ }
42
+ if (/^s3:\/\//i.test(spec)) {
43
+ const url = new URL(spec);
44
+ const bucket = decodeURIComponent(url.hostname);
45
+ if (!bucket)
46
+ throw new Error(`"${spec}" names no bucket. Write s3://<bucket>/<folder>`);
47
+ if (url.username || url.password)
48
+ throw new Error('Keys do not belong in a store URL: put them in AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (or ATLAS_S3_*), or sign in with `aws sso login`.');
49
+ const prefix = decodeURIComponent(url.pathname).replace(/^\/+|\/+$/g, '');
50
+ const region = url.searchParams.get('region') ?? undefined;
51
+ const endpoint = url.searchParams.get('endpoint') ?? undefined;
52
+ const pathStyle = url.searchParams.get('pathStyle');
53
+ return { kind: 's3', bucket, prefix, ...(region ? { region } : {}), ...(endpoint ? { endpoint } : {}), ...(pathStyle ? { pathStyle: pathStyle !== '0' && pathStyle !== 'false' } : {}) };
54
+ }
55
+ if (/^https?:\/\//i.test(spec)) {
56
+ const url = new URL(spec);
57
+ if (url.username || url.password)
58
+ throw new Error('A token does not belong in a server URL: set ATLAS_TOKEN.');
59
+ return { kind: 'server', url: `${url.origin}${url.pathname.replace(/\/+$/, '')}` };
60
+ }
61
+ if (/^[a-z][a-z0-9+.-]+:\/\//i.test(spec) && !/^file:\/\//i.test(spec))
62
+ throw new Error(`Unsupported store "${spec}". Use a directory, an Atlas server (https://…), s3://bucket/prefix, r2://bucket/prefix?account=<id> or gs://bucket/prefix`);
63
+ return { kind: 'directory', directory: /^file:\/\//i.test(spec) ? fileURLToPath(spec) : path.resolve(base, spec) };
64
+ }
65
+ /**
66
+ * Opens one store from its name. A directory is not created here (commands that write create it). Throws for a name it cannot make sense of, and for a token that would be sent over plain `http://` beyond this machine.
67
+ *
68
+ * @example
69
+ * const { store } = openStore('s3://my-bucket/atlas/my-project?region=us-east-1', { base: process.cwd(), env: process.env });
70
+ */
71
+ export function openStore(spec, options) {
72
+ const location = parseStoreLocation(spec, options.base, options.env ?? {});
73
+ if (location.kind === 'directory')
74
+ return { store: directoryStore(location.directory), label: location.directory, location };
75
+ if (location.kind === 'server') {
76
+ const env = options.env ?? {};
77
+ const token = serverToken(env, options.role ?? { id: '', soleServer: true });
78
+ if (token)
79
+ refuseClearToken(location.url, env);
80
+ const server = httpStore({ url: location.url, ...(env.ATLAS_PROJECT ? { project: env.ATLAS_PROJECT } : {}), ...(token ? { token } : {}), ...(options.fetch ? { fetch: options.fetch } : {}) });
81
+ return { store: server, label: server.label, location, server };
82
+ }
83
+ const s3 = s3Store({
84
+ bucket: location.bucket,
85
+ prefix: location.prefix,
86
+ ...(location.region ? { region: location.region } : {}),
87
+ ...(location.endpoint ? { endpoint: location.endpoint } : {}),
88
+ ...(location.pathStyle !== undefined ? { forcePathStyle: location.pathStyle } : {}),
89
+ ...(options.env ? { env: options.env } : {}),
90
+ ...(options.fetch ? { fetch: options.fetch } : {})
91
+ });
92
+ return { store: s3, label: s3.label, location, s3 };
93
+ }
94
+ /**
95
+ * The token a client sends to one Atlas server. `ATLAS_TOKEN_<ID>` (or the first line of the file `ATLAS_TOKEN_FILE_<ID>`
96
+ * names) is for the store called `<ID>`; `ATLAS_TOKEN` / `ATLAS_TOKEN_FILE` is for the only server there is. With
97
+ * several servers, `ATLAS_TOKEN` is for none of them: a token is a secret of one server, never sent to another.
98
+ */
99
+ export function serverToken(env, role = { id: '', soleServer: true }, readFile = file => { try {
100
+ return readFileSync(file, 'utf8');
101
+ }
102
+ catch {
103
+ return null;
104
+ } }) {
105
+ const suffix = role.id ? `_${role.id.toUpperCase().replace(/[^A-Z0-9]/g, '_')}` : null;
106
+ const named = suffix ? env[`ATLAS_TOKEN${suffix}`] : undefined;
107
+ if (named)
108
+ return named.trim();
109
+ const namedFile = suffix ? env[`ATLAS_TOKEN_FILE${suffix}`] : undefined;
110
+ if (namedFile)
111
+ return readFile(namedFile)?.split('\n')[0]?.trim() || null;
112
+ if (!role.soleServer)
113
+ return null;
114
+ if (env.ATLAS_TOKEN)
115
+ return env.ATLAS_TOKEN.trim();
116
+ if (env.ATLAS_TOKEN_FILE)
117
+ return readFile(env.ATLAS_TOKEN_FILE)?.split('\n')[0]?.trim() || null;
118
+ return null;
119
+ }
120
+ /** A token over plain `http` crosses the network readable by anyone on it: only to this machine, unless the network is said to be trusted. */
121
+ function refuseClearToken(url, env) {
122
+ const { protocol, hostname } = new URL(url);
123
+ if (protocol === 'https:' || ['localhost', '127.0.0.1', '[::1]', '::1'].includes(hostname) || env.ATLAS_TRUST_NETWORK === '1')
124
+ return;
125
+ throw new Error(`Not sending a token to ${hostname} over plain http: anyone on the network could read it. Use https://, or set ATLAS_TRUST_NETWORK=1 if this network is yours.`);
126
+ }
127
+ /** A short id for a store, unique among the given ones: `my-bucket-ci`, `reports`. */
128
+ export function storeId(location, taken) {
129
+ const raw = location.kind === 's3' ? [location.bucket, location.prefix].filter(Boolean).join('-') : location.kind === 'server' ? new URL(location.url).host : path.basename(location.directory);
130
+ const base = raw.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'store';
131
+ let id = base;
132
+ for (let n = 2; taken.has(id); n++)
133
+ id = `${base}-${n}`;
134
+ return id;
135
+ }
136
+ /** `--store` values, or `ATLAS_STORE` when there are none: one store, or several separated by commas or new lines (a folder may have spaces in its name). */
137
+ export function storeSpecs(flags, env) {
138
+ if (flags.length)
139
+ return [...flags];
140
+ return (env.ATLAS_STORE ?? '').split(/[\n,]+/).map(spec => spec.trim()).filter(Boolean);
141
+ }
142
+ /** `id=url` or a bare url, opened and given a unique id. */
143
+ export function openBackends(specs, options) {
144
+ const taken = new Set();
145
+ const named = specs.map(spec => /^([a-z0-9-]+)=(.+)$/.exec(spec)).flatMap(match => (match ? [match[1]] : []));
146
+ for (const id of named) {
147
+ if (taken.has(id))
148
+ throw new Error(`Two stores are named "${id}"`);
149
+ taken.add(id);
150
+ }
151
+ const servers = specs.filter(spec => /^https?:\/\//i.test(spec.replace(/^[a-z0-9-]+=/, ''))).length;
152
+ return specs.map(spec => {
153
+ const match = /^([a-z0-9-]+)=(.+)$/.exec(spec);
154
+ const target = match ? match[2] : spec;
155
+ const id = match ? match[1] : storeId(parseStoreLocation(target, options.base, options.env ?? {}), taken);
156
+ const opened = openStore(target, { ...options, role: { id, soleServer: servers === 1 } });
157
+ taken.add(id);
158
+ return { ...opened, id, label: match ? id : opened.location.kind === 'directory' ? path.basename(opened.location.directory) : opened.label };
159
+ });
160
+ }
@@ -1,3 +1,14 @@
1
+ /**
2
+ * @module Test as a catalog row
3
+ *
4
+ * Maps a recorded test to the flat row the viewer's searchable, groupable list draws.
5
+ */
1
6
  import type { HtmlReportTest } from './recording-bundle.js';
2
7
  import type { CatalogEntity } from './catalog.js';
8
+ /**
9
+ * Flattens a recorded test into a `CatalogEntity`: its id, title, status and duration; the base name of its source
10
+ * file; the package (the part of the viewer test id's source before `--`, or `report`); its runtime; and the first
11
+ * line of its error as the detail. `search` joins the title, ids, paths, system, runtime, package and error for text
12
+ * matching. `href` is `null`; the caller sets where a row links to.
13
+ */
3
14
  export declare function testCatalogEntity(test: HtmlReportTest): CatalogEntity;