@crvouga/atlas 0.0.0-stage → 0.2.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 (606) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +649 -2
  4. package/dist/bun/src/driver.d.ts +67 -0
  5. package/dist/bun/src/driver.js +90 -0
  6. package/dist/bun/src/index.d.ts +2 -0
  7. package/dist/bun/src/index.js +2 -0
  8. package/dist/core/src/cli.d.ts +1 -0
  9. package/dist/core/src/cli.js +177 -0
  10. package/dist/core/src/config.d.ts +477 -0
  11. package/dist/core/src/config.js +100 -0
  12. package/dist/core/src/events/cloudevents.d.ts +56 -0
  13. package/dist/core/src/events/cloudevents.js +152 -0
  14. package/dist/core/src/graph.d.ts +113 -0
  15. package/dist/core/src/graph.js +221 -0
  16. package/dist/core/src/index.d.ts +17 -0
  17. package/dist/core/src/index.js +17 -0
  18. package/dist/core/src/lint.d.ts +53 -0
  19. package/dist/core/src/lint.js +138 -0
  20. package/dist/core/src/plan.d.ts +109 -0
  21. package/dist/core/src/plan.js +324 -0
  22. package/dist/core/src/privacy.d.ts +8 -0
  23. package/dist/core/src/privacy.js +42 -0
  24. package/dist/core/src/report/formats.d.ts +69 -0
  25. package/dist/core/src/report/formats.js +109 -0
  26. package/dist/core/src/report/markdown.d.ts +8 -0
  27. package/dist/core/src/report/markdown.js +140 -0
  28. package/dist/core/src/run/clients.d.ts +42 -0
  29. package/dist/core/src/run/clients.js +250 -0
  30. package/dist/core/src/run/execute.d.ts +854 -0
  31. package/dist/core/src/run/execute.js +877 -0
  32. package/dist/core/src/run/frames.d.ts +16 -0
  33. package/dist/core/src/run/frames.js +52 -0
  34. package/dist/core/src/run/http.d.ts +41 -0
  35. package/dist/core/src/run/http.js +71 -0
  36. package/dist/core/src/run/types.d.ts +248 -0
  37. package/dist/core/src/run/types.js +39 -0
  38. package/dist/core/src/signals.d.ts +110 -0
  39. package/dist/core/src/signals.js +121 -0
  40. package/dist/core/src/spec/compose.d.ts +28 -0
  41. package/dist/core/src/spec/compose.js +71 -0
  42. package/dist/core/src/spec/index.d.ts +5 -0
  43. package/dist/core/src/spec/index.js +5 -0
  44. package/dist/core/src/spec/load.d.ts +8 -0
  45. package/dist/core/src/spec/load.js +46 -0
  46. package/dist/core/src/spec/parse.d.ts +5 -0
  47. package/dist/core/src/spec/parse.js +32 -0
  48. package/dist/core/src/spec/scxml.d.ts +19 -0
  49. package/dist/core/src/spec/scxml.js +239 -0
  50. package/dist/core/src/spec/types.d.ts +96 -0
  51. package/dist/core/src/spec/types.js +25 -0
  52. package/dist/detox/src/index.d.ts +75 -0
  53. package/dist/detox/src/index.js +110 -0
  54. package/dist/playwright/src/driver.d.ts +44 -0
  55. package/dist/playwright/src/driver.js +82 -0
  56. package/dist/playwright/src/index.d.ts +3 -0
  57. package/dist/playwright/src/index.js +3 -0
  58. package/dist/playwright/src/media.d.ts +68 -0
  59. package/dist/playwright/src/media.js +283 -0
  60. package/dist/playwright/src/signals.d.ts +34 -0
  61. package/dist/playwright/src/signals.js +56 -0
  62. package/dist/reports/src/browser-navigation.d.ts +2 -0
  63. package/dist/reports/src/browser-navigation.js +28 -0
  64. package/dist/reports/src/bundle.d.ts +24 -0
  65. package/dist/reports/src/bundle.js +114 -0
  66. package/dist/reports/src/catalog.d.ts +37 -0
  67. package/dist/reports/src/catalog.js +127 -0
  68. package/dist/reports/src/clock.d.ts +14 -0
  69. package/dist/reports/src/clock.js +4 -0
  70. package/dist/reports/src/completion-order.d.ts +10 -0
  71. package/dist/reports/src/completion-order.js +76 -0
  72. package/dist/reports/src/evidence-live.d.ts +40 -0
  73. package/dist/reports/src/evidence-live.js +118 -0
  74. package/dist/reports/src/evidence-types.d.ts +90 -0
  75. package/dist/reports/src/evidence-types.js +15 -0
  76. package/dist/reports/src/evidence-view.d.ts +68 -0
  77. package/dist/reports/src/evidence-view.js +113 -0
  78. package/dist/reports/src/explorer-types.d.ts +56 -0
  79. package/dist/reports/src/explorer-types.js +0 -0
  80. package/dist/reports/src/explorer-versions.d.ts +4 -0
  81. package/dist/reports/src/explorer-versions.js +21 -0
  82. package/dist/reports/src/export-evidence.d.ts +11 -0
  83. package/dist/reports/src/export-evidence.js +94 -0
  84. package/dist/reports/src/fetcher.d.ts +50 -0
  85. package/dist/reports/src/fetcher.js +205 -0
  86. package/dist/reports/src/format-date.d.ts +1 -0
  87. package/dist/reports/src/format-date.js +10 -0
  88. package/dist/reports/src/format.d.ts +16 -0
  89. package/dist/reports/src/format.js +71 -0
  90. package/dist/reports/src/frames.d.ts +46 -0
  91. package/dist/reports/src/frames.js +245 -0
  92. package/dist/reports/src/hash.d.ts +17 -0
  93. package/dist/reports/src/hash.js +43 -0
  94. package/dist/reports/src/html-frame.d.ts +13 -0
  95. package/dist/reports/src/html-frame.js +63 -0
  96. package/dist/reports/src/http.d.ts +3 -0
  97. package/dist/reports/src/http.js +0 -0
  98. package/dist/reports/src/junit.d.ts +23 -0
  99. package/dist/reports/src/junit.js +119 -0
  100. package/dist/reports/src/keyboard.d.ts +16 -0
  101. package/dist/reports/src/keyboard.js +135 -0
  102. package/dist/reports/src/keys.d.ts +53 -0
  103. package/dist/reports/src/keys.js +122 -0
  104. package/dist/reports/src/layer-catalog.d.ts +5 -0
  105. package/dist/reports/src/layer-catalog.js +39 -0
  106. package/dist/reports/src/layer-merge.d.ts +43 -0
  107. package/dist/reports/src/layer-merge.js +154 -0
  108. package/dist/reports/src/layer-source.d.ts +6 -0
  109. package/dist/reports/src/layer-source.js +32 -0
  110. package/dist/reports/src/layer-store.d.ts +10 -0
  111. package/dist/reports/src/layer-store.js +36 -0
  112. package/dist/reports/src/layer-types.d.ts +32 -0
  113. package/dist/reports/src/layer-types.js +0 -0
  114. package/dist/reports/src/layers-route.d.ts +20 -0
  115. package/dist/reports/src/layers-route.js +259 -0
  116. package/dist/reports/src/namespace-guard.d.ts +11 -0
  117. package/dist/reports/src/namespace-guard.js +40 -0
  118. package/dist/reports/src/navigation.d.ts +8 -0
  119. package/dist/reports/src/navigation.js +0 -0
  120. package/dist/reports/src/object-io.d.ts +9 -0
  121. package/dist/reports/src/object-io.js +41 -0
  122. package/dist/reports/src/object-store.d.ts +67 -0
  123. package/dist/reports/src/object-store.js +32 -0
  124. package/dist/reports/src/performance-part.d.ts +9 -0
  125. package/dist/reports/src/performance-part.js +42 -0
  126. package/dist/reports/src/performance.d.ts +13 -0
  127. package/dist/reports/src/performance.js +45 -0
  128. package/dist/reports/src/persist.d.ts +10 -0
  129. package/dist/reports/src/persist.js +38 -0
  130. package/dist/reports/src/phone.d.ts +5 -0
  131. package/dist/reports/src/phone.js +18 -0
  132. package/dist/reports/src/protocol.d.ts +78 -0
  133. package/dist/reports/src/protocol.js +36 -0
  134. package/dist/reports/src/publication.d.ts +4 -0
  135. package/dist/reports/src/publication.js +35 -0
  136. package/dist/reports/src/publish.d.ts +52 -0
  137. package/dist/reports/src/publish.js +189 -0
  138. package/dist/reports/src/read.d.ts +62 -0
  139. package/dist/reports/src/read.js +214 -0
  140. package/dist/reports/src/recording-bundle.d.ts +22 -0
  141. package/dist/reports/src/recording-bundle.js +4 -0
  142. package/dist/reports/src/recording-keys.d.ts +9 -0
  143. package/dist/reports/src/recording-keys.js +50 -0
  144. package/dist/reports/src/remote-report.d.ts +39 -0
  145. package/dist/reports/src/remote-report.js +304 -0
  146. package/dist/reports/src/report-loader.d.ts +22 -0
  147. package/dist/reports/src/report-loader.js +0 -0
  148. package/dist/reports/src/report-parts.d.ts +10 -0
  149. package/dist/reports/src/report-parts.js +112 -0
  150. package/dist/reports/src/report-types.d.ts +194 -0
  151. package/dist/reports/src/report-types.js +29 -0
  152. package/dist/reports/src/reports-explorer-route.d.ts +90 -0
  153. package/dist/reports/src/reports-explorer-route.js +65 -0
  154. package/dist/reports/src/reports-list-route.d.ts +23 -0
  155. package/dist/reports/src/reports-list-route.js +55 -0
  156. package/dist/reports/src/reports-route.d.ts +267 -0
  157. package/dist/reports/src/reports-route.js +204 -0
  158. package/dist/reports/src/reports-store.d.ts +11 -0
  159. package/dist/reports/src/reports-store.js +28 -0
  160. package/dist/reports/src/resilient-cache.d.ts +3 -0
  161. package/dist/reports/src/resilient-cache.js +33 -0
  162. package/dist/reports/src/retention.d.ts +9 -0
  163. package/dist/reports/src/retention.js +16 -0
  164. package/dist/reports/src/scenario.d.ts +15 -0
  165. package/dist/reports/src/scenario.js +35 -0
  166. package/dist/reports/src/snapshot-bundle.d.ts +18 -0
  167. package/dist/reports/src/snapshot-bundle.js +94 -0
  168. package/dist/reports/src/snapshot-surface.d.ts +29 -0
  169. package/dist/reports/src/snapshot-surface.js +339 -0
  170. package/dist/reports/src/source-content.d.ts +30 -0
  171. package/dist/reports/src/source-content.js +63 -0
  172. package/dist/reports/src/source-entries.d.ts +4 -0
  173. package/dist/reports/src/source-entries.js +34 -0
  174. package/dist/reports/src/source-part.d.ts +4 -0
  175. package/dist/reports/src/source-part.js +66 -0
  176. package/dist/reports/src/source-reader.d.ts +6 -0
  177. package/dist/reports/src/source-reader.js +17 -0
  178. package/dist/reports/src/spec-evidence.d.ts +6 -0
  179. package/dist/reports/src/spec-evidence.js +20 -0
  180. package/dist/reports/src/spec-inventory.d.ts +14 -0
  181. package/dist/reports/src/spec-inventory.js +75 -0
  182. package/dist/reports/src/status.d.ts +4 -0
  183. package/dist/reports/src/status.js +8 -0
  184. package/dist/reports/src/store-prefix.d.ts +21 -0
  185. package/dist/reports/src/store-prefix.js +53 -0
  186. package/dist/reports/src/test-catalog-entity.d.ts +3 -0
  187. package/dist/reports/src/test-catalog-entity.js +34 -0
  188. package/dist/reports/src/types.d.ts +5 -0
  189. package/dist/reports/src/types.js +0 -0
  190. package/dist/reports/src/use-report-nav.d.ts +25 -0
  191. package/dist/reports/src/use-report-nav.js +181 -0
  192. package/dist/schema/src/common.d.ts +57 -0
  193. package/dist/schema/src/common.js +32 -0
  194. package/dist/schema/src/index.d.ts +5 -0
  195. package/dist/schema/src/index.js +5 -0
  196. package/dist/schema/src/manifest.d.ts +457 -0
  197. package/dist/schema/src/manifest.js +247 -0
  198. package/dist/schema/src/report-sources.d.ts +68 -0
  199. package/dist/schema/src/report-sources.js +53 -0
  200. package/dist/schema/src/runs-index.d.ts +52 -0
  201. package/dist/schema/src/runs-index.js +24 -0
  202. package/dist/schema/src/spec.d.ts +245 -0
  203. package/dist/schema/src/spec.js +133 -0
  204. package/dist/vitest/src/index.d.ts +22 -0
  205. package/dist/vitest/src/index.js +33 -0
  206. package/dist/webdriver/src/driver.d.ts +69 -0
  207. package/dist/webdriver/src/driver.js +230 -0
  208. package/dist/webdriver/src/index.d.ts +11 -0
  209. package/dist/webdriver/src/index.js +10 -0
  210. package/dist/webdriver/src/protocol.d.ts +60 -0
  211. package/dist/webdriver/src/protocol.js +146 -0
  212. package/package.json +181 -4
  213. package/packages/bun/README.md +69 -0
  214. package/packages/core/README.md +78 -0
  215. package/packages/core/bin/atlas.mjs +8 -0
  216. package/packages/detox/README.md +73 -0
  217. package/packages/playwright/README.md +77 -0
  218. package/packages/reports/README.md +47 -0
  219. package/packages/reports/STORAGE.md +73 -0
  220. package/packages/schema/README.md +23 -0
  221. package/packages/schema/schemas/atlas-change.schema.json +34 -0
  222. package/packages/schema/schemas/atlas.schema.json +32 -0
  223. package/packages/schema/schemas/cloud-event.schema.json +40 -0
  224. package/packages/schema/schemas/journeys.schema.json +17 -0
  225. package/packages/schema/schemas/manifest.schema.json +160 -0
  226. package/packages/schema/schemas/path-record.schema.json +151 -0
  227. package/packages/schema/schemas/report-sources.schema.json +95 -0
  228. package/packages/schema/schemas/run-info.schema.json +176 -0
  229. package/packages/schema/schemas/run-summary.schema.json +206 -0
  230. package/packages/schema/schemas/runs-index.schema.json +23 -0
  231. package/packages/schema/schemas/state-node.schema.json +238 -0
  232. package/packages/schema/schemas/state-record.schema.json +354 -0
  233. package/packages/schema/schemas/timeline-entry.schema.json +124 -0
  234. package/packages/schema/schemas/transition-record.schema.json +125 -0
  235. package/packages/visualizer/README.md +154 -0
  236. package/packages/visualizer/bin/atlas-visualizer.mjs +176 -0
  237. package/packages/visualizer/example/specs/coffee/ordering/checkout.scxml +82 -0
  238. package/packages/visualizer/example/specs/coffee/ordering/journeys.yaml +129 -0
  239. package/packages/visualizer/example/specs/coffee/ordering/known-issues.md +14 -0
  240. package/packages/visualizer/example/specs/coffee/ordering/machine.yaml +158 -0
  241. package/packages/visualizer/example/specs/coffee/ordering/open-questions.md +25 -0
  242. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/chart.mmd +67 -0
  243. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/ctrf.json +245 -0
  244. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/junit.xml +12 -0
  245. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/manifest.json +2003 -0
  246. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/001.vtt +9 -0
  247. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/002.vtt +9 -0
  248. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/003.vtt +9 -0
  249. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/004.vtt +9 -0
  250. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/005.vtt +17 -0
  251. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/006.vtt +9 -0
  252. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/007.vtt +17 -0
  253. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/008.vtt +17 -0
  254. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/009.vtt +13 -0
  255. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/010.vtt +9 -0
  256. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/011.vtt +9 -0
  257. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/012.vtt +9 -0
  258. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/013.vtt +9 -0
  259. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/01.mp4 +0 -0
  260. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/01.poster.webp +0 -0
  261. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/02.mp4 +0 -0
  262. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/02.poster.webp +0 -0
  263. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/03.mp4 +0 -0
  264. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/03.poster.webp +0 -0
  265. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/04.mp4 +0 -0
  266. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/04.poster.webp +0 -0
  267. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/01.png +0 -0
  268. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/01.webp +0 -0
  269. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/02.png +0 -0
  270. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/02.webp +0 -0
  271. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/03.png +0 -0
  272. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/03.webp +0 -0
  273. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/04.png +0 -0
  274. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/04.webp +0 -0
  275. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/05.png +0 -0
  276. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/05.webp +0 -0
  277. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-2/01.png +0 -0
  278. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-2/01.webp +0 -0
  279. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-3/failed-01.png +0 -0
  280. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-3/failed-01.webp +0 -0
  281. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-4/01.png +0 -0
  282. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-4/01.webp +0 -0
  283. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-5/01.png +0 -0
  284. package/packages/visualizer/fixtures/failures/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-5/01.webp +0 -0
  285. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.scxml +82 -0
  286. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/journeys.yaml +129 -0
  287. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/known-issues.md +14 -0
  288. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/machine.yaml +158 -0
  289. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/open-questions.md +25 -0
  290. package/packages/visualizer/fixtures/full/runs/2026-10-02T09-10-00-000Z-fast/chart.mmd +67 -0
  291. package/packages/visualizer/fixtures/full/runs/2026-10-02T09-10-00-000Z-fast/ctrf.json +247 -0
  292. package/packages/visualizer/fixtures/full/runs/2026-10-02T09-10-00-000Z-fast/junit.xml +12 -0
  293. package/packages/visualizer/fixtures/full/runs/2026-10-02T09-10-00-000Z-fast/manifest.json +1875 -0
  294. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/chart.mmd +67 -0
  295. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/ctrf.json +244 -0
  296. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/junit.xml +12 -0
  297. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/manifest.json +1977 -0
  298. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/captions/001.vtt +9 -0
  299. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/captions/002.vtt +9 -0
  300. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/captions/003.vtt +9 -0
  301. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/captions/004.vtt +9 -0
  302. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/captions/005.vtt +17 -0
  303. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/captions/006.vtt +9 -0
  304. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/clips/journey-1/01.mp4 +0 -0
  305. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/clips/journey-1/01.poster.webp +0 -0
  306. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/clips/journey-1/02.mp4 +0 -0
  307. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/clips/journey-1/02.poster.webp +0 -0
  308. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/paths/journey-1/01.png +0 -0
  309. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/paths/journey-1/01.webp +0 -0
  310. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/paths/journey-1/02.png +0 -0
  311. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/paths/journey-1/02.webp +0 -0
  312. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/paths/journey-1/03.png +0 -0
  313. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/paths/journey-1/03.webp +0 -0
  314. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/paths/journey-2/01.png +0 -0
  315. package/packages/visualizer/fixtures/full/runs/2026-10-04T16-20-00-000Z-showcase/media/paths/journey-2/01.webp +0 -0
  316. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/chart.mmd +67 -0
  317. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/ctrf.json +472 -0
  318. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/junit.xml +16 -0
  319. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/manifest.json +3701 -0
  320. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/001.vtt +9 -0
  321. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/002.vtt +9 -0
  322. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/003.vtt +9 -0
  323. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/004.vtt +9 -0
  324. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/005.vtt +9 -0
  325. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/006.vtt +13 -0
  326. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/007.vtt +9 -0
  327. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/008.vtt +13 -0
  328. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/009.vtt +17 -0
  329. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/010.vtt +9 -0
  330. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/011.vtt +13 -0
  331. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/012.vtt +9 -0
  332. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/013.vtt +9 -0
  333. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/014.vtt +9 -0
  334. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/015.vtt +9 -0
  335. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/016.vtt +9 -0
  336. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/017.vtt +9 -0
  337. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/018.vtt +17 -0
  338. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/019.vtt +9 -0
  339. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/020.vtt +9 -0
  340. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/021.vtt +9 -0
  341. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/022.vtt +17 -0
  342. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/023.vtt +17 -0
  343. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/024.vtt +13 -0
  344. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/025.vtt +9 -0
  345. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/026.vtt +9 -0
  346. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/027.vtt +9 -0
  347. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/028.vtt +9 -0
  348. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/01.mp4 +0 -0
  349. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/01.poster.webp +0 -0
  350. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/02.mp4 +0 -0
  351. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/02.poster.webp +0 -0
  352. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/03.mp4 +0 -0
  353. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/03.poster.webp +0 -0
  354. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/04.mp4 +0 -0
  355. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/04.poster.webp +0 -0
  356. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/01.png +0 -0
  357. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/01.webp +0 -0
  358. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/02.png +0 -0
  359. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/02.webp +0 -0
  360. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/03.png +0 -0
  361. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/03.webp +0 -0
  362. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/04.png +0 -0
  363. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/04.webp +0 -0
  364. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/05.png +0 -0
  365. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/05.webp +0 -0
  366. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/06.png +0 -0
  367. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/06.webp +0 -0
  368. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/07.png +0 -0
  369. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/07.webp +0 -0
  370. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/08.png +0 -0
  371. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/08.webp +0 -0
  372. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/09.png +0 -0
  373. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/09.webp +0 -0
  374. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/10.png +0 -0
  375. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/10.webp +0 -0
  376. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/11.png +0 -0
  377. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/11.webp +0 -0
  378. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/12.png +0 -0
  379. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/12.webp +0 -0
  380. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/13.png +0 -0
  381. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/13.webp +0 -0
  382. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/14.png +0 -0
  383. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/14.webp +0 -0
  384. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-2/01.png +0 -0
  385. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-2/01.webp +0 -0
  386. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-3/01.png +0 -0
  387. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-3/01.webp +0 -0
  388. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-4/01.png +0 -0
  389. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-4/01.webp +0 -0
  390. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-5/01.png +0 -0
  391. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-5/01.webp +0 -0
  392. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-6/01.png +0 -0
  393. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-6/01.webp +0 -0
  394. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-7/01.png +0 -0
  395. package/packages/visualizer/fixtures/full/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-7/01.webp +0 -0
  396. package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.scxml +82 -0
  397. package/packages/visualizer/fixtures/full/specs/coffee/ordering/journeys.yaml +129 -0
  398. package/packages/visualizer/fixtures/full/specs/coffee/ordering/known-issues.md +14 -0
  399. package/packages/visualizer/fixtures/full/specs/coffee/ordering/machine.yaml +158 -0
  400. package/packages/visualizer/fixtures/full/specs/coffee/ordering/open-questions.md +25 -0
  401. package/packages/visualizer/fixtures/large/runs/2026-10-06T14-40-00-000Z-fast/ctrf.json +1280 -0
  402. package/packages/visualizer/fixtures/large/runs/2026-10-06T14-40-00-000Z-fast/junit.xml +26 -0
  403. package/packages/visualizer/fixtures/large/runs/2026-10-06T14-40-00-000Z-fast/manifest.json +28393 -0
  404. package/packages/visualizer/fixtures/large/specs/accounts/sign-up/identity-check.machine.yaml +397 -0
  405. package/packages/visualizer/fixtures/large/specs/accounts/sign-up/journeys.yaml +129 -0
  406. package/packages/visualizer/fixtures/large/specs/accounts/sign-up/machine.yaml +830 -0
  407. package/packages/visualizer/fixtures/large/specs/atlas.yaml +65 -0
  408. package/packages/visualizer/fixtures/large/specs/billing/invoices/journeys.yaml +132 -0
  409. package/packages/visualizer/fixtures/large/specs/billing/invoices/machine.yaml +830 -0
  410. package/packages/visualizer/fixtures/large/specs/billing/invoices/refund-handling.scxml +288 -0
  411. package/packages/visualizer/fixtures/large/specs/catalog/browsing/journeys.yaml +126 -0
  412. package/packages/visualizer/fixtures/large/specs/catalog/browsing/machine.yaml +831 -0
  413. package/packages/visualizer/fixtures/large/specs/catalog/browsing/product-comparison.machine.yaml +395 -0
  414. package/packages/visualizer/fixtures/large/specs/orders/checkout/journeys.yaml +120 -0
  415. package/packages/visualizer/fixtures/large/specs/orders/checkout/machine.yaml +724 -0
  416. package/packages/visualizer/fixtures/large/specs/orders/checkout/open-questions.md +16 -0
  417. package/packages/visualizer/fixtures/large/specs/orders/checkout/payment.scxml +288 -0
  418. package/packages/visualizer/fixtures/large/specs/shipping/deliveries/delivery-scheduling.machine.yaml +397 -0
  419. package/packages/visualizer/fixtures/large/specs/shipping/deliveries/journeys.yaml +132 -0
  420. package/packages/visualizer/fixtures/large/specs/shipping/deliveries/known-issues.md +13 -0
  421. package/packages/visualizer/fixtures/large/specs/shipping/deliveries/machine.yaml +830 -0
  422. package/packages/visualizer/fixtures/large/specs/support/tickets/escalation.machine.yaml +395 -0
  423. package/packages/visualizer/fixtures/large/specs/support/tickets/journeys.yaml +123 -0
  424. package/packages/visualizer/fixtures/large/specs/support/tickets/machine.yaml +720 -0
  425. package/packages/visualizer/fixtures/malformed/runs/2026-10-06T10-00-00-000Z-showcase/manifest.json +68 -0
  426. package/packages/visualizer/fixtures/malformed/runs/2026-10-06T11-00-00-000Z-showcase/ctrf.json +50 -0
  427. package/packages/visualizer/fixtures/malformed/runs/2026-10-06T11-00-00-000Z-showcase/junit.xml +6 -0
  428. package/packages/visualizer/fixtures/malformed/runs/2026-10-06T11-00-00-000Z-showcase/manifest.json +1281 -0
  429. package/packages/visualizer/fixtures/malformed/runs/2026-10-06T12-00-00-000Z-showcase/ctrf.json +50 -0
  430. package/packages/visualizer/fixtures/malformed/runs/2026-10-06T12-00-00-000Z-showcase/junit.xml +6 -0
  431. package/packages/visualizer/fixtures/malformed/runs/2026-10-06T12-00-00-000Z-showcase/manifest.json +1232 -0
  432. package/packages/visualizer/fixtures/malformed/runs/2026-10-06T13-00-00-000Z-showcase/manifest.json +1244 -0
  433. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.scxml +82 -0
  434. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/journeys.yaml +134 -0
  435. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/known-issues.md +14 -0
  436. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.scxml +4 -0
  437. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/machine.yaml +156 -0
  438. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/open-questions.md +25 -0
  439. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/chart.mmd +67 -0
  440. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/ctrf.json +149 -0
  441. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/junit.xml +9 -0
  442. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/manifest.json +1718 -0
  443. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/001.vtt +9 -0
  444. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/002.vtt +17 -0
  445. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/003.vtt +17 -0
  446. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/004.vtt +9 -0
  447. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/005.vtt +9 -0
  448. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/01.mp4 +0 -0
  449. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/01.poster.webp +0 -0
  450. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/02.mp4 +0 -0
  451. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/02.poster.webp +0 -0
  452. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/03.mp4 +0 -0
  453. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/03.poster.webp +0 -0
  454. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/01.png +0 -0
  455. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/01.webp +0 -0
  456. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/02.png +0 -0
  457. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/02.webp +0 -0
  458. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/03.png +0 -0
  459. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/03.webp +0 -0
  460. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/05.png +0 -0
  461. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/05.webp +0 -0
  462. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-3/01.png +0 -0
  463. package/packages/visualizer/fixtures/partial/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-3/01.webp +0 -0
  464. package/packages/visualizer/fixtures/partial/runs/2026-10-06T15-05-00-000Z-showcase/media/paths/journey-1/00-start-01.png +0 -0
  465. package/packages/visualizer/fixtures/partial/runs/2026-10-06T15-05-00-000Z-showcase/media/paths/journey-1/00-start-01.webp +0 -0
  466. package/packages/visualizer/fixtures/partial/runs/2026-10-06T15-05-00-000Z-showcase/media/paths/journey-1/02.png +0 -0
  467. package/packages/visualizer/fixtures/partial/runs/2026-10-06T15-05-00-000Z-showcase/media/paths/journey-1/02.webp +0 -0
  468. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.scxml +82 -0
  469. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/journeys.yaml +129 -0
  470. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/known-issues.md +14 -0
  471. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/machine.yaml +158 -0
  472. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/open-questions.md +25 -0
  473. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/ctrf.json +79 -0
  474. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/junit.xml +7 -0
  475. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/manifest.json +1504 -0
  476. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/001.vtt +9 -0
  477. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/002.vtt +9 -0
  478. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/003.vtt +9 -0
  479. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/captions/004.vtt +17 -0
  480. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/01.mp4 +0 -0
  481. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/01.poster.webp +0 -0
  482. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/02.mp4 +0 -0
  483. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/clips/journey-1/02.poster.webp +0 -0
  484. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/01.png +0 -0
  485. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/01.webp +0 -0
  486. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/02.png +0 -0
  487. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/02.webp +0 -0
  488. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/03.png +0 -0
  489. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/03.webp +0 -0
  490. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/04.png +0 -0
  491. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-1/04.webp +0 -0
  492. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-2/01.png +0 -0
  493. package/packages/visualizer/fixtures/removed/runs/2026-10-06T14-40-00-000Z-showcase/media/paths/journey-2/01.webp +0 -0
  494. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.scxml +82 -0
  495. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/journeys.yaml +129 -0
  496. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/known-issues.md +14 -0
  497. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/machine.yaml +158 -0
  498. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/open-questions.md +25 -0
  499. package/packages/visualizer/fixtures/spec-only/runs/.gitkeep +0 -0
  500. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.scxml +82 -0
  501. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/journeys.yaml +129 -0
  502. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/known-issues.md +14 -0
  503. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/machine.yaml +158 -0
  504. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/open-questions.md +25 -0
  505. package/packages/visualizer/index.html +13 -0
  506. package/packages/visualizer/scripts/make-fixtures.ts +1490 -0
  507. package/packages/visualizer/scripts/publish-data.ts +22 -0
  508. package/packages/visualizer/src/app/AppShell.tsx +56 -0
  509. package/packages/visualizer/src/app/atlas-context.tsx +23 -0
  510. package/packages/visualizer/src/app/gazetteer.module.css +97 -0
  511. package/packages/visualizer/src/app/gazetteer.tsx +105 -0
  512. package/packages/visualizer/src/app/report-sources.tsx +122 -0
  513. package/packages/visualizer/src/app/router.tsx +69 -0
  514. package/packages/visualizer/src/app/search-query.tsx +14 -0
  515. package/packages/visualizer/src/components/DataIssues.module.css +136 -0
  516. package/packages/visualizer/src/components/DataIssues.tsx +70 -0
  517. package/packages/visualizer/src/components/Header.module.css +189 -0
  518. package/packages/visualizer/src/components/Header.tsx +127 -0
  519. package/packages/visualizer/src/components/Legend.module.css +84 -0
  520. package/packages/visualizer/src/components/Legend.tsx +50 -0
  521. package/packages/visualizer/src/components/Screen.module.css +312 -0
  522. package/packages/visualizer/src/components/Screen.tsx +141 -0
  523. package/packages/visualizer/src/components/Status.module.css +116 -0
  524. package/packages/visualizer/src/components/Status.tsx +60 -0
  525. package/packages/visualizer/src/components/icons.tsx +156 -0
  526. package/packages/visualizer/src/components/labels.tsx +22 -0
  527. package/packages/visualizer/src/data/adapters/adapter.ts +112 -0
  528. package/packages/visualizer/src/data/adapters/context.tsx +11 -0
  529. package/packages/visualizer/src/data/adapters/index.ts +29 -0
  530. package/packages/visualizer/src/data/adapters/report-backend.test.ts +96 -0
  531. package/packages/visualizer/src/data/adapters/report-backend.ts +42 -0
  532. package/packages/visualizer/src/data/fixtures.test.ts +407 -0
  533. package/packages/visualizer/src/data/model/build.ts +418 -0
  534. package/packages/visualizer/src/data/model/event-kind.test.ts +59 -0
  535. package/packages/visualizer/src/data/model/event-kind.ts +54 -0
  536. package/packages/visualizer/src/data/model/index.ts +3 -0
  537. package/packages/visualizer/src/data/model/journeys.test.ts +159 -0
  538. package/packages/visualizer/src/data/model/journeys.ts +29 -0
  539. package/packages/visualizer/src/data/model/simulation.test.ts +115 -0
  540. package/packages/visualizer/src/data/model/simulation.ts +187 -0
  541. package/packages/visualizer/src/data/model/status.test.ts +34 -0
  542. package/packages/visualizer/src/data/model/status.ts +43 -0
  543. package/packages/visualizer/src/data/model/types.ts +280 -0
  544. package/packages/visualizer/src/data/parse/issues.test.ts +82 -0
  545. package/packages/visualizer/src/data/parse/issues.ts +114 -0
  546. package/packages/visualizer/src/data/parse/manifest.ts +173 -0
  547. package/packages/visualizer/src/data/parse/markdown.test.ts +119 -0
  548. package/packages/visualizer/src/data/parse/markdown.ts +133 -0
  549. package/packages/visualizer/src/data/parse/spec.test.ts +106 -0
  550. package/packages/visualizer/src/data/parse/spec.ts +557 -0
  551. package/packages/visualizer/src/data/parse/structured.ts +44 -0
  552. package/packages/visualizer/src/data/queries.ts +369 -0
  553. package/packages/visualizer/src/data/testing/load-fixture.ts +63 -0
  554. package/packages/visualizer/src/evidence/api.ts +41 -0
  555. package/packages/visualizer/src/evidence/check-detail.tsx +257 -0
  556. package/packages/visualizer/src/evidence/decision-table.tsx +68 -0
  557. package/packages/visualizer/src/evidence/document-page.tsx +40 -0
  558. package/packages/visualizer/src/evidence/evidence-context.tsx +159 -0
  559. package/packages/visualizer/src/evidence/evidence.module.css +877 -0
  560. package/packages/visualizer/src/evidence/feature-page.tsx +88 -0
  561. package/packages/visualizer/src/evidence/format.ts +56 -0
  562. package/packages/visualizer/src/evidence/gherkin.ts +98 -0
  563. package/packages/visualizer/src/evidence/marks.tsx +118 -0
  564. package/packages/visualizer/src/evidence/model-page.tsx +60 -0
  565. package/packages/visualizer/src/evidence/model-trace.tsx +57 -0
  566. package/packages/visualizer/src/evidence/overview.tsx +134 -0
  567. package/packages/visualizer/src/evidence/quint.ts +45 -0
  568. package/packages/visualizer/src/evidence/recording-page.tsx +69 -0
  569. package/packages/visualizer/src/evidence/recording.module.css +464 -0
  570. package/packages/visualizer/src/evidence/recording.tsx +197 -0
  571. package/packages/visualizer/src/evidence/snapshot.tsx +108 -0
  572. package/packages/visualizer/src/evidence/sources-panel.tsx +134 -0
  573. package/packages/visualizer/src/evidence/sources.module.css +168 -0
  574. package/packages/visualizer/src/evidence/spec-header.tsx +64 -0
  575. package/packages/visualizer/src/evidence/spec-index.tsx +82 -0
  576. package/packages/visualizer/src/evidence/spec-page.tsx +92 -0
  577. package/packages/visualizer/src/evidence/spec-snapshot.tsx +26 -0
  578. package/packages/visualizer/src/layout/graph.test.ts +113 -0
  579. package/packages/visualizer/src/layout/graph.ts +324 -0
  580. package/packages/visualizer/src/layout/layout.ts +237 -0
  581. package/packages/visualizer/src/layout/use-chart-layout.ts +18 -0
  582. package/packages/visualizer/src/main.tsx +31 -0
  583. package/packages/visualizer/src/map/ChartMap.tsx +252 -0
  584. package/packages/visualizer/src/map/EventEdge.tsx +138 -0
  585. package/packages/visualizer/src/map/map.module.css +546 -0
  586. package/packages/visualizer/src/map/nodes.tsx +145 -0
  587. package/packages/visualizer/src/map/path.ts +24 -0
  588. package/packages/visualizer/src/reachable.test.ts +54 -0
  589. package/packages/visualizer/src/spec-kind.test.ts +27 -0
  590. package/packages/visualizer/src/spec-kind.ts +56 -0
  591. package/packages/visualizer/src/state/ui-store.ts +44 -0
  592. package/packages/visualizer/src/styles/global.css +105 -0
  593. package/packages/visualizer/src/styles/tokens.css +80 -0
  594. package/packages/visualizer/src/views/ChartPage.tsx +52 -0
  595. package/packages/visualizer/src/views/ProductPage.tsx +35 -0
  596. package/packages/visualizer/src/views/SidePanel.tsx +339 -0
  597. package/packages/visualizer/src/views/views.module.css +546 -0
  598. package/packages/visualizer/vite/atlas-dev-plugin.ts +169 -0
  599. package/packages/visualizer/vite/atlas-files.test.ts +119 -0
  600. package/packages/visualizer/vite/atlas-files.ts +199 -0
  601. package/packages/visualizer/vite/publish.test.ts +52 -0
  602. package/packages/visualizer/vite/publish.ts +99 -0
  603. package/packages/visualizer/vite/reports-dev-plugin.ts +82 -0
  604. package/packages/visualizer/vite.config.ts +13 -0
  605. package/packages/vitest/README.md +58 -0
  606. package/packages/webdriver/README.md +77 -0
