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