@crvouga/atlas 0.3.0 → 0.5.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 (429) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +178 -604
  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 +30 -1
  9. package/dist/core/src/config.d.ts +70 -4
  10. package/dist/core/src/config.js +62 -3
  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 +69 -16
  30. package/dist/core/src/run/execute.js +67 -16
  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 +83 -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 +40 -4
  94. package/dist/reports/src/evidence-live.js +52 -4
  95. package/dist/reports/src/evidence-types.d.ts +70 -1
  96. package/dist/reports/src/evidence-types.js +5 -1
  97. package/dist/reports/src/evidence-view.d.ts +58 -6
  98. package/dist/reports/src/evidence-view.js +44 -5
  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 +52 -0
  120. package/dist/reports/src/http-store.js +145 -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 +364 -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 +48 -0
  134. package/dist/reports/src/layer-axes.js +71 -0
  135. package/dist/reports/src/layer-catalog.d.ts +28 -1
  136. package/dist/reports/src/layer-catalog.js +19 -2
  137. package/dist/reports/src/layer-merge.d.ts +98 -9
  138. package/dist/reports/src/layer-merge.js +154 -33
  139. package/dist/reports/src/layer-source.d.ts +18 -0
  140. package/dist/reports/src/layer-source.js +14 -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 +34 -1
  144. package/dist/reports/src/layers-route.d.ts +47 -4
  145. package/dist/reports/src/layers-route.js +88 -12
  146. package/dist/reports/src/media-types.d.ts +11 -0
  147. package/dist/reports/src/media-types.js +22 -0
  148. package/dist/reports/src/namespace-guard.d.ts +10 -1
  149. package/dist/reports/src/namespace-guard.js +10 -1
  150. package/dist/reports/src/navigation.d.ts +12 -0
  151. package/dist/reports/src/navigation.js +7 -0
  152. package/dist/reports/src/object-io.d.ts +11 -4
  153. package/dist/reports/src/object-io.js +4 -4
  154. package/dist/reports/src/object-store-conformance.d.ts +27 -0
  155. package/dist/reports/src/object-store-conformance.js +87 -0
  156. package/dist/reports/src/object-store.d.ts +33 -24
  157. package/dist/reports/src/object-store.js +17 -24
  158. package/dist/reports/src/parse-with.d.ts +16 -0
  159. package/dist/reports/src/parse-with.js +14 -0
  160. package/dist/reports/src/performance-part.d.ts +25 -1
  161. package/dist/reports/src/performance-part.js +15 -1
  162. package/dist/reports/src/performance.d.ts +22 -2
  163. package/dist/reports/src/performance.js +12 -2
  164. package/dist/reports/src/persist.d.ts +12 -3
  165. package/dist/reports/src/persist.js +12 -3
  166. package/dist/reports/src/phone.d.ts +18 -1
  167. package/dist/reports/src/phone.js +18 -1
  168. package/dist/reports/src/protocol.d.ts +30 -4
  169. package/dist/reports/src/protocol.js +15 -4
  170. package/dist/reports/src/publication.d.ts +21 -1
  171. package/dist/reports/src/publication.js +21 -1
  172. package/dist/reports/src/publish-checks.d.ts +35 -0
  173. package/dist/reports/src/publish-checks.js +122 -0
  174. package/dist/reports/src/publish.d.ts +44 -9
  175. package/dist/reports/src/publish.js +40 -7
  176. package/dist/reports/src/read.d.ts +46 -16
  177. package/dist/reports/src/read.js +38 -14
  178. package/dist/reports/src/recording-bundle.d.ts +13 -2
  179. package/dist/reports/src/recording-bundle.js +4 -2
  180. package/dist/reports/src/recording-keys.d.ts +15 -1
  181. package/dist/reports/src/recording-keys.js +15 -1
  182. package/dist/reports/src/remote-report.d.ts +28 -7
  183. package/dist/reports/src/remote-report.js +15 -7
  184. package/dist/reports/src/report-cli.d.ts +51 -0
  185. package/dist/reports/src/report-cli.js +485 -0
  186. package/dist/reports/src/report-index.d.ts +99 -0
  187. package/dist/reports/src/report-index.js +337 -0
  188. package/dist/reports/src/report-loader.d.ts +10 -0
  189. package/dist/reports/src/report-loader.js +6 -0
  190. package/dist/reports/src/report-parts.d.ts +29 -3
  191. package/dist/reports/src/report-parts.js +40 -3
  192. package/dist/reports/src/report-status.d.ts +48 -0
  193. package/dist/reports/src/report-status.js +156 -0
  194. package/dist/reports/src/report-types.d.ts +73 -0
  195. package/dist/reports/src/report-types.js +3 -0
  196. package/dist/reports/src/reporter.d.ts +67 -0
  197. package/dist/reports/src/reporter.js +128 -0
  198. package/dist/reports/src/reports-explorer-route.d.ts +541 -0
  199. package/dist/reports/src/reports-explorer-route.js +11 -0
  200. package/dist/reports/src/reports-list-route.d.ts +9 -0
  201. package/dist/reports/src/reports-list-route.js +9 -0
  202. package/dist/reports/src/reports-route.d.ts +561 -22
  203. package/dist/reports/src/reports-route.js +11 -3
  204. package/dist/reports/src/reports-store.d.ts +19 -2
  205. package/dist/reports/src/reports-store.js +10 -2
  206. package/dist/reports/src/resilient-cache.d.ts +14 -1
  207. package/dist/reports/src/resilient-cache.js +8 -1
  208. package/dist/reports/src/retention.d.ts +17 -1
  209. package/dist/reports/src/retention.js +5 -1
  210. package/dist/reports/src/run-bundle.d.ts +63 -0
  211. package/dist/reports/src/run-bundle.js +159 -0
  212. package/dist/reports/src/s3-store.d.ts +79 -0
  213. package/dist/reports/src/s3-store.js +295 -0
  214. package/dist/reports/src/scenario.d.ts +14 -4
  215. package/dist/reports/src/scenario.js +5 -4
  216. package/dist/reports/src/sigv4.d.ts +72 -0
  217. package/dist/reports/src/sigv4.js +96 -0
  218. package/dist/reports/src/snapshot-bundle.d.ts +24 -1
  219. package/dist/reports/src/snapshot-bundle.js +27 -2
  220. package/dist/reports/src/snapshot-surface.d.ts +20 -9
  221. package/dist/reports/src/snapshot-surface.js +32 -19
  222. package/dist/reports/src/source-content.d.ts +23 -0
  223. package/dist/reports/src/source-content.js +30 -1
  224. package/dist/reports/src/source-entries.d.ts +13 -0
  225. package/dist/reports/src/source-entries.js +6 -0
  226. package/dist/reports/src/source-part.d.ts +15 -0
  227. package/dist/reports/src/source-part.js +8 -0
  228. package/dist/reports/src/source-reader.d.ts +11 -0
  229. package/dist/reports/src/source-reader.js +5 -0
  230. package/dist/reports/src/spec-evidence.d.ts +12 -2
  231. package/dist/reports/src/spec-evidence.js +6 -2
  232. package/dist/reports/src/spec-inventory.d.ts +15 -1
  233. package/dist/reports/src/spec-inventory.js +22 -1
  234. package/dist/reports/src/spec-link.d.ts +28 -0
  235. package/dist/reports/src/spec-link.js +70 -0
  236. package/dist/reports/src/spec-set.d.ts +63 -0
  237. package/dist/reports/src/spec-set.js +177 -0
  238. package/dist/reports/src/status.d.ts +10 -0
  239. package/dist/reports/src/status.js +5 -0
  240. package/dist/reports/src/store-prefix.d.ts +28 -0
  241. package/dist/reports/src/store-prefix.js +22 -0
  242. package/dist/reports/src/store-url.d.ts +89 -0
  243. package/dist/reports/src/store-url.js +160 -0
  244. package/dist/reports/src/test-catalog-entity.d.ts +11 -0
  245. package/dist/reports/src/test-catalog-entity.js +6 -0
  246. package/dist/reports/src/types.d.ts +6 -1
  247. package/dist/reports/src/use-report-nav.d.ts +35 -0
  248. package/dist/reports/src/use-report-nav.js +12 -0
  249. package/dist/schema/src/api.d.ts +799 -0
  250. package/dist/schema/src/api.js +201 -0
  251. package/dist/schema/src/cli-results.d.ts +210 -0
  252. package/dist/schema/src/cli-results.js +124 -0
  253. package/dist/schema/src/common.d.ts +53 -0
  254. package/dist/schema/src/common.js +42 -0
  255. package/dist/schema/src/index.d.ts +14 -0
  256. package/dist/schema/src/index.js +14 -0
  257. package/dist/schema/src/layer.d.ts +2273 -0
  258. package/dist/schema/src/layer.js +462 -0
  259. package/dist/schema/src/manifest.d.ts +95 -0
  260. package/dist/schema/src/manifest.js +84 -0
  261. package/dist/schema/src/report-sources.d.ts +28 -0
  262. package/dist/schema/src/report-sources.js +25 -0
  263. package/dist/schema/src/runs-index.d.ts +16 -0
  264. package/dist/schema/src/runs-index.js +15 -0
  265. package/dist/schema/src/server-config.d.ts +93 -0
  266. package/dist/schema/src/server-config.js +73 -0
  267. package/dist/schema/src/spec.d.ts +69 -0
  268. package/dist/schema/src/spec.js +58 -0
  269. package/dist/server/src/app.d.ts +62 -0
  270. package/dist/server/src/app.js +419 -0
  271. package/dist/server/src/auth.d.ts +41 -0
  272. package/dist/server/src/auth.js +59 -0
  273. package/dist/server/src/config.d.ts +83 -0
  274. package/dist/server/src/config.js +187 -0
  275. package/dist/server/src/dev.d.ts +12 -0
  276. package/dist/server/src/dev.js +80 -0
  277. package/dist/server/src/events.d.ts +29 -0
  278. package/dist/server/src/events.js +101 -0
  279. package/dist/server/src/host.d.ts +30 -0
  280. package/dist/server/src/host.js +115 -0
  281. package/dist/server/src/index.d.ts +16 -0
  282. package/dist/server/src/index.js +16 -0
  283. package/dist/server/src/llms.d.ts +19 -0
  284. package/dist/server/src/llms.js +63 -0
  285. package/dist/server/src/main.d.ts +30 -0
  286. package/dist/server/src/main.js +112 -0
  287. package/dist/server/src/openapi.d.ts +59 -0
  288. package/dist/server/src/openapi.js +374 -0
  289. package/dist/server/src/problem.d.ts +8 -0
  290. package/dist/server/src/problem.js +39 -0
  291. package/dist/server/src/site.d.ts +13 -0
  292. package/dist/server/src/site.js +43 -0
  293. package/dist/server/src/store-api.d.ts +28 -0
  294. package/dist/server/src/store-api.js +159 -0
  295. package/dist/server/src/validate.d.ts +71 -0
  296. package/dist/server/src/validate.js +197 -0
  297. package/dist/vitest/src/index.d.ts +27 -5
  298. package/dist/vitest/src/index.js +18 -5
  299. package/dist/webdriver/src/driver.d.ts +63 -11
  300. package/dist/webdriver/src/driver.js +57 -6
  301. package/dist/webdriver/src/index.d.ts +8 -0
  302. package/dist/webdriver/src/index.js +1 -0
  303. package/dist/webdriver/src/protocol.d.ts +75 -6
  304. package/dist/webdriver/src/protocol.js +79 -6
  305. package/docs/agents.md +137 -0
  306. package/docs/deploying.md +345 -0
  307. package/docs/dogfooding.md +175 -0
  308. package/docs/extending.md +242 -0
  309. package/docs/integrating.md +485 -0
  310. package/docs/reference/bun.md +141 -0
  311. package/docs/reference/cli.md +146 -0
  312. package/docs/reference/core.md +2426 -0
  313. package/docs/reference/detox.md +150 -0
  314. package/docs/reference/http-api.md +303 -0
  315. package/docs/reference/playwright.md +303 -0
  316. package/docs/reference/reports.md +7098 -0
  317. package/docs/reference/schema.md +1651 -0
  318. package/docs/reference/server.md +685 -0
  319. package/docs/reference/vitest.md +45 -0
  320. package/docs/reference/webdriver.md +330 -0
  321. package/docs/report-backends.md +144 -0
  322. package/docs/report-contract.md +400 -0
  323. package/docs/reporting-issues.md +160 -0
  324. package/docs/server.md +285 -0
  325. package/docs/statecharts.md +641 -0
  326. package/llms-full.txt +3506 -0
  327. package/llms.txt +47 -0
  328. package/package.json +33 -5
  329. package/packages/core/README.md +2 -1
  330. package/packages/reports/README.md +69 -2
  331. package/packages/reports/STORAGE.md +5 -1
  332. package/packages/schema/README.md +14 -0
  333. package/packages/schema/schemas/check-detail.schema.json +1032 -0
  334. package/packages/schema/schemas/checks-result.schema.json +26 -0
  335. package/packages/schema/schemas/command-error.schema.json +35 -0
  336. package/packages/schema/schemas/doctor-result.schema.json +60 -0
  337. package/packages/schema/schemas/effective-delta.schema.json +490 -0
  338. package/packages/schema/schemas/effective-evidence.schema.json +471 -0
  339. package/packages/schema/schemas/evidence-page.schema.json +271 -0
  340. package/packages/schema/schemas/hello-event.schema.json +22 -0
  341. package/packages/schema/schemas/init-plan.schema.json +347 -0
  342. package/packages/schema/schemas/layer-rows.schema.json +199 -0
  343. package/packages/schema/schemas/layer.schema.json +2888 -0
  344. package/packages/schema/schemas/merge-result.schema.json +40 -0
  345. package/packages/schema/schemas/outline.schema.json +62 -0
  346. package/packages/schema/schemas/problem.schema.json +37 -0
  347. package/packages/schema/schemas/publish-checks.schema.json +1413 -0
  348. package/packages/schema/schemas/publish-result.schema.json +168 -0
  349. package/packages/schema/schemas/ready.schema.json +68 -0
  350. package/packages/schema/schemas/report-event.schema.json +100 -0
  351. package/packages/schema/schemas/report-status.schema.json +370 -0
  352. package/packages/schema/schemas/server-config.schema.json +194 -0
  353. package/packages/schema/schemas/server-info.schema.json +114 -0
  354. package/packages/schema/schemas/spec-set.schema.json +103 -0
  355. package/packages/schema/schemas/store-listing.schema.json +29 -0
  356. package/packages/schema/schemas/store-write-result.schema.json +28 -0
  357. package/packages/schema/schemas/stream-result.schema.json +59 -0
  358. package/packages/server/README.md +43 -0
  359. package/packages/server/openapi.json +6699 -0
  360. package/packages/visualizer/README.md +27 -5
  361. package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
  362. package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  363. package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
  364. package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
  365. package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
  366. package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
  367. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
  368. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  369. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
  370. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
  371. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
  372. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
  373. package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
  374. package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  375. package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
  376. package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
  377. package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
  378. package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
  379. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
  380. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  381. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
  382. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
  383. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
  384. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
  385. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
  386. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  387. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
  388. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
  389. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
  390. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
  391. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
  392. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  393. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
  394. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
  395. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
  396. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
  397. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
  398. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  399. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
  400. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
  401. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
  402. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
  403. package/packages/visualizer/scripts/make-fixtures.ts +2 -0
  404. package/packages/visualizer/scripts/publish-data.ts +1 -2
  405. package/packages/visualizer/src/app/AppShell.tsx +9 -2
  406. package/packages/visualizer/src/app/report-sources.tsx +6 -4
  407. package/packages/visualizer/src/app/router.tsx +3 -3
  408. package/packages/visualizer/src/components/Header.module.css +35 -0
  409. package/packages/visualizer/src/components/Header.tsx +41 -4
  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/evidence/api.ts +14 -2
  413. package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
  414. package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
  415. package/packages/visualizer/src/evidence/check-detail.tsx +38 -4
  416. package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
  417. package/packages/visualizer/src/evidence/evidence.module.css +20 -0
  418. package/packages/visualizer/src/evidence/marks.tsx +2 -0
  419. package/packages/visualizer/src/evidence/overview.tsx +3 -3
  420. package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
  421. package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
  422. package/packages/visualizer/src/find/Finder.tsx +1 -7
  423. package/packages/visualizer/src/find/open.ts +4 -0
  424. package/packages/visualizer/src/live/live-stream.ts +204 -0
  425. package/packages/visualizer/src/runtime.ts +19 -0
  426. package/packages/visualizer/src/state/ui-store.ts +6 -2
  427. package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
  428. package/packages/visualizer/vite/example-reports.ts +475 -0
  429. package/packages/visualizer/vite.config.ts +18 -2