@@ -0,0 +1,877 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { compareBusinessEvents, correlated } from '../events/cloudevents.js';
6
+ import { mergePrivacy, privacyFindings } from '../privacy.js';
7
+ import { toCtrf, toJUnit, toMermaid, toWebVtt } from '../report/formats.js';
8
+ import { splitTransitionId, transitionId, walkStates } from '../spec/types.js';
9
+ import { Timeline } from './types.js';
10
+ function git(command) {
11
+ try {
12
+ return execSync(`git ${command}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function specVersion(bundle) {
19
+ const hash = createHash('sha256');
20
+ for (const chart of bundle.charts)
21
+ hash.update(readFileSync(path.join(bundle.directory, chart.file)));
22
+ hash.update(JSON.stringify(bundle.journeys));
23
+ return hash.digest('hex').slice(0, 12);
24
+ }
25
+ const relative = (root, file) => path.relative(root, file).split(path.sep).join('/');
26
+ const pad = (n) => String(n).padStart(2, '0');
27
+ const sameStates = (a, b) => a.length === b.length && [...a].sort().join('|') === [...b].sort().join('|');
28
+ /** A rough cost of a path before it has ever run: setting it up, then each step. */
29
+ export const estimateMs = (p) => 5_000 + 2_500 * p.steps.length;
30
+ /**
31
+ * The paths one shard runs: longest first onto the least loaded shard, by the plan alone, so
32
+ * every machine computes the same split. Paths reported without running go to the first shard.
33
+ */
34
+ export function shardPaths(paths, shard) {
35
+ if (shard.count < 1 || shard.index < 1 || shard.index > shard.count)
36
+ throw new Error(`There is no shard ${shard.index}/${shard.count}`);
37
+ const load = Array.from({ length: shard.count }, () => 0);
38
+ const owner = new Map();
39
+ const running = paths.filter((p) => !p.skipRun).sort((a, b) => estimateMs(b) - estimateMs(a) || paths.indexOf(a) - paths.indexOf(b));
40
+ for (const p of running) {
41
+ const lightest = load.indexOf(Math.min(...load));
42
+ owner.set(p.key, lightest + 1);
43
+ load[lightest] += estimateMs(p);
44
+ }
45
+ return paths.filter((p) => (p.skipRun ? shard.index === 1 : owner.get(p.key) === shard.index));
46
+ }
47
+ /** Media files an outcome refers to, relative to its run directory. */
48
+ function mediaOf(outcome) {
49
+ const files = [];
50
+ const image = (i) => {
51
+ if (!i)
52
+ return;
53
+ files.push(i.png, ...(i.webp ? [i.webp] : []));
54
+ for (const a of i.also ?? [])
55
+ image(a);
56
+ };
57
+ for (const o of outcome.observed) {
58
+ image(o.shot);
59
+ if (o.clip)
60
+ files.push(o.clip.video, ...(o.clip.poster ? [o.clip.poster] : []), ...(o.clip.captions ? [o.clip.captions] : []));
61
+ }
62
+ image(outcome.failure?.shot);
63
+ for (const a of outcome.attempts)
64
+ files.push(...(a.trace ? [a.trace] : []), ...Object.values(a.traces ?? {}));
65
+ return files;
66
+ }
67
+ function copyMedia(outcome, from, to) {
68
+ for (const file of mediaOf(outcome)) {
69
+ const source = path.join(from, file);
70
+ if (!existsSync(source))
71
+ continue;
72
+ mkdirSync(path.dirname(path.join(to, file)), { recursive: true });
73
+ cpSync(source, path.join(to, file));
74
+ }
75
+ }
76
+ /** The plan keys and outcomes a run wrote, for merging shards and reusing passed paths. */
77
+ export function readOutcomes(runDir) {
78
+ const file = path.join(runDir, 'outcomes.json');
79
+ if (!existsSync(file))
80
+ return null;
81
+ try {
82
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
83
+ return { runId: parsed.runId, outcomes: parsed.outcomes ?? {} };
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ /** How long each path took in the newest run under `outputRoot` that recorded it, by path key. */
90
+ export function durationHistory(outputRoot) {
91
+ const history = {};
92
+ if (!existsSync(outputRoot))
93
+ return history;
94
+ const runs = readdirSync(outputRoot)
95
+ .filter((name) => !name.startsWith('.'))
96
+ .sort()
97
+ .reverse()
98
+ .slice(0, 10);
99
+ for (const run of runs) {
100
+ for (const [key, outcome] of Object.entries(readOutcomes(path.join(outputRoot, run))?.outcomes ?? {})) {
101
+ if (!(key in history) && !outcome.reusedFrom)
102
+ history[key] = outcome.durationMs;
103
+ }
104
+ }
105
+ return history;
106
+ }
107
+ /** The newest run under `outputRoot` that wrote outcomes. */
108
+ export function latestRun(outputRoot) {
109
+ if (!existsSync(outputRoot))
110
+ return null;
111
+ const runs = readdirSync(outputRoot)
112
+ .filter((name) => !name.startsWith('.') && existsSync(path.join(outputRoot, name, 'outcomes.json')))
113
+ .sort();
114
+ return runs.length ? path.join(outputRoot, runs.at(-1)) : null;
115
+ }
116
+ /**
117
+ * Fold path outcomes into the run's report, in plan order: the same plan and outcomes always give
118
+ * the same manifest, however many workers ran them, in whatever order they finished.
119
+ */
120
+ export function buildManifest(context, paths, outcomes, run) {
121
+ const { implementation: impl, graph, scope } = context;
122
+ const metaOf = new Map();
123
+ for (const chart of context.bundle.charts)
124
+ for (const { name, node } of walkStates(chart.machine))
125
+ if (node.meta)
126
+ metaOf.set(name, node.meta);
127
+ for (const { name, node } of walkStates(context.composition.machine))
128
+ if (node.meta && !metaOf.has(name))
129
+ metaOf.set(name, node.meta);
130
+ const states = Object.fromEntries([...scope.states].map((name) => [
131
+ name,
132
+ { status: 'not-reached', screenshot: null, screenshotsByPath: {}, recognizer: null, looksLike: [], businessEvents: null, checks: [], reachedBy: [], notes: [] }
133
+ ]));
134
+ const transitions = Object.fromEntries([...scope.transitions].map((id) => [id, { status: 'not-reached', clip: null, timeline: [], reason: 'No path reached it.', reachedBy: [] }]));
135
+ const leaks = [];
136
+ const results = [];
137
+ const resultStatus = new Map();
138
+ const reachedConfigs = new Map();
139
+ for (const planned of paths) {
140
+ const pathFields = {
141
+ id: planned.id,
142
+ key: planned.key,
143
+ name: planned.name,
144
+ kind: planned.kind,
145
+ description: planned.description,
146
+ start: planned.start,
147
+ ...(planned.seed ? { seed: planned.seed } : {}),
148
+ ...(planned.journeys ? { journeys: planned.journeys } : {})
149
+ };
150
+ if (planned.skipRun) {
151
+ const firstBlocked = planned.steps.findIndex((s) => !impl.events[s.event] || impl.events[s.event].blocked);
152
+ const steps = planned.steps.map((s, i) => ({
153
+ transition: s.transitions[0] ?? s.event,
154
+ event: s.event,
155
+ status: 'not-reached',
156
+ reason: firstBlocked < 0
157
+ ? `Not run: ${planned.blockedBy.join('; ')}`
158
+ : i < firstBlocked
159
+ ? 'Run on another path.'
160
+ : i === firstBlocked
161
+ ? `Blocked: ${impl.events[s.event]?.blocked ?? `No implementation for "${s.event}".`}`
162
+ : `Not reached: the path is blocked at "${planned.steps[firstBlocked]?.event}".`
163
+ }));
164
+ for (const s of steps.slice(Math.max(0, firstBlocked))) {
165
+ const tr = transitions[s.transition];
166
+ if (tr && tr.status === 'not-reached')
167
+ tr.reason = s.reason;
168
+ }
169
+ results.push({ ...pathFields, progress: 'complete', status: 'not-reached', attempts: [], steps, stoppedAt: null });
170
+ resultStatus.set(planned.id, 'not-reached');
171
+ continue;
172
+ }
173
+ const outcome = outcomes.get(planned.key);
174
+ if (!outcome) {
175
+ results.push({ ...pathFields, progress: run.finished ? 'complete' : run.activePaths?.includes(planned.id) ? 'running' : 'queued', status: 'not-reached', attempts: [], steps: [], stoppedAt: null });
176
+ continue;
177
+ }
178
+ const flaky = outcome.status === 'flaky';
179
+ leaks.push(...outcome.leaks);
180
+ if (outcome.failure && states[outcome.failure.state]) {
181
+ const record = states[outcome.failure.state];
182
+ record.recognizer = outcome.failure.recognizer;
183
+ record.looksLike = outcome.failure.looksLike;
184
+ if (outcome.failure.shot)
185
+ record.screenshotsByPath[`${planned.id} (failed)`] = outcome.failure.shot;
186
+ }
187
+ for (const item of outcome.observed) {
188
+ if (item.step.to.length)
189
+ reachedConfigs.set(planned.id, [...(reachedConfigs.get(planned.id) ?? []), item.step.to]);
190
+ const record = states[item.target];
191
+ if (record) {
192
+ if (!record.reachedBy.includes(planned.id))
193
+ record.reachedBy.push(planned.id);
194
+ if (record.status !== 'failed')
195
+ record.status = flaky ? 'flaky' : 'passed';
196
+ if (item.shot) {
197
+ record.screenshotsByPath[planned.id] = item.shot;
198
+ if (!record.screenshot)
199
+ record.screenshot = item.shot;
200
+ }
201
+ if (item.transient)
202
+ record.notes.push('Shown too briefly to capture; not seeing it is not a failure.');
203
+ record.recognizer = item.recognizer;
204
+ for (const check of item.checks)
205
+ if (!record.checks.some((c) => c.check === check.check && c.passed === check.passed))
206
+ record.checks.push(check);
207
+ if (item.checks.some((c) => !c.passed))
208
+ record.status = 'failed';
209
+ if (item.events.expected.length)
210
+ record.businessEvents = item.events;
211
+ }
212
+ for (let parent = graph.parent(item.target); parent; parent = graph.parent(parent)) {
213
+ const ancestor = states[parent];
214
+ if (!ancestor)
215
+ continue;
216
+ if (!ancestor.reachedBy.includes(planned.id))
217
+ ancestor.reachedBy.push(planned.id);
218
+ if (ancestor.status === 'not-reached')
219
+ ancestor.status = flaky ? 'flaky' : 'passed';
220
+ }
221
+ for (const t of item.step.transitions) {
222
+ const tr = transitions[t];
223
+ if (!tr)
224
+ continue;
225
+ if (!tr.reachedBy.includes(planned.id))
226
+ tr.reachedBy.push(planned.id);
227
+ if (tr.status !== 'failed')
228
+ tr.status = flaky ? 'flaky' : 'passed';
229
+ tr.reason = null;
230
+ if (item.clip && (!tr.clip || planned.kind === 'journey')) {
231
+ tr.clip = item.clip;
232
+ tr.timeline = item.timeline;
233
+ }
234
+ else if (!tr.timeline.length) {
235
+ tr.timeline = item.timeline;
236
+ }
237
+ }
238
+ }
239
+ for (const s of outcome.steps) {
240
+ const tr = transitions[s.transition];
241
+ if (!tr)
242
+ continue;
243
+ if (s.status === 'failed') {
244
+ tr.status = 'failed';
245
+ tr.reason = s.reason;
246
+ if (!tr.reachedBy.includes(planned.id))
247
+ tr.reachedBy.push(planned.id);
248
+ const { source, event } = splitTransitionId(s.transition);
249
+ const target = graph.target(source, event);
250
+ if (target && states[target])
251
+ states[target].status = 'failed';
252
+ }
253
+ else if (s.status === 'not-reached' && tr.status === 'not-reached') {
254
+ tr.reason = s.reason;
255
+ }
256
+ }
257
+ if (outcome.status === 'failed' && outcome.stoppedAt && states[outcome.stoppedAt])
258
+ states[outcome.stoppedAt].status = 'failed';
259
+ results.push({
260
+ ...pathFields,
261
+ progress: run.activePaths?.includes(planned.id) ? 'running' : 'complete',
262
+ status: outcome.status,
263
+ attempts: outcome.attempts,
264
+ steps: outcome.steps,
265
+ stoppedAt: outcome.stoppedAt,
266
+ durationMs: outcome.durationMs,
267
+ ...(outcome.setupMs !== undefined ? { setupMs: outcome.setupMs } : {}),
268
+ ...(outcome.reusedFrom ? { reusedFrom: outcome.reusedFrom } : {})
269
+ });
270
+ resultStatus.set(planned.id, outcome.status);
271
+ }
272
+ const journeys = (context.journeys ?? []).map((j) => {
273
+ const statuses = j.paths.map((id) => resultStatus.get(id));
274
+ const status = statuses.some((s) => s === 'failed')
275
+ ? 'failed'
276
+ : statuses.some((s) => s === undefined)
277
+ ? statuses.every((s) => s === undefined)
278
+ ? 'not-run'
279
+ : 'not-reached'
280
+ : statuses.every((s) => s === 'passed')
281
+ ? 'passed'
282
+ : statuses.every((s) => s === 'passed' || s === 'flaky')
283
+ ? 'flaky'
284
+ : 'not-reached';
285
+ return { name: j.name, description: j.description, status, paths: j.paths, stoppedAt: j.paths.find((id) => resultStatus.get(id) !== 'passed' && resultStatus.get(id) !== 'flaky') ?? null };
286
+ });
287
+ const seeds = (context.seeds ?? []).map((s) => {
288
+ const started = paths.filter((p) => p.seed === s.name).map((p) => p.id);
289
+ const verifiedBy = [...reachedConfigs.entries()]
290
+ .filter(([id, configs]) => ['passed', 'flaky'].includes(resultStatus.get(id) ?? '') && configs.some((c) => sameStates(c, s.active)))
291
+ .map(([id]) => id);
292
+ const seed = impl.seeds?.[s.name];
293
+ return {
294
+ name: s.name,
295
+ at: s.active.filter((n) => graph.isLeaf(n)),
296
+ how: seed?.how ?? null,
297
+ ...(seed?.shows ? { shows: seed.shows } : {}),
298
+ ...(seed?.for ? { for: seed.for } : {}),
299
+ ...(s.blocked ? { blocked: s.blocked } : {}),
300
+ paths: started,
301
+ verifiedBy
302
+ };
303
+ });
304
+ const count = (records) => ({
305
+ total: records.length,
306
+ passed: records.filter((r) => r.status === 'passed').length,
307
+ failed: records.filter((r) => r.status === 'failed').length,
308
+ flaky: records.filter((r) => r.status === 'flaky').length,
309
+ notReached: records.filter((r) => r.status === 'not-reached').length
310
+ });
311
+ const kindOf = (event) => impl.events[event]?.kind ?? (context.composition.handOffs.some((h) => h.event === event) ? 'hand-off' : 'user');
312
+ const allStates = walkStates(context.composition.machine);
313
+ const privacy = mergePrivacy(context.privacy);
314
+ const uniqueLeaks = [...new Set(leaks)];
315
+ const draft = {
316
+ $schema: 'https://github.com/crvouga/atlas/schemas/manifest.schema.json',
317
+ schemaVersion: 1,
318
+ run: {
319
+ id: run.id,
320
+ startedAt: run.startedAt.toISOString(),
321
+ finishedAt: run.finished ? new Date().toISOString() : null,
322
+ progress: {
323
+ totalPaths: paths.length,
324
+ completedPaths: paths.filter((p) => p.skipRun || (outcomes.has(p.key) && !run.activePaths?.includes(p.id))).length,
325
+ activePaths: run.activePaths ?? [],
326
+ updatedAt: new Date().toISOString()
327
+ },
328
+ durationMs: Date.now() - run.startedAt.getTime(),
329
+ commit: git('rev-parse HEAD'),
330
+ branch: git('rev-parse --abbrev-ref HEAD'),
331
+ environment: { driver: run.driver.name, ...(run.driver.clients ? { clients: run.driver.clients } : {}), ...context.environment },
332
+ mode: run.mode,
333
+ specVersion: specVersion(context.bundle),
334
+ specDirectory: relative(process.cwd(), context.bundle.directory),
335
+ approvals: [],
336
+ scope: { chart: scope.chart, start: scope.startLabel, ...(scope.state ? { state: scope.state } : {}) },
337
+ ...run.extra
338
+ },
339
+ charts: context.bundle.charts.map((c) => {
340
+ const host = context.composition.hosts.get(c.machine.id) ?? null;
341
+ return {
342
+ id: c.machine.id,
343
+ file: c.file,
344
+ description: typeof c.machine.meta?.description === 'string' ? c.machine.meta.description : '',
345
+ parentState: host,
346
+ handOffs: Object.fromEntries(context.composition.handOffs.filter((h) => h.childChart === c.machine.id).map((h) => [h.finalState, h.event]))
347
+ };
348
+ }),
349
+ map: {
350
+ nodes: allStates.map(({ name, node, trail }) => ({
351
+ id: name,
352
+ parent: trail.at(-1) ?? null,
353
+ chart: context.composition.chartOf.get(name) ?? context.bundle.root,
354
+ type: node.type ?? (node.states ? 'compound' : 'atomic'),
355
+ initial: node.initial ?? null,
356
+ inScope: scope.states.has(name)
357
+ })),
358
+ edges: allStates.flatMap(({ name, node }) => Object.keys(node.on ?? {}).map((event) => ({
359
+ id: transitionId(name, event),
360
+ source: name,
361
+ target: graph.target(name, event) ?? '',
362
+ event,
363
+ kind: kindOf(event),
364
+ inScope: scope.transitions.has(transitionId(name, event))
365
+ })))
366
+ },
367
+ states: Object.fromEntries(Object.entries(states).map(([name, record]) => {
368
+ const meta = metaOf.get(name);
369
+ const seededBy = seeds.filter((s) => s.at.includes(name)).map((s) => s.name);
370
+ return [
371
+ name,
372
+ {
373
+ name,
374
+ chart: context.composition.chartOf.get(name) ?? null,
375
+ description: meta?.description ?? '',
376
+ snapshot: meta?.snapshot ?? '',
377
+ ...(impl.states[name]?.client ? { client: impl.states[name].client } : {}),
378
+ ...(seededBy.length ? { seeds: seededBy } : {}),
379
+ confidence: meta?.confidence ?? 'assumed',
380
+ source: meta?.source ?? [],
381
+ expectedEvents: meta?.events ?? [],
382
+ ...record
383
+ }
384
+ ];
385
+ })),
386
+ transitions: Object.fromEntries(Object.entries(transitions).map(([id, record]) => {
387
+ const { source, event } = splitTransitionId(id);
388
+ const client = impl.events[event]?.client;
389
+ return [id, { id, source, target: graph.target(source, event) ?? null, event, kind: kindOf(event), how: impl.events[event]?.how ?? null, ...(client ? { client } : {}), ...record }];
390
+ })),
391
+ paths: results,
392
+ journeys,
393
+ seeds,
394
+ coverage: {
395
+ [scope.chart]: { states: count(Object.values(states)), transitions: count(Object.values(transitions)) },
396
+ overall: { states: count(Object.values(states)), transitions: count(Object.values(transitions)) }
397
+ },
398
+ privacy: { findings: uniqueLeaks, passed: uniqueLeaks.length === 0 },
399
+ reports: { junit: 'junit.xml', ctrf: 'ctrf.json', mermaid: 'chart.mmd' }
400
+ };
401
+ const found = privacyFindings(JSON.stringify(draft), 'manifest.json', privacy);
402
+ return found.length ? { ...draft, privacy: { findings: [...draft.privacy.findings, ...found], passed: false } } : draft;
403
+ }
404
+ function writeAtomic(file, text) {
405
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.tmp`);
406
+ writeFileSync(temporary, text);
407
+ renameSync(temporary, file);
408
+ }
409
+ /** Write a run's manifest, reports and outcomes; returns the manifest. */
410
+ export function writeRun(runDir, context, paths, outcomes, run) {
411
+ const manifest = buildManifest(context, paths, outcomes, run);
412
+ writeAtomic(path.join(runDir, 'junit.xml'), toJUnit(manifest));
413
+ writeAtomic(path.join(runDir, 'ctrf.json'), `${JSON.stringify(toCtrf(manifest), null, 2)}\n`);
414
+ writeAtomic(path.join(runDir, 'chart.mmd'), toMermaid(context.composition.machine));
415
+ const ordered = Object.fromEntries(paths.flatMap((p) => (outcomes.has(p.key) && !run.activePaths?.includes(p.id) ? [[p.key, outcomes.get(p.key)]] : [])));
416
+ writeAtomic(path.join(runDir, 'outcomes.json'), `${JSON.stringify({ runId: run.id, plan: paths.map((p) => p.key), outcomes: ordered })}\n`);
417
+ writeAtomic(path.join(runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
418
+ return manifest;
419
+ }
420
+ const newRunId = (mode) => `${new Date().toISOString().replace(/[:.]/g, '-')}-${mode}`;
421
+ /**
422
+ * Compose the outcomes of several runs of the same plan (the shards of one run, or a rerun of the
423
+ * paths that failed) into one run, folded in plan order. Media are copied in, so the merged run
424
+ * stands on its own. Later runs win when two ran the same path.
425
+ */
426
+ export function mergeRuns(context, paths, runDirs, options) {
427
+ const outcomes = new Map();
428
+ const keys = new Set(paths.map((p) => p.key));
429
+ const sources = [];
430
+ let mode = options.mode ?? 'fast';
431
+ let driver = options.driver ?? { name: 'merged' };
432
+ for (const dir of runDirs) {
433
+ const read = readOutcomes(dir);
434
+ if (!read)
435
+ throw new Error(`${dir} has no outcomes.json to merge`);
436
+ sources.push(read.runId);
437
+ try {
438
+ const manifest = JSON.parse(readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
439
+ if (manifest.run?.mode)
440
+ mode = options.mode ?? manifest.run.mode;
441
+ if (manifest.run?.environment?.driver && !options.driver)
442
+ driver = { name: manifest.run.environment.driver };
443
+ }
444
+ catch {
445
+ /* the outcomes are enough */
446
+ }
447
+ for (const [key, outcome] of Object.entries(read.outcomes))
448
+ if (keys.has(key))
449
+ outcomes.set(key, { ...outcome, reusedFrom: outcome.reusedFrom ?? read.runId });
450
+ }
451
+ const id = `${newRunId(mode).replace(/-(fast|showcase)$/, '')}-merged-${mode}`;
452
+ const runDir = path.join(options.outputRoot, id);
453
+ mkdirSync(runDir, { recursive: true });
454
+ for (const dir of runDirs) {
455
+ const read = readOutcomes(dir);
456
+ for (const [key, outcome] of Object.entries(read.outcomes))
457
+ if (outcomes.get(key)?.reusedFrom === (outcome.reusedFrom ?? read.runId))
458
+ copyMedia(outcome, dir, runDir);
459
+ }
460
+ const manifest = writeRun(runDir, context, paths, outcomes, { id, startedAt: new Date(), finished: true, mode, driver, extra: { mergedFrom: sources } });
461
+ return { runDir, manifest, ok: manifest.privacy.passed && manifest.paths.every((r) => r.status !== 'failed') };
462
+ }
463
+ /**
464
+ * Run planned paths against a system through a driver. Paths start from their seed (or the
465
+ * implementation's `setup`) and run on a pool of workers, longest first, each attempt in its own
466
+ * driver context; every state is recognised from the outside after every step; a failed path is
467
+ * retried once to tell flaky from broken. The manifest is rewritten as each path finishes, from
468
+ * all outcomes so far in plan order, so a crash keeps what ran and the order paths finished in
469
+ * never changes the report.
470
+ */
471
+ export async function runPaths(options) {
472
+ const { driver, implementation: impl, graph, scope } = options;
473
+ const runId = newRunId(options.mode);
474
+ const runDir = path.join(options.outputRoot, runId);
475
+ const mediaDir = path.join(runDir, 'media');
476
+ mkdirSync(mediaDir, { recursive: true });
477
+ const startedAt = new Date();
478
+ const showcase = options.mode === 'showcase';
479
+ const timeoutMs = options.stepTimeoutMs ?? 45_000;
480
+ const log = options.log ?? ((line) => process.stdout.write(`${line}\n`));
481
+ const privacy = mergePrivacy(options.privacy);
482
+ const sources = options.eventSources ?? [];
483
+ const hasCloudEvents = sources.some((s) => !s.name.startsWith('log:'));
484
+ const metaOf = new Map();
485
+ for (const chart of options.bundle.charts)
486
+ for (const { name, node } of walkStates(chart.machine))
487
+ if (node.meta)
488
+ metaOf.set(name, node.meta);
489
+ for (const { name, node } of walkStates(options.composition.machine))
490
+ if (node.meta && !metaOf.has(name))
491
+ metaOf.set(name, node.meta);
492
+ const toRun = options.paths.filter((p) => !p.skipRun);
493
+ const workers = Math.max(1, Math.min(options.workers ?? 1, driver.concurrency ?? 1, toRun.length || 1));
494
+ const sideBySide = workers > 1;
495
+ const outcomes = new Map();
496
+ const activeOutcomes = new Map();
497
+ const activePaths = new Set();
498
+ const reused = new Set();
499
+ for (const p of toRun) {
500
+ const previous = options.reuse?.outcomes[p.key];
501
+ if (!previous || previous.status !== 'passed')
502
+ continue;
503
+ copyMedia(previous, options.reuse.runDir, runDir);
504
+ outcomes.set(p.key, { ...previous, id: p.id, reusedFrom: previous.reusedFrom ?? path.basename(options.reuse.runDir) });
505
+ reused.add(p.key);
506
+ }
507
+ /** The state a path must show before its first step: what its seed says it shows, or the last active leaf. */
508
+ const startStateOf = (planned) => {
509
+ const seed = planned.seed ? impl.seeds?.[planned.seed] : undefined;
510
+ const recognizable = planned.start.filter((n) => graph.isLeaf(n) && impl.states[n]);
511
+ if (seed?.shows)
512
+ return seed.shows;
513
+ if (typeof seed?.at === 'string') {
514
+ const at = seed.at;
515
+ if (graph.isLeaf(at))
516
+ return at;
517
+ const within = recognizable.filter((n) => {
518
+ for (let p = graph.parent(n); p; p = graph.parent(p))
519
+ if (p === at)
520
+ return true;
521
+ return false;
522
+ });
523
+ if (within.length)
524
+ return within.at(-1);
525
+ }
526
+ return recognizable.at(-1) ?? planned.steps[0]?.from.at(-1) ?? '';
527
+ };
528
+ const leafTarget = (step) => {
529
+ const first = step.transitions[0];
530
+ const target = first ? graph.target(splitTransitionId(first).source, step.event) : undefined;
531
+ if (target && graph.isLeaf(target))
532
+ return target;
533
+ return step.to.filter((n) => graph.isLeaf(n) && scope.states.has(n) && impl.states[n]).at(-1) ?? target ?? step.to.at(-1);
534
+ };
535
+ const recognizeNow = async (ctx, state) => impl.states[state] ? impl.states[state].recognize(ctx) : { matched: false, signals: [] };
536
+ const waitFor = async (ctx, state) => {
537
+ const s = impl.states[state];
538
+ const deadline = Date.now() + timeoutMs;
539
+ let nextLook = Date.now() + 10_000;
540
+ let pause = 25;
541
+ let last = await recognizeNow(ctx, state);
542
+ while (!last.matched && Date.now() < deadline) {
543
+ await new Promise((r) => setTimeout(r, pause));
544
+ pause = Math.min(pause * 2, 250);
545
+ if (s?.lookIn && Date.now() > nextLook) {
546
+ await s.lookIn(ctx).catch(() => undefined);
547
+ nextLook = Date.now() + 10_000;
548
+ }
549
+ last = await recognizeNow(ctx, state);
550
+ }
551
+ return last;
552
+ };
553
+ const looksLike = async (ctx, except) => {
554
+ const names = Object.keys(impl.states).filter((name) => name !== except && impl.states[name]?.sameAs !== except && impl.states[except]?.sameAs !== name);
555
+ const seen = await Promise.all(names.map((name) => recognizeNow(ctx, name).then((r) => r.matched, () => false)));
556
+ return names.filter((_, i) => seen[i]);
557
+ };
558
+ const collectEvents = async (ctx, marks, expected) => {
559
+ if (!expected.length)
560
+ return compareBusinessEvents([], { events: [], text: '' }, {}, hasCloudEvents);
561
+ if (sideBySide && !impl.correlate)
562
+ return { expected, observed: [], missing: [], noSignal: expected, events: [] };
563
+ const all = await Promise.all(sources.map((s, i) => s.collect(marks[i])));
564
+ const collected = { events: all.flatMap((a) => a.events), text: all.map((a) => a.text ?? '').join('\n') };
565
+ const mine = impl.correlate ? correlated(collected, await impl.correlate(ctx)) : collected;
566
+ return compareBusinessEvents(expected, mine, options.eventMatchers ?? {}, hasCloudEvents);
567
+ };
568
+ const rel = (image) => ({
569
+ png: relative(runDir, image.png),
570
+ ...(image.webp ? { webp: relative(runDir, image.webp) } : {}),
571
+ ...(image.client ? { client: image.client } : {}),
572
+ ...(image.also?.length ? { also: image.also.map(rel) } : {})
573
+ });
574
+ async function attempt(planned, attemptNumber) {
575
+ const tag = `${planned.id}${attemptNumber > 1 ? '-retry' : ''}`;
576
+ const timeline = new Timeline();
577
+ const ctx = await driver.open({ path: planned, attempt: attemptNumber, mode: options.mode, directory: path.join(runDir, '.work', tag), timeline });
578
+ const pacing = showcase ? (driver.pacing ?? { before: 500, after: 1_000 }) : { before: 0, after: 0 };
579
+ let focus = {};
580
+ const tools = {
581
+ timeline,
582
+ mode: options.mode,
583
+ data: {},
584
+ hold: async (ms) => {
585
+ if (ms <= 0)
586
+ return;
587
+ if (driver.hold)
588
+ await driver.hold(ctx, ms, focus);
589
+ else
590
+ await new Promise((r) => setTimeout(r, ms));
591
+ }
592
+ };
593
+ const on = (client) => (client ? { client } : {});
594
+ const steps = [];
595
+ const observed = [];
596
+ const leaks = [];
597
+ let failure = null;
598
+ let error = null;
599
+ let stoppedAt = null;
600
+ let failed = false;
601
+ const attemptStarted = Date.now();
602
+ let setupMs;
603
+ const publishAttempt = () => {
604
+ activeOutcomes.set(planned.key, {
605
+ id: planned.id,
606
+ key: planned.key,
607
+ status: error ? 'failed' : 'not-reached',
608
+ attempts: [],
609
+ steps,
610
+ stoppedAt,
611
+ observed,
612
+ failure,
613
+ leaks,
614
+ durationMs: Date.now() - attemptStarted,
615
+ ...(setupMs !== undefined ? { setupMs } : {})
616
+ });
617
+ publish();
618
+ };
619
+ const shotFile = (name) => path.join(mediaDir, 'paths', tag, `${name}.png`);
620
+ const clipFile = (index) => path.join(mediaDir, 'clips', tag, `${pad(index + 1)}.mp4`);
621
+ const shoot = async (name, at) => {
622
+ const image = driver.screenshot ? await driver.screenshot(ctx, shotFile(name), at).catch(() => null) : null;
623
+ return image ? rel(image) : null;
624
+ };
625
+ try {
626
+ for (const event of new Set(planned.steps.map((s) => s.event)))
627
+ await impl.events[event]?.prepare?.(ctx, tools);
628
+ if (planned.seed) {
629
+ const seed = impl.seeds?.[planned.seed];
630
+ if (!seed)
631
+ throw new Error(`There is no seed "${planned.seed}"`);
632
+ await seed.run(ctx, { path: planned, tools });
633
+ }
634
+ else if (impl.setup) {
635
+ await impl.setup(ctx, { path: planned, tools });
636
+ }
637
+ else {
638
+ throw new Error('The path starts without a seed and the implementation has no setup');
639
+ }
640
+ const startState = startStateOf(planned);
641
+ const startTransient = impl.states[startState]?.transient === true;
642
+ const startCheck = startTransient ? await recognizeNow(ctx, startState) : await waitFor(ctx, startState);
643
+ if (!startCheck.matched && !startTransient) {
644
+ stoppedAt = startState;
645
+ const like = await looksLike(ctx, startState);
646
+ const by = planned.seed ? `the seed "${planned.seed}" did not show` : 'expected';
647
+ throw Object.assign(new Error(`The path could not start: ${by} "${startState}", the app looks like ${like.join(', ') || 'none of the known states'}`), {
648
+ recognizer: startCheck,
649
+ looksLike: like,
650
+ state: startState,
651
+ stepIndex: 0
652
+ });
653
+ }
654
+ setupMs = Date.now() - attemptStarted;
655
+ observed.push({
656
+ step: { event: '(start)', transitions: [], handOff: false, from: [], to: [] },
657
+ target: startState,
658
+ clip: null,
659
+ timeline: [],
660
+ events: { expected: [], observed: [], missing: [], noSignal: [], events: [] },
661
+ recognizer: startCheck,
662
+ shot: await shoot('00-start', on(impl.states[startState]?.client)),
663
+ transient: false,
664
+ checks: (await impl.states[startState]?.checks?.(ctx)) ?? []
665
+ });
666
+ if (driver.text)
667
+ leaks.push(...privacyFindings(await driver.text(ctx).catch(() => ''), `${planned.id} / ${startState}`, privacy));
668
+ publishAttempt();
669
+ for (const [index, step] of planned.steps.entries()) {
670
+ const ev = impl.events[step.event];
671
+ const target = leafTarget(step);
672
+ const transition = step.transitions[0] ?? transitionId('?', step.event);
673
+ if (!ev || ev.blocked) {
674
+ const reason = ev?.blocked ?? `No implementation for "${step.event}".`;
675
+ stoppedAt = step.from.filter((n) => scope.states.has(n)).at(-1) ?? null;
676
+ for (const rest of planned.steps.slice(index)) {
677
+ steps.push({
678
+ transition: rest.transitions[0] ?? rest.event,
679
+ event: rest.event,
680
+ status: 'not-reached',
681
+ reason: rest === step ? `Blocked: ${reason}` : `Not reached: the path stopped before it (blocked "${step.event}").`
682
+ });
683
+ }
684
+ break;
685
+ }
686
+ focus = on(ev.client ?? impl.states[target]?.client);
687
+ const recording = showcase && !step.handOff && driver.record ? await driver.record(ctx, path.join(runDir, '.frames', tag, String(index)), focus) : null;
688
+ timeline.restart();
689
+ await tools.hold(pacing.before);
690
+ const marks = await Promise.all(sources.map((s) => s.mark()));
691
+ try {
692
+ await ev.run(ctx, tools);
693
+ }
694
+ catch (e) {
695
+ await recording?.stop(clipFile(index)).catch(() => null);
696
+ const like = await looksLike(ctx, '');
697
+ const first = (e instanceof Error ? e.message : String(e)).split('\n')[0];
698
+ throw Object.assign(new Error(`"${step.event}" could not be done (${first}); the app looks like ${like.length ? like.map((x) => `"${x}"`).join(', ') : 'none of the known states'}`), {
699
+ state: step.from.filter((n) => impl.states[n]).at(-1) ?? target,
700
+ looksLike: like,
701
+ stepIndex: index
702
+ });
703
+ }
704
+ const s = impl.states[target];
705
+ if (s?.lookIn && !(await recognizeNow(ctx, target)).matched) {
706
+ // Looking is a courtesy: if the place to look cannot be opened, recognition decides.
707
+ await s.lookIn(ctx).catch(() => undefined);
708
+ timeline.add({ kind: 'note', label: 'Looks where this shows up' });
709
+ }
710
+ const recognizer = s?.transient ? await recognizeNow(ctx, target) : await waitFor(ctx, target);
711
+ const transient = !recognizer.matched && s?.transient === true;
712
+ if (!recognizer.matched && !transient) {
713
+ const like = await looksLike(ctx, target);
714
+ await recording?.stop(clipFile(index)).catch(() => null);
715
+ throw Object.assign(new Error(`Expected "${target}", the app looks like ${like.length ? like.map((x) => `"${x}"`).join(', ') : 'none of the known states'}`), {
716
+ recognizer,
717
+ looksLike: like,
718
+ state: target,
719
+ stepIndex: index
720
+ });
721
+ }
722
+ timeline.add({ kind: 'screen', label: `Now: ${target}` });
723
+ const checks = transient ? [] : ((await s?.checks?.(ctx)) ?? []);
724
+ await tools.hold(pacing.after);
725
+ const recorded = recording ? await recording.stop(clipFile(index)) : null;
726
+ let clip = null;
727
+ if (recorded) {
728
+ const captions = recorded.video.replace(/\.mp4$/, '.vtt');
729
+ writeFileSync(captions, toWebVtt([...timeline.entries], recorded.durationMs));
730
+ clip = {
731
+ video: relative(runDir, recorded.video),
732
+ ...(recorded.poster ? { poster: relative(runDir, recorded.poster) } : {}),
733
+ durationMs: recorded.durationMs,
734
+ captions: relative(runDir, captions),
735
+ ...(recorded.client ? { client: recorded.client } : {})
736
+ };
737
+ }
738
+ const events = await collectEvents(ctx, marks, metaOf.get(target)?.events ?? []);
739
+ if (driver.text)
740
+ leaks.push(...privacyFindings(await driver.text(ctx).catch(() => ''), `${planned.id} / ${target}`, privacy));
741
+ const shot = transient ? null : await shoot(pad(index + 1), on(s?.client));
742
+ observed.push({ step, target, clip, timeline: [...timeline.entries], events, recognizer, shot, transient, checks });
743
+ steps.push({ transition, event: step.event, status: 'passed', reason: null });
744
+ publishAttempt();
745
+ await s?.settle?.(ctx);
746
+ }
747
+ }
748
+ catch (e) {
749
+ failed = true;
750
+ const err = e;
751
+ error = err.message;
752
+ stoppedAt = err.state ?? stoppedAt;
753
+ const index = err.stepIndex ?? 0;
754
+ const failedShot = await shoot(`failed-${pad(index + 1)}`, on(err.state ? impl.states[err.state]?.client : undefined));
755
+ if (err.state)
756
+ failure = { state: err.state, recognizer: err.recognizer ?? null, looksLike: err.looksLike ?? [], shot: failedShot };
757
+ const failedStep = planned.steps[index];
758
+ for (const rest of planned.steps.slice(steps.length)) {
759
+ steps.push({
760
+ transition: rest.transitions[0] ?? rest.event,
761
+ event: rest.event,
762
+ status: rest === failedStep ? 'failed' : 'not-reached',
763
+ reason: rest === failedStep ? err.message : `Not reached: the path stopped at "${err.state ?? 'start'}".`
764
+ });
765
+ }
766
+ publishAttempt();
767
+ }
768
+ const traceFile = path.join(mediaDir, 'traces', `${tag}.zip`);
769
+ const closed = await driver.close(ctx, { failed, traceFile }).catch(() => undefined);
770
+ const trace = closed && closed.trace ? relative(runDir, closed.trace) : null;
771
+ const traces = closed && closed.traces ? Object.fromEntries(Object.entries(closed.traces).map(([client, file]) => [client, relative(runDir, file)])) : undefined;
772
+ return { error, stoppedAt, steps, observed, trace, traces, failure, leaks, setupMs };
773
+ }
774
+ async function safeAttempt(planned, attemptNumber) {
775
+ try {
776
+ return await attempt(planned, attemptNumber);
777
+ }
778
+ catch (e) {
779
+ const message = `The path could not be set up: ${(e instanceof Error ? e.message : String(e)).split('\n')[0]}`;
780
+ return {
781
+ error: message,
782
+ stoppedAt: null,
783
+ steps: planned.steps.map((s, i) => ({
784
+ transition: s.transitions[0] ?? s.event,
785
+ event: s.event,
786
+ status: (i === 0 ? 'failed' : 'not-reached'),
787
+ reason: i === 0 ? message : 'Not reached: the path could not be set up.'
788
+ })),
789
+ observed: [],
790
+ trace: null,
791
+ traces: undefined,
792
+ failure: null,
793
+ leaks: [],
794
+ setupMs: undefined
795
+ };
796
+ }
797
+ }
798
+ async function runPath(planned) {
799
+ const started = Date.now();
800
+ log(`▶ ${planned.id} ${planned.name}${planned.seed ? ` (from "${planned.seed}")` : ''}`);
801
+ const attempts = [];
802
+ const record = (o) => attempts.push({ status: o.error ? 'failed' : 'passed', error: o.error, stoppedAt: o.stoppedAt, trace: o.trace, ...(o.traces ? { traces: o.traces } : {}) });
803
+ let outcome = await safeAttempt(planned, 1);
804
+ record(outcome);
805
+ let flaky = false;
806
+ if (outcome.error && options.retry !== false) {
807
+ const second = await safeAttempt(planned, 2);
808
+ record(second);
809
+ flaky = !second.error;
810
+ outcome = second;
811
+ }
812
+ const blocked = outcome.steps.some((s) => s.reason?.startsWith('Blocked:'));
813
+ const status = outcome.error ? 'failed' : flaky ? 'flaky' : blocked ? 'not-reached' : 'passed';
814
+ log(` ${planned.id} ${status}${outcome.error ? ` — ${outcome.error}` : ''}`);
815
+ return {
816
+ id: planned.id,
817
+ key: planned.key,
818
+ status,
819
+ attempts,
820
+ steps: outcome.steps,
821
+ stoppedAt: outcome.stoppedAt,
822
+ observed: outcome.observed,
823
+ failure: outcome.failure,
824
+ leaks: outcome.leaks,
825
+ durationMs: Date.now() - started,
826
+ ...(outcome.setupMs !== undefined ? { setupMs: outcome.setupMs } : {})
827
+ };
828
+ }
829
+ const runInfo = (finished) => ({
830
+ id: runId,
831
+ startedAt,
832
+ finished,
833
+ mode: options.mode,
834
+ driver: { name: driver.name, ...(driver.clients ? { clients: driver.clients } : {}) },
835
+ activePaths: options.paths.filter((p) => activePaths.has(p.id)).map((p) => p.id),
836
+ extra: {
837
+ workers,
838
+ ...(options.shard ? { shard: options.shard } : {}),
839
+ ...(reused.size ? { reused: { from: path.basename(options.reuse.runDir), paths: reused.size } } : {})
840
+ }
841
+ });
842
+ function publish() {
843
+ writeRun(runDir, options, options.paths, new Map([...outcomes, ...activeOutcomes]), runInfo(false));
844
+ }
845
+ const history = durationHistory(options.outputRoot);
846
+ const cost = (p) => history[p.key] ?? estimateMs(p);
847
+ const queue = toRun.filter((p) => !reused.has(p.key)).sort((a, b) => cost(b) - cost(a) || options.paths.indexOf(a) - options.paths.indexOf(b));
848
+ if (reused.size)
849
+ log(`Reusing ${reused.size} passed path(s) from ${path.basename(options.reuse.runDir)}`);
850
+ log(`Running ${queue.length} path(s) on ${workers} worker(s)`);
851
+ writeRun(runDir, options, options.paths, outcomes, runInfo(false));
852
+ // A driver can throw outside any awaited call (a message for a context that just closed); it
853
+ // belongs to one attempt, so it is logged and the run goes on instead of the process dying.
854
+ const stray = (error) => log(` (ignored an error thrown outside any step: ${(error instanceof Error ? error.message : String(error)).split('\n')[0]})`);
855
+ process.on('uncaughtException', stray);
856
+ process.on('unhandledRejection', stray);
857
+ try {
858
+ await Promise.all(Array.from({ length: workers }, async () => {
859
+ for (let next = queue.shift(); next; next = queue.shift()) {
860
+ activePaths.add(next.id);
861
+ publish();
862
+ outcomes.set(next.key, await runPath(next));
863
+ activeOutcomes.delete(next.key);
864
+ activePaths.delete(next.id);
865
+ publish();
866
+ }
867
+ }));
868
+ }
869
+ finally {
870
+ process.off('uncaughtException', stray);
871
+ process.off('unhandledRejection', stray);
872
+ }
873
+ await driver.dispose?.();
874
+ await Promise.all(sources.map((s) => s.close?.()));
875
+ const manifest = writeRun(runDir, options, options.paths, outcomes, runInfo(true));
876
+ return { runDir, manifest, ok: manifest.privacy.passed && manifest.paths.every((r) => r.status !== 'failed') };
877
+ }