@@ -1,23 +1,50 @@
1
1
  /**
2
- * Overlay test-runner outcomes (JUnit XML) onto a report bundle. A harness run
3
- * only sees failures raised inside its own steps; assertions in the test body
4
- * surface through the runner's report instead.
2
+ * @module JUnit XML parsing
3
+ *
4
+ * Reads the `<testcase>` elements of a JUnit XML document without an XML library, and overlays test-runner
5
+ * outcomes (JUnit XML) onto a report bundle. A harness run only sees failures raised inside its own steps;
6
+ * assertions in the test body surface through the runner's report instead. Parsing is tolerant by design
7
+ * (nested suites, self-closing elements, CDATA) and never drops a case silently, because a result that goes
8
+ * missing reads as a pass. `ingest.ts` turns the cases into checks.
5
9
  */
6
10
  import type { HtmlReportBundle } from './recording-bundle.js';
11
+ /** One `<testcase>` as read, with the suite it is in. */
7
12
  export type JunitCase = {
13
+ /** The case's `file` attribute, else its suite's, else empty. */
8
14
  readonly file: string;
15
+ /** The `classname` attribute, which many runners use for the suite or the file. */
16
+ readonly classname: string;
17
+ /** The `name` of the `<testsuite>` the case is in. */
18
+ readonly suite: string;
19
+ /** Seconds, when the runner emits it. */
20
+ readonly time: number | null;
9
21
  readonly name: string;
10
22
  /** 1-based line of the test declaration, when the runner emits it. */
11
23
  readonly line: number | null;
24
+ /** `failed` when the case has a `<failure>` or an `<error>`, `skipped` when it has `<skipped>`, otherwise `passed`. */
12
25
  readonly status: 'passed' | 'failed' | 'skipped';
26
+ /** The failure's text (else its `message`, else `failed`), or null when the case did not fail. */
13
27
  readonly failure: string | null;
28
+ /** The case's `<system-out>`, when it has one. */
29
+ readonly stdout?: string;
30
+ /** The case's `<system-err>`, when it has one. */
31
+ readonly stderr?: string;
32
+ /** The case's `<properties>`, then its suite's: names a runner chose to record. */
33
+ readonly properties?: Readonly<Record<string, string>>;
14
34
  };
15
- /** Parse the `<testcase>` elements of a JUnit XML document. */
35
+ /** Parses every `<testcase>` of a JUnit XML document, in document order, whatever the nesting of its suites. Returns `[]` for text with none; never throws. */
16
36
  export declare function parseJunit(xml: string): JunitCase[];
17
37
  /**
18
38
  * The test case whose declaration precedes `sourceLine` in the same file and
19
- * is the nearest one (bodies follow declarations).
39
+ * is the nearest one (bodies follow declarations). Files match when equal or when one path ends with the other
40
+ * (after normalising backslashes). With one case in the file, or no line, the first case in the file is used.
41
+ * Null when no case is in the file, or none declared at or before the line.
20
42
  */
21
43
  export declare function matchJunitCase(cases: readonly JunitCase[], sourceFile: string, sourceLine: number | undefined): JunitCase | null;
22
- /** Apply runner outcomes / titles to every test with a known source file. */
44
+ /**
45
+ * Applies runner outcomes and titles to every test of a bundle that has a known source file and a matching case:
46
+ * the test takes the case's name as title, its failure as error (a harness error is kept), and fails if either the
47
+ * runner or the harness failed. The bundle fails if any test does. Tests without a match are returned unchanged;
48
+ * the input is not mutated.
49
+ */
23
50
  export declare function applyJunitOutcomes(bundle: HtmlReportBundle, cases: readonly JunitCase[]): HtmlReportBundle;
@@ -1,7 +1,11 @@
1
1
  /**
2
- * Overlay test-runner outcomes (JUnit XML) onto a report bundle. A harness run
3
- * only sees failures raised inside its own steps; assertions in the test body
4
- * surface through the runner's report instead.
2
+ * @module JUnit XML parsing
3
+ *
4
+ * Reads the `<testcase>` elements of a JUnit XML document without an XML library, and overlays test-runner
5
+ * outcomes (JUnit XML) onto a report bundle. A harness run only sees failures raised inside its own steps;
6
+ * assertions in the test body surface through the runner's report instead. Parsing is tolerant by design
7
+ * (nested suites, self-closing elements, CDATA) and never drops a case silently, because a result that goes
8
+ * missing reads as a pass. `ingest.ts` turns the cases into checks.
5
9
  */
6
10
  const ENTITIES = {
7
11
  '&lt;': '<',
@@ -21,24 +25,49 @@ function attr(tag, name) {
21
25
  return m === null ? null : decodeXml(m[1] ?? '');
22
26
  }
23
27
  function failureText(body) {
24
- const m = /<failure\b([^>]*)(?:\/>|>([\s\S]*?)<\/failure>)/.exec(body);
28
+ // `<failure>` is an assertion that did not hold, `<error>` an unexpected exception: both are a check that failed.
29
+ const m = /<(failure|error)\b([^>]*)(?:\/>|>([\s\S]*?)<\/\1>)/.exec(body);
25
30
  if (m === null)
26
31
  return null;
27
- const message = attr(m[1] ?? '', 'message');
28
- const inner = (m[2] ?? '').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
32
+ const message = attr(m[2] ?? '', 'message');
33
+ const inner = (m[3] ?? '').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
29
34
  const text = decodeXml(inner).trim();
30
35
  return text.length > 0 ? text : (message ?? 'failed');
31
36
  }
32
- function parseCase(tag, inner, suiteFile) {
37
+ function streamText(body, name) {
38
+ const found = new RegExp(`<${name}\\b[^>]*>([\\s\\S]*?)</${name}>`).exec(body);
39
+ if (!found)
40
+ return undefined;
41
+ const text = decodeXml((found[1] ?? '').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')).trim();
42
+ return text.length ? text : undefined;
43
+ }
44
+ function propertiesOf(body) {
45
+ const out = {};
46
+ for (const match of body.matchAll(/<property\b([^>]*?)\/?>/g)) {
47
+ const name = attr(match[1] ?? '', 'name');
48
+ const value = attr(match[1] ?? '', 'value');
49
+ if (name)
50
+ out[name] = value ?? '';
51
+ }
52
+ return out;
53
+ }
54
+ function parseCase(tag, inner, suiteFile, suiteName, suiteProperties = {}) {
33
55
  const lineRaw = attr(tag, 'line');
34
56
  const line = lineRaw === null ? null : Number(lineRaw);
35
57
  const failure = failureText(inner);
58
+ const time = Number(attr(tag, 'time'));
36
59
  return {
37
60
  file: attr(tag, 'file') ?? suiteFile,
61
+ classname: attr(tag, 'classname') ?? '',
62
+ suite: suiteName,
63
+ time: attr(tag, 'time') !== null && Number.isFinite(time) ? time : null,
38
64
  name: attr(tag, 'name') ?? '',
39
65
  line: line !== null && Number.isFinite(line) ? line : null,
40
66
  status: caseStatus(failure, inner),
41
67
  failure,
68
+ ...(streamText(inner, 'system-out') ? { stdout: streamText(inner, 'system-out') } : {}),
69
+ ...(streamText(inner, 'system-err') ? { stderr: streamText(inner, 'system-err') } : {}),
70
+ ...(Object.keys({ ...suiteProperties, ...propertiesOf(inner) }).length ? { properties: { ...suiteProperties, ...propertiesOf(inner) } } : {}),
42
71
  };
43
72
  }
44
73
  function caseStatus(failure, inner) {
@@ -48,22 +77,52 @@ function caseStatus(failure, inner) {
48
77
  return 'skipped';
49
78
  return 'passed';
50
79
  }
51
- function parseSuite(tag, body, out) {
52
- const suiteFile = attr(tag, 'file') ?? '';
53
- const caseRe = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
54
- for (let c = caseRe.exec(body); c; c = caseRe.exec(body)) {
55
- out.push(parseCase(c[1] ?? '', c[2] ?? '', suiteFile));
56
- }
57
- }
58
- /** Parse the `<testcase>` elements of a JUnit XML document. */
59
- export function parseJunit(xml) {
80
+ /**
81
+ * Every `<testcase>` of the document, whatever the nesting of its suites: a suite inside a suite, a case after
82
+ * an inner suite, an empty self-closing suite. Each case is read with the suite it is in, and that suite's own
83
+ * `<properties>`. A case is never skipped silently, because a result that goes missing reads as a pass.
84
+ */
85
+ function allCases(xml) {
60
86
  const cases = [];
61
- const suiteRe = /<testsuite\b([^>]*)>([\s\S]*?)<\/testsuite>/g;
62
- for (let suite = suiteRe.exec(xml); suite; suite = suiteRe.exec(xml)) {
63
- parseSuite(suite[1] ?? '', suite[2] ?? '', cases);
87
+ const stack = [];
88
+ const tags = /<(\/?)(testsuite|testcase|properties)\b([^>]*?)(\/?)>/g;
89
+ for (let tag = tags.exec(xml); tag; tag = tags.exec(xml)) {
90
+ const [, closing, name, attributes = '', selfClosing] = tag;
91
+ if (name === 'testsuite') {
92
+ if (closing) {
93
+ stack.pop();
94
+ continue;
95
+ }
96
+ const suite = { file: attr(attributes, 'file') ?? '', name: attr(attributes, 'name') ?? '', properties: {} };
97
+ if (!selfClosing)
98
+ stack.push(suite);
99
+ }
100
+ else if (name === 'testcase' && !closing) {
101
+ let inner = '';
102
+ if (!selfClosing) {
103
+ const end = xml.indexOf('</testcase>', tags.lastIndex);
104
+ if (end < 0)
105
+ continue;
106
+ inner = xml.slice(tags.lastIndex, end);
107
+ tags.lastIndex = end + '</testcase>'.length;
108
+ }
109
+ const suite = stack.at(-1);
110
+ cases.push(parseCase(attributes, inner, suite?.file ?? '', suite?.name ?? '', Object.assign({}, ...stack.map(item => item.properties))));
111
+ }
112
+ else if (name === 'properties' && !closing && !selfClosing && stack.length) {
113
+ const end = xml.indexOf('</properties>', tags.lastIndex);
114
+ if (end < 0)
115
+ continue;
116
+ Object.assign(stack.at(-1).properties, propertiesOf(xml.slice(tags.lastIndex, end)));
117
+ tags.lastIndex = end + '</properties>'.length;
118
+ }
64
119
  }
65
120
  return cases;
66
121
  }
122
+ /** Parses every `<testcase>` of a JUnit XML document, in document order, whatever the nesting of its suites. Returns `[]` for text with none; never throws. */
123
+ export function parseJunit(xml) {
124
+ return allCases(xml);
125
+ }
67
126
  function sameFile(a, b) {
68
127
  const na = a.replace(/\\/g, '/');
69
128
  const nb = b.replace(/\\/g, '/');
@@ -71,7 +130,9 @@ function sameFile(a, b) {
71
130
  }
72
131
  /**
73
132
  * The test case whose declaration precedes `sourceLine` in the same file and
74
- * is the nearest one (bodies follow declarations).
133
+ * is the nearest one (bodies follow declarations). Files match when equal or when one path ends with the other
134
+ * (after normalising backslashes). With one case in the file, or no line, the first case in the file is used.
135
+ * Null when no case is in the file, or none declared at or before the line.
75
136
  */
76
137
  export function matchJunitCase(cases, sourceFile, sourceLine) {
77
138
  const inFile = cases.filter((c) => sameFile(c.file, sourceFile));
@@ -105,7 +166,12 @@ function overlayTest(test, c) {
105
166
  },
106
167
  };
107
168
  }
108
- /** Apply runner outcomes / titles to every test with a known source file. */
169
+ /**
170
+ * Applies runner outcomes and titles to every test of a bundle that has a known source file and a matching case:
171
+ * the test takes the case's name as title, its failure as error (a harness error is kept), and fails if either the
172
+ * runner or the harness failed. The bundle fails if any test does. Tests without a match are returned unchanged;
173
+ * the input is not mutated.
174
+ */
109
175
  export function applyJunitOutcomes(bundle, cases) {
110
176
  const tests = bundle.tests.map((test) => {
111
177
  const { sourceFile, sourceLine } = test.report.meta;
@@ -1,16 +1,36 @@
1
1
  import type { FlatFrame } from './frames.js';
2
2
  import type { Route } from './hash.js';
3
3
  import type { ReportData } from './types.js';
4
+ /** What {@link onReportKeyDown} may ask the host to do, and what it reads from it. */
4
5
  export type KeyNavHandlers = {
6
+ /** Opens or closes the shortcut help. */
5
7
  toggleHelp: () => void;
8
+ /** Whether the help is open. */
6
9
  showHelp: () => boolean;
10
+ /** Whether the command palette is open; while it is, no other shortcut acts. */
7
11
  showPalette: () => boolean;
8
12
  togglePalette: () => void;
13
+ /** Stops frame playback. */
9
14
  stopPlay: () => void;
15
+ /** Goes to a route (writes the URL hash). */
10
16
  navigate: (route: Route) => void;
17
+ /** Selects a frame, optionally of a given client (`node`). */
11
18
  goFrame: (frame: FlatFrame | null, node?: string) => void;
12
19
  togglePlay: () => void;
20
+ /** Sets a URL param on the current route; the empty string removes it. */
13
21
  setParam: (key: string, value: string) => void;
22
+ /** The current route. */
14
23
  route: () => Route;
15
24
  };
25
+ /**
26
+ * Handles one `keydown` for the viewer. Keys: Cmd/Ctrl+K palette; `?` help; Escape closes help or the palette,
27
+ * else stops playback and returns to the list. On a test page: Left/Right/Home/End scrub frames, Space plays,
28
+ * Up/Down jump between steps, `n`/`N` between failures, `[`/`]` to the previous/next test (keeping the list's
29
+ * params), `1`-`9` pick a client, and, when the frame has a UI snapshot, `m` single/multi view, `a` aside, `p`
30
+ * press targets, `f` cycles zoom (fit, 50, 75, 100).
31
+ *
32
+ * Cmd/Ctrl+K and Escape-closing-the-palette work anywhere. Every other key is ignored while the palette is open and
33
+ * while focus is in an input, textarea, select, button, link or editable element; the test-page keys also need the
34
+ * route to be a test that is in `data`.
35
+ */
16
36
  export declare function onReportKeyDown(e: KeyboardEvent, data: ReportData, h: KeyNavHandlers): void;
@@ -85,6 +85,17 @@ function viewKeys(e, ctx) {
85
85
  ctx.h.goFrame(ctx.sel, ids[n - 1] ?? '');
86
86
  }
87
87
  }
88
+ /**
89
+ * Handles one `keydown` for the viewer. Keys: Cmd/Ctrl+K palette; `?` help; Escape closes help or the palette,
90
+ * else stops playback and returns to the list. On a test page: Left/Right/Home/End scrub frames, Space plays,
91
+ * Up/Down jump between steps, `n`/`N` between failures, `[`/`]` to the previous/next test (keeping the list's
92
+ * params), `1`-`9` pick a client, and, when the frame has a UI snapshot, `m` single/multi view, `a` aside, `p`
93
+ * press targets, `f` cycles zoom (fit, 50, 75, 100).
94
+ *
95
+ * Cmd/Ctrl+K and Escape-closing-the-palette work anywhere. Every other key is ignored while the palette is open and
96
+ * while focus is in an input, textarea, select, button, link or editable element; the test-page keys also need the
97
+ * route to be a test that is in `data`.
98
+ */
88
99
  export function onReportKeyDown(e, data, h) {
89
100
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
90
101
  e.preventDefault();
@@ -1,6 +1,9 @@
1
1
  /**
2
- * Object-store key layout of the shared reports store. Historical recording addresses use
3
- * the full commit sha. Immutable report layers combine across those addresses:
2
+ * @module Commit-keyed object-store key layout
3
+ *
4
+ * Builds and validates the keys of the commit-addressed layout of the shared reports store. Historical recording
5
+ * addresses use the full commit sha. Immutable report layers (`layers/…`, see `docs/report-contract.md`) combine
6
+ * across those addresses:
4
7
  *
5
8
  * index/<13-digit inverted start ms>-<sha>.json one per published commit;
6
9
  * sorted keys = newest first
@@ -15,38 +18,53 @@
15
18
  * A source is one report bucket of one package (`libs-chess--part-2`).
16
19
  * Writers put a source's tests first and its `sources/` record last, so a
17
20
  * reader that sees the record can read every test it names.
21
+ *
22
+ * Every builder validates its inputs (a full 40-hex sha, path-safe segments) and throws on a bad one, so a key
23
+ * made here can never climb out of its prefix.
18
24
  */
19
25
  /**
20
26
  * Where the reports live in their bucket. Every reader and writer scopes its
21
27
  * store to it, so the keys below are relative to it.
22
28
  */
23
29
  export declare const REPORTS_ROOT_PREFIX = "spec-reports";
30
+ /** The prefix of the per-commit catalog entries, `index/`. */
24
31
  export declare const COMMITS_INDEX_PREFIX = "index/";
32
+ /** Whether the value is a full 40-digit lowercase hex commit sha (an abbreviated sha is not). */
25
33
  export declare function isCommitSha(value: string): boolean;
26
34
  /** One path segment the harness could have written (never `.` or `..`). */
27
35
  export declare function isKeySegment(value: string): boolean;
28
36
  /** A frame file relative to its test (`frames/f-0001.state.json`). */
29
37
  export declare function isFramePath(value: string): boolean;
30
- /** `libs/chess` + `part-2` → `libs-chess--part-2`. */
38
+ /** The id of a source (one report bucket of one package): `libs/chess` + `part-2` → `libs-chess--part-2`. Throws `not a report source` when the result is not a safe key segment. */
31
39
  export declare function sourceId(packageDir: string, bucket: string): string;
40
+ /** The catalog key of a commit, `index/<13-digit inverted start ms>-<sha>.json`: the time is inverted so a listing is newest first. Negative times count as 0. Throws for a sha that is not 40 hex digits. */
32
41
  export declare function commitIndexKey(sha: string, startedAtMs: number): string;
33
42
  /** The sha an index key names, or null for anything else. */
34
43
  export declare function shaOfIndexKey(key: string): string | null;
44
+ /** The folder of everything recorded for a commit, `commits/<sha>/`. Throws for a sha that is not 40 hex digits. */
35
45
  export declare function commitPrefix(sha: string): string;
46
+ /** The key of the commit's run record, `commits/<sha>/commit.json`. */
36
47
  export declare function commitKey(sha: string): string;
48
+ /** The key of the stylesheet the commit's snapshots render with, `commits/<sha>/frame.css`. */
37
49
  export declare function frameCssKey(sha: string): string;
50
+ /** The folder of a commit's source records, `commits/<sha>/sources/`. */
38
51
  export declare function sourcesPrefix(sha: string): string;
52
+ /** The key of one source's record, `commits/<sha>/sources/<source>.json`; written last, after the source's tests. Throws `not a source` for an unsafe name. */
39
53
  export declare function sourceKey(sha: string, source: string): string;
40
54
  /** The source a `sources/` key names, or null for anything else. */
41
55
  export declare function sourceOfKey(sha: string, key: string): string | null;
56
+ /** The folder of one recorded test, `commits/<sha>/tests/<source>/<testId>/`. Throws for an unsafe source or test id. */
42
57
  export declare function remoteTestPrefix(sha: string, source: string, testId: string): string;
58
+ /** The key of a recorded test's report, `<test folder>/report.json`. */
43
59
  export declare function remoteTestReportKey(sha: string, source: string, testId: string): string;
60
+ /** The key of a file a recorded test captured (a frame, a snapshot), inside its folder. A leading `/` is dropped; throws `not a frame file` for a path with an unsafe segment (`..`, empty or odd characters). */
44
61
  export declare function remoteTestFileKey(sha: string, source: string, testId: string, file: string): string;
45
62
  /**
46
63
  * The id a viewer shows for a test of a source (`libs-chess--part-2/monkey`):
47
64
  * unique across the commit, since two packages may both name a test `monkey`.
48
65
  */
49
66
  export declare function viewerTestId(source: string, testId: string): string;
67
+ /** The inverse of {@link viewerTestId}: splits at the first `/`; null when either side is not a safe key segment. */
50
68
  export declare function splitViewerTestId(id: string): {
51
69
  readonly source: string;
52
70
  readonly testId: string;
@@ -1,6 +1,9 @@
1
1
  /**
2
- * Object-store key layout of the shared reports store. Historical recording addresses use
3
- * the full commit sha. Immutable report layers combine across those addresses:
2
+ * @module Commit-keyed object-store key layout
3
+ *
4
+ * Builds and validates the keys of the commit-addressed layout of the shared reports store. Historical recording
5
+ * addresses use the full commit sha. Immutable report layers (`layers/…`, see `docs/report-contract.md`) combine
6
+ * across those addresses:
4
7
  *
5
8
  * index/<13-digit inverted start ms>-<sha>.json one per published commit;
6
9
  * sorted keys = newest first
@@ -15,6 +18,9 @@
15
18
  * A source is one report bucket of one package (`libs-chess--part-2`).
16
19
  * Writers put a source's tests first and its `sources/` record last, so a
17
20
  * reader that sees the record can read every test it names.
21
+ *
22
+ * Every builder validates its inputs (a full 40-hex sha, path-safe segments) and throws on a bad one, so a key
23
+ * made here can never climb out of its prefix.
18
24
  */
19
25
  /**
20
26
  * Where the reports live in their bucket. Every reader and writer scopes its
@@ -26,7 +32,9 @@ const SHA = /^[0-9a-f]{40}$/;
26
32
  const SEGMENT = /^[a-zA-Z0-9._-]+$/;
27
33
  /** Inverted start times sort newest first while there are 13-digit ms. */
28
34
  const INVERT_FROM = 9_999_999_999_999;
35
+ /** The prefix of the per-commit catalog entries, `index/`. */
29
36
  export const COMMITS_INDEX_PREFIX = 'index/';
37
+ /** Whether the value is a full 40-digit lowercase hex commit sha (an abbreviated sha is not). */
30
38
  export function isCommitSha(value) {
31
39
  return SHA.test(value);
32
40
  }
@@ -44,13 +52,14 @@ function assertSha(sha) {
44
52
  throw new Error(`report key needs a full 40-hex commit sha, got "${sha}"`);
45
53
  }
46
54
  }
47
- /** `libs/chess` + `part-2` → `libs-chess--part-2`. */
55
+ /** The id of a source (one report bucket of one package): `libs/chess` + `part-2` → `libs-chess--part-2`. Throws `not a report source` when the result is not a safe key segment. */
48
56
  export function sourceId(packageDir, bucket) {
49
57
  const id = `${packageDir.replaceAll('/', '-')}--${bucket}`;
50
58
  if (!isKeySegment(id))
51
59
  throw new Error(`not a report source: "${id}"`);
52
60
  return id;
53
61
  }
62
+ /** The catalog key of a commit, `index/<13-digit inverted start ms>-<sha>.json`: the time is inverted so a listing is newest first. Negative times count as 0. Throws for a sha that is not 40 hex digits. */
54
63
  export function commitIndexKey(sha, startedAtMs) {
55
64
  assertSha(sha);
56
65
  const inverted = String(INVERT_FROM - Math.max(0, Math.floor(startedAtMs)));
@@ -61,19 +70,24 @@ export function shaOfIndexKey(key) {
61
70
  const m = /^index\/\d{13}-([0-9a-f]{40})\.json$/.exec(key);
62
71
  return m === null ? null : (m[1] ?? null);
63
72
  }
73
+ /** The folder of everything recorded for a commit, `commits/<sha>/`. Throws for a sha that is not 40 hex digits. */
64
74
  export function commitPrefix(sha) {
65
75
  assertSha(sha);
66
76
  return `commits/${sha}/`;
67
77
  }
78
+ /** The key of the commit's run record, `commits/<sha>/commit.json`. */
68
79
  export function commitKey(sha) {
69
80
  return `${commitPrefix(sha)}commit.json`;
70
81
  }
82
+ /** The key of the stylesheet the commit's snapshots render with, `commits/<sha>/frame.css`. */
71
83
  export function frameCssKey(sha) {
72
84
  return `${commitPrefix(sha)}frame.css`;
73
85
  }
86
+ /** The folder of a commit's source records, `commits/<sha>/sources/`. */
74
87
  export function sourcesPrefix(sha) {
75
88
  return `${commitPrefix(sha)}sources/`;
76
89
  }
90
+ /** The key of one source's record, `commits/<sha>/sources/<source>.json`; written last, after the source's tests. Throws `not a source` for an unsafe name. */
77
91
  export function sourceKey(sha, source) {
78
92
  if (!isKeySegment(source))
79
93
  throw new Error(`not a source: "${source}"`);
@@ -87,6 +101,7 @@ export function sourceOfKey(sha, key) {
87
101
  const source = key.slice(head.length, -'.json'.length);
88
102
  return isKeySegment(source) ? source : null;
89
103
  }
104
+ /** The folder of one recorded test, `commits/<sha>/tests/<source>/<testId>/`. Throws for an unsafe source or test id. */
90
105
  export function remoteTestPrefix(sha, source, testId) {
91
106
  if (!isKeySegment(source))
92
107
  throw new Error(`not a source: "${source}"`);
@@ -94,9 +109,11 @@ export function remoteTestPrefix(sha, source, testId) {
94
109
  throw new Error(`not a test id: "${testId}"`);
95
110
  return `${commitPrefix(sha)}tests/${source}/${testId}/`;
96
111
  }
112
+ /** The key of a recorded test's report, `<test folder>/report.json`. */
97
113
  export function remoteTestReportKey(sha, source, testId) {
98
114
  return `${remoteTestPrefix(sha, source, testId)}report.json`;
99
115
  }
116
+ /** The key of a file a recorded test captured (a frame, a snapshot), inside its folder. A leading `/` is dropped; throws `not a frame file` for a path with an unsafe segment (`..`, empty or odd characters). */
100
117
  export function remoteTestFileKey(sha, source, testId, file) {
101
118
  const rel = file.replace(/^\/+/, '');
102
119
  if (!isFramePath(rel))
@@ -110,6 +127,7 @@ export function remoteTestFileKey(sha, source, testId, file) {
110
127
  export function viewerTestId(source, testId) {
111
128
  return `${source}/${testId}`;
112
129
  }
130
+ /** The inverse of {@link viewerTestId}: splits at the first `/`; null when either side is not a safe key segment. */
113
131
  export function splitViewerTestId(id) {
114
132
  const slash = id.indexOf('/');
115
133
  if (slash < 0)
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @module Environment axis of a layer
3
+ *
4
+ * Owns how a layer's `environment` (the client that was checked: `ios`, `desktop-web`, ...) is normalised to a stable
5
+ * slug, read from either the `environment` field or an `environment` parameter, and shown to people. The environment
6
+ * is part of the slot a check fills, so two spellings of one client must normalise to one id or their results would
7
+ * never replace each other; an unrecognised name is kept as its own slug rather than rejected.
8
+ *
9
+ * @see docs/report-contract.md
10
+ */
11
+ import type { LayerParameters, LayerSummary } from './layer-types.js';
12
+ /**
13
+ * The environments Atlas names and labels out of the box. Any other recorded name is kept, so a new
14
+ * client layers beside these instead of being dropped or renamed away.
15
+ */
16
+ export declare const REPORT_ENVIRONMENTS: readonly ["desktop-web", "mobile-web", "ios", "android"];
17
+ /**
18
+ * Turns any spelling of a client name into a stable lowercase slug, or `null` when none was given.
19
+ *
20
+ * Trims, lowercases, turns whitespace and underscores into `-`, drops every other character outside `a-z0-9-`,
21
+ * collapses repeated `-`, then maps known aliases (`iPhone` and `iPad` to `ios`, `mobile browser` to `mobile-web`,
22
+ * `desktop` to `desktop-web`). A name with no alias is returned as its slug. `null`, `undefined` and a value that
23
+ * slugs to nothing return `null`, which means "the layer did not say", so older layers still compose.
24
+ *
25
+ * @example
26
+ * normalizeEnvironment('Desktop Web'); // 'desktop-web'
27
+ * normalizeEnvironment('iPhone'); // 'ios'
28
+ * normalizeEnvironment('Smart TV'); // 'smart-tv'
29
+ * normalizeEnvironment(' '); // null
30
+ */
31
+ export declare function normalizeEnvironment(value: string | null | undefined): string | null;
32
+ /**
33
+ * Splits a layer into the two slot axes it fills: its normalised environment and its remaining parameters.
34
+ *
35
+ * The environment is the layer's `environment` field, or when that is `null` or absent the `environment` parameter
36
+ * (an empty-string field does not fall back to the parameter). The `environment` parameter is always removed from
37
+ * the returned `parameters`, so it is never both an environment and a free choice that splits the slot twice.
38
+ */
39
+ export declare function layerPlace(summary: Pick<LayerSummary, 'environment' | 'parameters'>): {
40
+ environment: string | null;
41
+ parameters: LayerParameters;
42
+ };
43
+ /**
44
+ * The human label of an environment id: `Desktop web`, `Mobile web`, `iOS`, `Android` for the known ids,
45
+ * `Unspecified` for a missing one, and otherwise the id with its first word capitalised and `-` shown as a space
46
+ * (`smart-tv` reads `Smart tv`).
47
+ */
48
+ export declare function environmentLabel(id: string | null | undefined): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The environments Atlas names and labels out of the box. Any other recorded name is kept, so a new
3
+ * client layers beside these instead of being dropped or renamed away.
4
+ */
5
+ export const REPORT_ENVIRONMENTS = ['desktop-web', 'mobile-web', 'ios', 'android'];
6
+ const ALIASES = {
7
+ desktop: 'desktop-web',
8
+ 'desktop-web': 'desktop-web',
9
+ 'desktop-browser': 'desktop-web',
10
+ 'web-desktop': 'desktop-web',
11
+ 'mobile-web': 'mobile-web',
12
+ 'mobile-browser': 'mobile-web',
13
+ 'web-mobile': 'mobile-web',
14
+ ios: 'ios',
15
+ iphone: 'ios',
16
+ ipad: 'ios',
17
+ android: 'android',
18
+ };
19
+ /**
20
+ * Turns any spelling of a client name into a stable lowercase slug, or `null` when none was given.
21
+ *
22
+ * Trims, lowercases, turns whitespace and underscores into `-`, drops every other character outside `a-z0-9-`,
23
+ * collapses repeated `-`, then maps known aliases (`iPhone` and `iPad` to `ios`, `mobile browser` to `mobile-web`,
24
+ * `desktop` to `desktop-web`). A name with no alias is returned as its slug. `null`, `undefined` and a value that
25
+ * slugs to nothing return `null`, which means "the layer did not say", so older layers still compose.
26
+ *
27
+ * @example
28
+ * normalizeEnvironment('Desktop Web'); // 'desktop-web'
29
+ * normalizeEnvironment('iPhone'); // 'ios'
30
+ * normalizeEnvironment('Smart TV'); // 'smart-tv'
31
+ * normalizeEnvironment(' '); // null
32
+ */
33
+ export function normalizeEnvironment(value) {
34
+ if (value == null)
35
+ return null;
36
+ const slug = value.trim().toLowerCase().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
37
+ if (!slug)
38
+ return null;
39
+ return ALIASES[slug] ?? slug;
40
+ }
41
+ /**
42
+ * Splits a layer into the two slot axes it fills: its normalised environment and its remaining parameters.
43
+ *
44
+ * The environment is the layer's `environment` field, or when that is `null` or absent the `environment` parameter
45
+ * (an empty-string field does not fall back to the parameter). The `environment` parameter is always removed from
46
+ * the returned `parameters`, so it is never both an environment and a free choice that splits the slot twice.
47
+ */
48
+ export function layerPlace(summary) {
49
+ const { environment: named, ...rest } = summary.parameters;
50
+ return { environment: normalizeEnvironment(summary.environment ?? named), parameters: rest };
51
+ }
52
+ const LABELS = {
53
+ 'desktop-web': 'Desktop web',
54
+ 'mobile-web': 'Mobile web',
55
+ ios: 'iOS',
56
+ android: 'Android',
57
+ };
58
+ /**
59
+ * The human label of an environment id: `Desktop web`, `Mobile web`, `iOS`, `Android` for the known ids,
60
+ * `Unspecified` for a missing one, and otherwise the id with its first word capitalised and `-` shown as a space
61
+ * (`smart-tv` reads `Smart tv`).
62
+ */
63
+ export function environmentLabel(id) {
64
+ if (!id)
65
+ return 'Unspecified';
66
+ const known = LABELS[id];
67
+ if (known)
68
+ return known;
69
+ const [first, ...rest] = id.split('-');
70
+ return [first.charAt(0).toUpperCase() + first.slice(1), ...rest].join(' ');
71
+ }
@@ -1,5 +1,32 @@
1
+ /**
2
+ * @module Layer catalog of one backend
3
+ *
4
+ * Lists what one backend holds as catalog entries (immutable layers under `layers/index/`, and commit-keyed
5
+ * recordings written before layers existed) and turns one entry into the layer summaries it stands for. It only
6
+ * reads: opening Atlas never rewrites a store. Whether a backend can be read at all, and how entries from several
7
+ * backends combine, is decided by the callers (`compose.ts`, `layers-route.ts`).
8
+ *
9
+ * @see docs/report-contract.md
10
+ */
1
11
  import type { ObjectStore } from './object-store.js';
2
12
  import type { LayerSummary } from './layer-types.js';
3
- /** Read existing recordings in place; opening Atlas never rewrites the evidence store. */
13
+ /**
14
+ * Lists one backend's catalog entries as opaque strings to pass to `catalogLayers`.
15
+ *
16
+ * An entry is `layer:<key>` for each key under `layers/index/` (listed newest first, in the store's own key order),
17
+ * followed by `commit:<sha>` for every 40-hex commit named by a key under `versions/` or `index/` (a store written
18
+ * before layers existed, sorted by sha). Only lists keys; reads no layer. A store that cannot be listed throws.
19
+ * Existing recordings are read in place and never rewritten.
20
+ */
4
21
  export declare function catalogEntries(store: ObjectStore): Promise<readonly string[]>;
22
+ /**
23
+ * Reads the layer summaries one catalog entry from `catalogEntries` stands for.
24
+ *
25
+ * A `layer:` entry yields exactly one summary (the layer the catalog entry names, without its bundle) and throws
26
+ * when the entry is not JSON, names no layer, the layer is missing, or the layer cannot be parsed, so the caller can
27
+ * report it as a notice instead of counting it as a pass. A `commit:` entry yields the older commit-keyed
28
+ * recordings of that sha: one summary per stored part (`part-<sha>-<id>`) and one per stored source
29
+ * (`source-<sha>-<id>`); a part whose checks a source already carries, and that has no parameters, is dropped so the
30
+ * same evidence is not counted twice. Those ids are mutable addresses, not content digests.
31
+ */
5
32
  export declare function catalogLayers(store: ObjectStore, entry: string): Promise<readonly LayerSummary[]>;
@@ -7,12 +7,29 @@ import { readStoredSource } from './source-content.js';
7
7
  import { sourceParts } from './source-part.js';
8
8
  import { LAYER_INDEX, readLayer, layerSummary } from './layer-store.js';
9
9
  import { eachLimited } from './completion-order.js';
10
- /** Read existing recordings in place; opening Atlas never rewrites the evidence store. */
10
+ /**
11
+ * Lists one backend's catalog entries as opaque strings to pass to `catalogLayers`.
12
+ *
13
+ * An entry is `layer:<key>` for each key under `layers/index/` (listed newest first, in the store's own key order),
14
+ * followed by `commit:<sha>` for every 40-hex commit named by a key under `versions/` or `index/` (a store written
15
+ * before layers existed, sorted by sha). Only lists keys; reads no layer. A store that cannot be listed throws.
16
+ * Existing recordings are read in place and never rewritten.
17
+ */
11
18
  export async function catalogEntries(store) {
12
19
  const [layers, versions, commits] = await Promise.all([store.listKeys(LAYER_INDEX), store.listKeys('versions/'), store.listKeys('index/')]);
13
20
  const shas = new Set([...versions, ...commits].flatMap(key => /([0-9a-f]{40})\.json$/.exec(key)?.[1] ?? []));
14
21
  return [...layers.map(key => `layer:${key}`), ...[...shas].sort().map(sha => `commit:${sha}`)];
15
22
  }
23
+ /**
24
+ * Reads the layer summaries one catalog entry from `catalogEntries` stands for.
25
+ *
26
+ * A `layer:` entry yields exactly one summary (the layer the catalog entry names, without its bundle) and throws
27
+ * when the entry is not JSON, names no layer, the layer is missing, or the layer cannot be parsed, so the caller can
28
+ * report it as a notice instead of counting it as a pass. A `commit:` entry yields the older commit-keyed
29
+ * recordings of that sha: one summary per stored part (`part-<sha>-<id>`) and one per stored source
30
+ * (`source-<sha>-<id>`); a part whose checks a source already carries, and that has no parameters, is dropped so the
31
+ * same evidence is not counted twice. Those ids are mutable addresses, not content digests.
32
+ */
16
33
  export async function catalogLayers(store, entry) {
17
34
  if (entry.startsWith('layer:')) {
18
35
  const raw = await readUtf8(store, entry.slice(6));
@@ -31,7 +48,7 @@ export async function catalogLayers(store, entry) {
31
48
  const stored = await readStoredSource(store, sha, sourceId);
32
49
  if (!stored)
33
50
  return;
34
- layers.push({ id: `source-${sha}-${sourceId}`, producer: sourceId, recordedAtMs: Math.max(commit?.startedAtMs ?? 0, stored.record.updatedAtMs), sha, branch: commit?.branch ?? null, runUrl: commit?.runUrl ?? null, parameters: {}, specs, parts: sourceParts(sha, stored.record), source: stored.record, recording: { type: 'commit', sha, source: sourceId } });
51
+ layers.push({ ...(stored.record.environment ? { environment: stored.record.environment } : {}), id: `source-${sha}-${sourceId}`, producer: sourceId, recordedAtMs: Math.max(commit?.startedAtMs ?? 0, stored.record.updatedAtMs), sha, branch: commit?.branch ?? null, runUrl: commit?.runUrl ?? null, parameters: stored.record.parameters ?? {}, specs, parts: sourceParts(sha, stored.record), source: stored.record, recording: { type: 'commit', sha, source: sourceId } });
35
52
  });
36
53
  const signature = (part) => JSON.stringify([part.id, part.kind, part.checks]);
37
54
  const sourceEvidence = new Set(layers.filter(layer => layer.source).flatMap(layer => layer.parts.map(signature)));