@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,1490 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ import { composeCharts, machineToScxml, parseChart, toCtrf, toJUnit, toMermaid, toWebVtt, type MachineConfig } from '@crvouga/atlas';
7
+ import type {
8
+ CheckResult,
9
+ Clip,
10
+ CloudEvent,
11
+ Count,
12
+ EventKind,
13
+ Image,
14
+ PathRecord,
15
+ RunInfo,
16
+ RunStatus,
17
+ StateRecord,
18
+ TimelineEntry,
19
+ TransitionRecord
20
+ } from '@crvouga/atlas/schema';
21
+ import YAML from 'yaml';
22
+
23
+ import { resolveEventKind } from '../src/data/model/event-kind';
24
+ import { createJourneyReplayer, type ReplayResult } from '../src/data/model/journeys';
25
+ import { createIssueSink } from '../src/data/parse/issues';
26
+ import { isMachineFile, parseSpec, slug, type ParsedState, type ParsedTransition, type SpecDocument } from '../src/data/parse/spec';
27
+ import { APP_ROOT, FIXTURES_ROOT, listSpecFiles } from '../vite/atlas-files';
28
+
29
+ /**
30
+ * Regenerates every fixture from the example spec in `example/specs` (a coffee-ordering app: an
31
+ * XState root chart with a parallel region that invokes an SCXML checkout chart) and a generated
32
+ * large atlas. All media is synthetic: ImageMagick draws the screens and ffmpeg the clips.
33
+ */
34
+
35
+ const EXAMPLE_SPECS = path.join(APP_ROOT, 'example/specs');
36
+ const SPEC_DIR = 'coffee/ordering';
37
+ const TOP_CHART = 'Coffee order';
38
+ const CHECKOUT_CHART = 'Checkout';
39
+ const ENTRY_STATES = ['Reviewing the cart'];
40
+ const ENTRY_TRANSITION = 'Reviewing the cart :: Starts checkout';
41
+ const MEDIA_CACHE = path.join(APP_ROOT, '.cache', 'fixture-media');
42
+ const MEDIA_VERSION = 3;
43
+ const SIZE_BUDGET = 6 * 1024 * 1024;
44
+ const CLIP_MS = 2500;
45
+
46
+ function tool(name: string) {
47
+ const pinned = `/opt/homebrew/bin/${name}`;
48
+ return existsSync(pinned) ? pinned : name;
49
+ }
50
+ const MAGICK = tool('magick');
51
+ const CWEBP = tool('cwebp');
52
+ const FFMPEG = tool('ffmpeg');
53
+ const FONT = '/System/Library/Fonts/Supplemental/Arial.ttf';
54
+ const FONT_BOLD = '/System/Library/Fonts/Supplemental/Arial Bold.ttf';
55
+
56
+ function exec(cmd: string, args: string[]) {
57
+ execFileSync(cmd, args, { stdio: ['ignore', 'ignore', 'pipe'] });
58
+ }
59
+
60
+ function log(line: string) {
61
+ process.stdout.write(`${line}\n`);
62
+ }
63
+
64
+ function hashOf(value: unknown, length = 16) {
65
+ return createHash('sha1').update(JSON.stringify(value)).digest('hex').slice(0, length);
66
+ }
67
+
68
+ function seeded(key: string) {
69
+ let a = Number.parseInt(hashOf(key, 8), 16);
70
+ return () => {
71
+ a = (a + 0x6d2b79f5) | 0;
72
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
73
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
74
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
75
+ };
76
+ }
77
+
78
+ function capitalize(text: string) {
79
+ return text ? text[0]!.toUpperCase() + text.slice(1) : text;
80
+ }
81
+
82
+ function lowerFirst(text: string) {
83
+ return text ? text[0]!.toLowerCase() + text.slice(1) : text;
84
+ }
85
+
86
+ function writeText(file: string, text: string) {
87
+ mkdirSync(path.dirname(file), { recursive: true });
88
+ writeFileSync(file, text);
89
+ }
90
+
91
+ function place(src: string, dest: string) {
92
+ mkdirSync(path.dirname(dest), { recursive: true });
93
+ copyFileSync(src, dest);
94
+ }
95
+
96
+ function runIdOf(iso: string, mode: 'fast' | 'showcase') {
97
+ return `${iso.replace(/:/g, '-').replace('.', '-')}-${mode}`;
98
+ }
99
+
100
+ function addMs(iso: string, ms: number) {
101
+ return new Date(Date.parse(iso) + ms).toISOString();
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------------------------
105
+ // Media
106
+
107
+ type ScreenArt = { title: string; subtitle: string; color: string; tone: 'normal' | 'error' };
108
+ type ClipKind = 'tap' | 'type' | 'system' | 'time';
109
+ const CLIP_KINDS: ClipKind[] = ['tap', 'type', 'system', 'time'];
110
+
111
+ function magickText(text: string) {
112
+ return text.replace(/\\/g, '\\\\').replace(/%/g, '%%').replace(/^@/, '\\@');
113
+ }
114
+
115
+ function renderScreen(art: ScreenArt) {
116
+ const key = hashOf({ v: MEDIA_VERSION, art });
117
+ const png = path.join(MEDIA_CACHE, `screen-${key}.png`);
118
+ const webp = path.join(MEDIA_CACHE, `screen-${key}.webp`);
119
+ if (existsSync(png) && existsSync(webp)) return { png, webp };
120
+ mkdirSync(MEDIA_CACHE, { recursive: true });
121
+ const bar = art.tone === 'error' ? '#B42318' : art.color;
122
+ const notice =
123
+ art.tone === 'error'
124
+ ? ['-fill', '#FEE4E2', '-draw', 'roundrectangle 20,604 370,664 12,12', '-fill', '#B42318', '-font', FONT_BOLD, '-pointsize', '15', '-annotate', '+36+640', 'Something went wrong']
125
+ : [];
126
+ exec(MAGICK, [
127
+ '-size', '390x844', 'xc:#F5F6F8',
128
+ '-fill', bar, '-draw', 'rectangle 0,0 390,108',
129
+ '-font', FONT_BOLD, '-pointsize', '14', '-fill', '#FFFFFF', '-annotate', '+20+34', '9:41',
130
+ '-font', FONT, '-pointsize', '15', '-annotate', '+20+88', magickText(art.subtitle),
131
+ '(', '-size', '350x', '-background', 'none', '-font', FONT_BOLD, '-pointsize', '26', '-fill', '#111827', `caption:${magickText(art.title)}`, ')',
132
+ '-geometry', '+20+132', '-composite',
133
+ '-fill', '#D5D9E0',
134
+ '-draw', 'roundrectangle 20,270 330,282 6,6',
135
+ '-draw', 'roundrectangle 20,294 290,306 6,6',
136
+ '-draw', 'roundrectangle 20,318 310,330 6,6',
137
+ '-fill', '#FFFFFF', '-stroke', '#E3E6EB', '-draw', 'roundrectangle 20,380 370,560 16,16', '-stroke', 'none',
138
+ '-fill', art.color, '-draw', 'circle 60,424 60,444',
139
+ '-fill', '#D5D9E0',
140
+ '-draw', 'roundrectangle 92,416 330,428 6,6',
141
+ '-draw', 'roundrectangle 40,480 350,492 6,6',
142
+ '-draw', 'roundrectangle 40,504 300,516 6,6',
143
+ ...notice,
144
+ '-fill', art.color, '-draw', 'roundrectangle 20,704 370,754 25,25',
145
+ '-fill', 'none', '-stroke', art.color, '-strokewidth', '2', '-draw', 'roundrectangle 21,768 369,814 23,23', '-stroke', 'none',
146
+ '-gravity', 'North', '-font', FONT_BOLD, '-pointsize', '16', '-fill', '#FFFFFF', '-annotate', '+0+720', 'Continue',
147
+ '-fill', art.color, '-annotate', '+0+782', 'Not now',
148
+ '-gravity', 'SouthEast', '-font', FONT, '-pointsize', '11', '-fill', '#9CA3AF', '-annotate', '+12+8', 'Fixture',
149
+ '-strip', '-define', 'png:exclude-chunk=date,time', '-colors', '48', `PNG8:${png}`
150
+ ]);
151
+ exec(CWEBP, ['-quiet', '-q', '70', '-resize', '200', '0', png, '-o', webp]);
152
+ return { png, webp };
153
+ }
154
+
155
+ const CLIP_ART: Record<ClipKind, { label: string; color: string; extra: string[] }> = {
156
+ tap: {
157
+ label: 'User taps',
158
+ color: '0x2F6FED',
159
+ extra: [
160
+ "drawbox=x=48:y=352:w=100:h=26:color=0x2F6FED:t=fill",
161
+ "drawbox=x='30+t*40':y='280+t*40':w=14:h=14:color=0x111827@0.5:t=fill:enable='lt(t,1.7)'",
162
+ "drawbox=x=44:y=348:w=108:h=34:color=0x111827@0.25:t=fill:enable='gte(t,1.7)'"
163
+ ]
164
+ },
165
+ type: {
166
+ label: 'User types',
167
+ color: '0x7A5AF8',
168
+ extra: [
169
+ "drawbox=x=10:y=200:w=176:h=30:color=0xFFFFFF:t=fill",
170
+ "drawbox=x=16:y=212:w='min(150,t*80)':h=7:color=0x111827@0.6:t=fill"
171
+ ]
172
+ },
173
+ system: {
174
+ label: 'System update',
175
+ color: '0x0F9D74',
176
+ extra: [
177
+ "drawbox=x=78:y=190:w=40:h=40:color=0x0F9D74@0.4:t=fill:enable='lt(t,1.2)'",
178
+ "drawbox=x=10:y=260:w=176:h=90:color=0xFFFFFF:t=fill:enable='gte(t,1.2)'",
179
+ "drawbox=x=20:y=280:w=120:h=8:color=0xD5D9E0:t=fill:enable='gte(t,1.2)'"
180
+ ]
181
+ },
182
+ time: {
183
+ label: 'Time passes',
184
+ color: '0xDC6803',
185
+ extra: [
186
+ "drawbox=x=58:y=170:w=80:h=80:color=0xDC6803@0.3:t=fill",
187
+ "drawbox=x=96:y='180+mod(t*40,60)':w=4:h=20:color=0x111827:t=fill"
188
+ ]
189
+ }
190
+ };
191
+
192
+ function renderClip(kind: ClipKind) {
193
+ const art = CLIP_ART[kind];
194
+ const key = hashOf({ v: MEDIA_VERSION, kind, art });
195
+ const mp4 = path.join(MEDIA_CACHE, `clip-${key}.mp4`);
196
+ const poster = path.join(MEDIA_CACHE, `clip-${key}.poster.webp`);
197
+ if (existsSync(mp4) && existsSync(poster)) return { mp4, poster };
198
+ mkdirSync(MEDIA_CACHE, { recursive: true });
199
+ const filters = [
200
+ `drawbox=x=0:y=0:w=196:h=54:color=${art.color}:t=fill`,
201
+ `drawtext=fontfile=${FONT}:text='${art.label}':x=10:y=72:fontsize=15:fontcolor=0x111827`,
202
+ 'drawbox=x=10:y=110:w=150:h=7:color=0xD5D9E0:t=fill',
203
+ 'drawbox=x=10:y=126:w=120:h=7:color=0xD5D9E0:t=fill',
204
+ ...art.extra,
205
+ `drawtext=fontfile=${FONT}:text='Fixture':x=w-tw-6:y=h-th-6:fontsize=9:fontcolor=0x9CA3AF`
206
+ ];
207
+ exec(FFMPEG, [
208
+ '-v', 'error', '-y', '-f', 'lavfi', '-i', `color=c=0xF5F6F8:s=196x424:r=12:d=${CLIP_MS / 1000}`,
209
+ '-vf', filters.join(','),
210
+ '-c:v', 'libx264', '-preset', 'slow', '-crf', '34', '-pix_fmt', 'yuv420p', '-threads', '1',
211
+ '-movflags', '+faststart', '-fflags', '+bitexact', '-flags:v', '+bitexact', '-map_metadata', '-1', '-an', mp4
212
+ ]);
213
+ const posterPng = path.join(MEDIA_CACHE, `clip-${key}.poster.png`);
214
+ exec(FFMPEG, ['-v', 'error', '-y', '-ss', '2', '-i', mp4, '-frames:v', '1', '-fflags', '+bitexact', '-flags:v', '+bitexact', posterPng]);
215
+ exec(CWEBP, ['-quiet', '-q', '70', posterPng, '-o', poster]);
216
+ return { mp4, poster };
217
+ }
218
+
219
+ type RunWriter = {
220
+ dir: string;
221
+ screen: (pathId: string, art: ScreenArt, options?: { prefix?: string; write?: boolean }) => Image;
222
+ clip: (kind: ClipKind, timeline: TimelineEntry[]) => Clip;
223
+ pendingClip: () => Clip;
224
+ };
225
+
226
+ /** Writes media the way the runner lays it out: `media/paths/<path>/NN.png|webp`, `media/clips/...`. */
227
+ function runWriter(runDir: string): RunWriter {
228
+ const counters = new Map<string, number>();
229
+ const placed = new Set<ClipKind>();
230
+ let captions = 0;
231
+ const clipBase = (n: number) => `media/clips/journey-1/${String(n).padStart(2, '0')}`;
232
+ return {
233
+ dir: runDir,
234
+ screen(pathId, art, { prefix = '', write = true } = {}) {
235
+ const n = (counters.get(pathId) ?? 0) + 1;
236
+ counters.set(pathId, n);
237
+ const base = `media/paths/${pathId}/${prefix}${String(n).padStart(2, '0')}`;
238
+ if (write) {
239
+ const media = renderScreen(art);
240
+ place(media.png, path.join(runDir, `${base}.png`));
241
+ place(media.webp, path.join(runDir, `${base}.webp`));
242
+ }
243
+ return { png: `${base}.png`, webp: `${base}.webp` };
244
+ },
245
+ clip(kind, timeline) {
246
+ const base = clipBase(CLIP_KINDS.indexOf(kind) + 1);
247
+ if (!placed.has(kind)) {
248
+ const media = renderClip(kind);
249
+ place(media.mp4, path.join(runDir, `${base}.mp4`));
250
+ place(media.poster, path.join(runDir, `${base}.poster.webp`));
251
+ placed.add(kind);
252
+ }
253
+ const vtt = `media/captions/${String(++captions).padStart(3, '0')}.vtt`;
254
+ writeText(path.join(runDir, vtt), toWebVtt(timeline, CLIP_MS));
255
+ return { video: `${base}.mp4`, poster: `${base}.poster.webp`, durationMs: CLIP_MS, captions: vtt };
256
+ },
257
+ pendingClip() {
258
+ const base = clipBase(CLIP_KINDS.length + 1);
259
+ return { video: `${base}.mp4`, poster: `${base}.poster.webp`, state: 'processing' };
260
+ }
261
+ };
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------------------------
265
+ // Specs
266
+
267
+ type SpecText = { path: string; text: string };
268
+ type LoadedSpec = { doc: SpecDocument; files: SpecText[]; replays: Map<string, ReplayResult>; version: string; specDirectory: string };
269
+
270
+ function loadSpec(files: SpecText[], specDirectory: string): LoadedSpec {
271
+ const sink = createIssueSink();
272
+ const doc = parseSpec({ ref: null, files }, sink);
273
+ const problems = sink.issues.filter((i) => i.severity !== 'info');
274
+ if (problems.length) throw new Error(`Spec has problems: ${JSON.stringify(problems, null, 2)}`);
275
+ const replay = createJourneyReplayer(doc, sink);
276
+ const replays = new Map(doc.journeys.map((j) => [j.id, replay(j)]));
277
+ const broken = doc.journeys.filter((j) => !replays.get(j.id)!.ok);
278
+ if (broken.length) throw new Error(`Journeys don't replay: ${broken.map((j) => j.name).join(', ')}\n${JSON.stringify(sink.issues, null, 2)}`);
279
+ return { doc, files, replays, version: hashOf(files.map((f) => f.text), 12), specDirectory };
280
+ }
281
+
282
+ function exampleFiles(): SpecText[] {
283
+ return listSpecFiles(EXAMPLE_SPECS).map((file) => ({ path: file, text: readFileSync(path.join(EXAMPLE_SPECS, file), 'utf8') }));
284
+ }
285
+
286
+ function writeSpec(fixtureDir: string, files: SpecText[]) {
287
+ for (const file of files) writeText(path.join(fixtureDir, 'specs', file.path), file.text);
288
+ }
289
+
290
+ /** The composed machine of one chart directory, as the runner composes it, for the Mermaid report. */
291
+ function composedMachine(files: SpecText[], dir: string, root: string): MachineConfig {
292
+ const charts = files.filter((f) => f.path.startsWith(`${dir}/`) && isMachineFile(f.path)).map((f) => parseChart(f.path.slice(dir.length + 1), f.text));
293
+ return composeCharts({ directory: dir, charts, journeys: [], root }).machine;
294
+ }
295
+
296
+ type Scope = { states: Set<string>; transitions: Set<string> };
297
+
298
+ /** The runner only knows composed transitions: hand-offs, never the parent transitions they replace. */
299
+ function runnable(t: ParsedTransition) {
300
+ return !t.carriedBy;
301
+ }
302
+
303
+ function fullScope(doc: SpecDocument): Scope {
304
+ return { states: new Set(doc.states.keys()), transitions: new Set([...doc.transitions.values()].filter(runnable).map((t) => t.id)) };
305
+ }
306
+
307
+ function chartScope(doc: SpecDocument, chartIds: string[]): Scope {
308
+ const states = new Set(doc.charts.filter((c) => chartIds.includes(c.id)).flatMap((c) => c.states));
309
+ const transitions = new Set([...doc.transitions.values()].filter((t) => chartIds.includes(t.chartId) && runnable(t)).map((t) => t.id));
310
+ return { states, transitions };
311
+ }
312
+
313
+ function checkoutScope(doc: SpecDocument): Scope {
314
+ const scope = chartScope(doc, [CHECKOUT_CHART]);
315
+ for (const name of ENTRY_STATES) scope.states.add(name);
316
+ const host = doc.charts.find((c) => c.id === CHECKOUT_CHART)?.parent?.state;
317
+ if (host) scope.states.add(host);
318
+ for (const t of doc.transitions.values()) if (t.chartId === CHECKOUT_CHART && t.handOff && t.target) scope.states.add(t.target);
319
+ scope.transitions.add(ENTRY_TRANSITION);
320
+ return scope;
321
+ }
322
+
323
+ function ancestors(doc: SpecDocument, name: string) {
324
+ const out: string[] = [];
325
+ for (let p = doc.states.get(name)?.parent ?? null; p; p = doc.states.get(p)?.parent ?? null) out.push(p);
326
+ return out;
327
+ }
328
+
329
+ function isScreenLike(state: ParsedState) {
330
+ return (state.type === 'atomic' || state.type === 'final') && !state.childChartId;
331
+ }
332
+
333
+ function kindOf(doc: SpecDocument, t: ParsedTransition): EventKind {
334
+ const meta = doc.charts.find((c) => c.id === t.chartId)?.meta;
335
+ return resolveEventKind({
336
+ event: t.event,
337
+ handOff: t.handOff || Boolean(t.carriedBy),
338
+ specKind: meta?.eventKinds?.[t.event],
339
+ specTimeSpan: meta?.timeEvents?.[t.event],
340
+ runKind: undefined
341
+ }).kind;
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------------------------
345
+ // Timelines
346
+
347
+ const TYPED: [RegExp, string, string][] = [
348
+ [/^Submits the card$/, 'Card number', '4242 4242 4242 4242'],
349
+ [/^Submits the /, 'Details', 'Typed by the fixture'],
350
+ [/^Saves the /, 'Details', 'Updated by the fixture']
351
+ ];
352
+
353
+ const TIME_SPANS: [RegExp, number, string[]][] = [
354
+ [/longer than a minute/, 70_000, ['Payment status check runs']],
355
+ [/ten minutes/, 10 * 60_000, ['Pickup reminder is sent', 'Unclaimed order check runs']],
356
+ [/longer than a day/, 26 * 3_600_000, ['Daily approval check runs']]
357
+ ];
358
+
359
+ function imperative(verb: string) {
360
+ const v = verb.toLowerCase();
361
+ if (v.endsWith('ies')) return `${v.slice(0, -3)}y`;
362
+ if (/(sh|ch|ss|x|o)es$/.test(v)) return v.slice(0, -2);
363
+ if (v.endsWith('s')) return v.slice(0, -1);
364
+ return v;
365
+ }
366
+
367
+ function buttonLabel(event: string) {
368
+ const [verb = '', ...rest] = event.split(' ');
369
+ const words = verb === 'Taps' ? rest : [imperative(verb), ...rest];
370
+ return capitalize(words.slice(0, 5).join(' '));
371
+ }
372
+
373
+ function systemCall(event: string, seq: number) {
374
+ const id = String(seq).padStart(4, '0');
375
+ const payment: [RegExp, string][] = [
376
+ [/approves the payment/, 'payment.approved'],
377
+ [/declines the payment/, 'payment.declined']
378
+ ];
379
+ for (const [pattern, type] of payment) {
380
+ if (pattern.test(event)) {
381
+ return {
382
+ source: 'Payment provider webhook',
383
+ endpoint: 'POST /webhooks/payments',
384
+ eventType: type,
385
+ payload: { type, data: { paymentId: `pay_fixture_${id}`, status: type.split('.').pop() } },
386
+ responseStatus: 200
387
+ };
388
+ }
389
+ }
390
+ if (/^Barista /.test(event)) {
391
+ const type = /starts/.test(event) ? 'order.started' : 'order.finished';
392
+ return { source: 'Barista display', endpoint: `POST /api/orders/order-fixture-${id}/status`, eventType: type, payload: { status: type.split('.').pop() }, responseStatus: 200 };
393
+ }
394
+ return { source: 'App backend', endpoint: `POST /api/${slug(event)}`, payload: { requestId: `request-fixture-${id}` }, responseStatus: 201 };
395
+ }
396
+
397
+ function notificationFor(event: string, target: string | null): TimelineEntry['notification'] | null {
398
+ if (/^Barista finishes the drink/.test(event)) return { channel: 'push', title: 'Your drink is ready', body: 'Pick it up at the counter.' };
399
+ if (/declines the payment/.test(event)) return { channel: 'in-app', title: 'Payment declined', body: 'Try another way to pay.' };
400
+ if (/ten minutes/.test(event)) return { channel: 'email', title: 'Your order was refunded', body: 'The drink waited too long, so we refunded it.' };
401
+ if (/longer than a minute/.test(event)) return { channel: 'in-app', title: 'Still waiting on your payment', body: 'This can take a little longer.' };
402
+ if (/longer than a day/.test(event)) return { channel: 'email', title: 'Still under review', body: 'We will let you know when it is approved.' };
403
+ if (/ approves the /.test(event)) return { channel: 'push', title: 'Approved', body: target ? `Next: ${lowerFirst(target)}.` : 'Approved.' };
404
+ return null;
405
+ }
406
+
407
+ function timelineFor(t: ParsedTransition, kind: EventKind, seq: number, clock: string) {
408
+ const rand = seeded(t.id);
409
+ const entries: TimelineEntry[] = [];
410
+ const screen = (atMs: number) => ({ atMs, kind: 'screen' as const, label: `Now: ${t.target ?? 'unknown'}` });
411
+ let clip: ClipKind = 'system';
412
+ let how = '';
413
+ if (kind === 'user') {
414
+ const typed = TYPED.find(([pattern]) => pattern.test(t.event));
415
+ const x = 40 + Math.round(rand() * 310);
416
+ const y = 560 + Math.round(rand() * 220);
417
+ if (typed) {
418
+ clip = 'type';
419
+ entries.push({ atMs: 300, kind: 'tap', label: typed[1], x: 195, y: 300 + Math.round(rand() * 120) });
420
+ entries.push({ atMs: 650, kind: 'type', label: typed[1], text: typed[2] });
421
+ entries.push({ atMs: 1500, kind: 'tap', label: buttonLabel(t.event), x, y });
422
+ } else {
423
+ clip = 'tap';
424
+ entries.push({ atMs: 700, kind: 'tap', label: buttonLabel(t.event), x, y });
425
+ }
426
+ if (/^Removes/.test(t.event)) entries.push({ atMs: 1700, kind: 'toast', label: 'Removed from the cart.' });
427
+ entries.push(screen(2100));
428
+ how = `The user ${lowerFirst(t.event)} in the app.`;
429
+ } else if (kind === 'time') {
430
+ clip = 'time';
431
+ const [, span, fired] = TIME_SPANS.find(([pattern]) => pattern.test(t.event)) ?? [/./, 86_400_000, [t.event]];
432
+ const to = addMs(clock, span);
433
+ entries.push({
434
+ atMs: 300,
435
+ kind: 'time',
436
+ label: t.event,
437
+ time: { from: clock, to, fired: fired.map((label, i) => ({ at: addMs(clock, Math.round((span * (i + 1)) / fired.length)), label })) }
438
+ });
439
+ how = 'The test clock jumps forward; nothing in the app is touched.';
440
+ const note = notificationFor(t.event, t.target);
441
+ if (note) entries.push({ atMs: 1100, kind: 'notification', label: note.title ?? t.event, notification: note });
442
+ entries.push(screen(1900));
443
+ } else if (kind === 'hand-off') {
444
+ entries.push({ atMs: 200, kind: 'note', label: `${t.chartId} is done and hands back with "${t.event}"` });
445
+ entries.push(screen(700));
446
+ how = 'The child chart reached an end, so the parent chart moves on.';
447
+ } else {
448
+ const call = systemCall(t.event, seq);
449
+ entries.push({ atMs: 400, kind: 'note', label: 'The user waits on this screen' });
450
+ entries.push({ atMs: 900, kind: 'system', label: t.event, system: call });
451
+ const note = notificationFor(t.event, t.target);
452
+ if (note) entries.push({ atMs: 1300, kind: 'notification', label: note.title ?? t.event, notification: note });
453
+ entries.push(screen(1900));
454
+ how = `The fixture sends ${call.endpoint} as ${call.source}.`;
455
+ }
456
+ return { entries, clip, how };
457
+ }
458
+
459
+ /** A CloudEvent for a business event the run observed, as an event source would report it. */
460
+ function cloudEvent(name: string, at: string, subject: string): CloudEvent {
461
+ return {
462
+ specversion: '1.0',
463
+ id: hashOf({ name, at, subject }, 20),
464
+ source: '/fixture/orders',
465
+ type: `com.example.${slug(name).replace(/-/g, '.')}`,
466
+ time: at,
467
+ subject,
468
+ datacontenttype: 'application/json',
469
+ data: { name }
470
+ };
471
+ }
472
+
473
+ // ---------------------------------------------------------------------------------------------
474
+ // Runs
475
+
476
+ type MediaChoice = 'ready' | 'none' | 'processing';
477
+
478
+ type RunPlan = {
479
+ startedAt: string;
480
+ mode: 'fast' | 'showcase';
481
+ durationMs: number;
482
+ scope: Scope;
483
+ scopeInfo?: RunInfo['scope'];
484
+ journeys: 'all' | string[];
485
+ failing?: Record<string, string>;
486
+ flaky?: Record<string, string>;
487
+ reasons?: Record<string, string>;
488
+ generated?: boolean;
489
+ screenshot?: (name: string, index: number) => MediaChoice;
490
+ clip?: (id: string, index: number) => MediaChoice;
491
+ art: (state: ParsedState, tone: ScreenArt['tone']) => ScreenArt;
492
+ branch?: string;
493
+ mermaid?: MachineConfig;
494
+ };
495
+
496
+ type TransitionOut = TransitionRecord & { id: string; timeline: TimelineEntry[] };
497
+
498
+ type FixtureStep = { transition: string; event: string; status: RunStatus; reason: string | null };
499
+
500
+ type FixturePath = Omit<PathRecord, 'steps' | 'attempts'> & {
501
+ attempts: { status: string; error: string | null; stoppedAt: string | null; trace: string | null }[];
502
+ steps: FixtureStep[];
503
+ };
504
+
505
+ type Manifest = {
506
+ $schema: string;
507
+ schemaVersion: number;
508
+ run: RunInfo;
509
+ charts: { id: string; file: string; description: string; parentState: string | null; handOffs: Record<string, string> }[];
510
+ map: { nodes: Record<string, unknown>[]; edges: Record<string, unknown>[] };
511
+ states: Record<string, StateRecord>;
512
+ transitions: Record<string, TransitionOut>;
513
+ paths: FixturePath[];
514
+ coverage: Record<string, { states: Count; transitions: Count }>;
515
+ privacy: { passed: boolean; findings: string[] };
516
+ reports: { junit: string; ctrf: string; mermaid?: string };
517
+ };
518
+
519
+ const STATUS_RANK: Record<RunStatus, number> = { failed: 3, flaky: 2, passed: 1, 'not-reached': 0 };
520
+
521
+ function worst(statuses: RunStatus[]) {
522
+ return statuses.reduce<RunStatus>((w, s) => (STATUS_RANK[s] > STATUS_RANK[w] ? s : w), 'not-reached');
523
+ }
524
+
525
+ function baseState(state: ParsedState): StateRecord {
526
+ return {
527
+ name: state.name,
528
+ chart: state.chartId,
529
+ description: state.meta.description ?? '',
530
+ snapshot: state.meta.snapshot ?? slug(state.name),
531
+ confidence: state.meta.confidence ?? 'confirmed',
532
+ source: state.meta.source ?? [],
533
+ expectedEvents: state.meta.events ?? [],
534
+ status: 'not-reached',
535
+ screenshot: null,
536
+ screenshotsByPath: {},
537
+ recognizer: null,
538
+ looksLike: [],
539
+ businessEvents: null,
540
+ checks: [],
541
+ reachedBy: [],
542
+ notes: []
543
+ };
544
+ }
545
+
546
+ function screenCheck(state: ParsedState, passed: boolean): CheckResult {
547
+ return {
548
+ check: `The screen shows "${state.name}"`,
549
+ passed,
550
+ expected: `test ID "${state.meta.snapshot ?? slug(state.name)}" visible`,
551
+ actual: passed ? 'visible' : 'hidden; the app looks like none of the known screens'
552
+ };
553
+ }
554
+
555
+ function countOf(statuses: RunStatus[]): Count {
556
+ return {
557
+ total: statuses.length,
558
+ passed: statuses.filter((s) => s === 'passed').length,
559
+ failed: statuses.filter((s) => s === 'failed').length,
560
+ flaky: statuses.filter((s) => s === 'flaky').length,
561
+ notReached: statuses.filter((s) => s === 'not-reached').length
562
+ };
563
+ }
564
+
565
+ function buildRun(spec: LoadedSpec, runsDir: string, plan: RunPlan) {
566
+ const { doc } = spec;
567
+ const id = runIdOf(plan.startedAt, plan.mode);
568
+ const runDir = path.join(runsDir, id);
569
+ const w = runWriter(runDir);
570
+ const showcase = plan.mode === 'showcase';
571
+ const failing = plan.failing ?? {};
572
+ const flaky = plan.flaky ?? {};
573
+ const reachedT = new Map<string, { path: string; status: RunStatus }[]>();
574
+ const reachedS = new Map<string, string[]>();
575
+ const failedTargets = new Map<string, { path: string; error: string }>();
576
+ const reachState = (name: string, pathId: string) => {
577
+ if (!plan.scope.states.has(name)) return;
578
+ const list = reachedS.get(name) ?? [];
579
+ if (!list.includes(pathId)) list.push(pathId);
580
+ reachedS.set(name, list);
581
+ };
582
+ const reachTransition = (tid: string, pathId: string, status: RunStatus) => {
583
+ const list = reachedT.get(tid) ?? [];
584
+ list.push({ path: pathId, status });
585
+ reachedT.set(tid, list);
586
+ };
587
+
588
+ const wanted = plan.journeys === 'all' ? null : new Set(plan.journeys);
589
+ for (const name of wanted ?? []) if (!doc.journeys.some((j) => j.name === name)) throw new Error(`No journey called "${name}"`);
590
+ const paths: FixturePath[] = [];
591
+ doc.journeys.forEach((journey, index) => {
592
+ if (wanted && !wanted.has(journey.name)) return;
593
+ const pathId = `journey-${index + 1}`;
594
+ const replay = spec.replays.get(journey.id)!;
595
+ const steps: FixtureStep[] = [];
596
+ let status: RunStatus = 'passed';
597
+ let stoppedAt: string | null = null;
598
+ let error: string | null = null;
599
+ let flakyError: string | null = null;
600
+ let started = false;
601
+ walk: for (const step of replay.steps) {
602
+ for (const tid of step.transitionIds) {
603
+ if (!plan.scope.transitions.has(tid)) {
604
+ if (started) break walk;
605
+ continue;
606
+ }
607
+ const t = doc.transitions.get(tid)!;
608
+ if (!started) for (const name of step.from) reachState(name, pathId);
609
+ started = true;
610
+ if (stoppedAt) {
611
+ steps.push({ transition: tid, event: t.event, status: 'not-reached', reason: `Not reached: the path stopped at "${stoppedAt}".` });
612
+ continue;
613
+ }
614
+ if (failing[tid]) {
615
+ status = 'failed';
616
+ stoppedAt = t.target ?? t.source;
617
+ error = failing[tid]!;
618
+ steps.push({ transition: tid, event: t.event, status: 'failed', reason: error });
619
+ reachTransition(tid, pathId, 'failed');
620
+ if (t.target && !failedTargets.has(t.target)) failedTargets.set(t.target, { path: pathId, error });
621
+ continue;
622
+ }
623
+ const stepStatus: RunStatus = flaky[tid] ? 'flaky' : 'passed';
624
+ if (stepStatus === 'flaky' && status === 'passed') {
625
+ status = 'flaky';
626
+ flakyError = flaky[tid]!;
627
+ }
628
+ steps.push({ transition: tid, event: t.event, status: stepStatus, reason: stepStatus === 'flaky' ? flaky[tid]! : null });
629
+ reachTransition(tid, pathId, stepStatus);
630
+ for (const name of step.to) reachState(name, pathId);
631
+ }
632
+ }
633
+ if (steps.length === 0) return;
634
+ const attempts =
635
+ status === 'failed'
636
+ ? [
637
+ { status: 'failed', error, stoppedAt, trace: null },
638
+ { status: 'failed', error, stoppedAt, trace: null }
639
+ ]
640
+ : status === 'flaky'
641
+ ? [
642
+ { status: 'failed', error: flakyError, stoppedAt: null, trace: null },
643
+ { status: 'passed', error: null, stoppedAt: null, trace: null }
644
+ ]
645
+ : [{ status: 'passed', error: null, stoppedAt: null, trace: null }];
646
+ paths.push({ id: pathId, name: journey.name, kind: 'journey', description: journey.description, status, attempts, steps, stoppedAt });
647
+ });
648
+
649
+ if (plan.generated) {
650
+ const byChart = new Map<string, ParsedTransition[]>();
651
+ for (const t of doc.transitions.values()) {
652
+ if (!plan.scope.transitions.has(t.id) || reachedT.has(t.id)) continue;
653
+ byChart.set(t.chartId, [...(byChart.get(t.chartId) ?? []), t]);
654
+ }
655
+ let n = 0;
656
+ for (const [chartId, list] of byChart) {
657
+ const pathId = `generated-${++n}`;
658
+ for (const t of list) {
659
+ reachTransition(t.id, pathId, 'passed');
660
+ for (const name of [t.source, ...(t.target ? [t.target, ...ancestors(doc, t.target)] : [])]) reachState(name, pathId);
661
+ }
662
+ paths.push({
663
+ id: pathId,
664
+ name: `Every other event in ${chartId}`,
665
+ kind: 'generated',
666
+ description: `Generated to reach the ${list.length} events in "${chartId}" that no named journey takes.`,
667
+ status: 'passed',
668
+ attempts: [{ status: 'passed', error: null, stoppedAt: null, trace: null }],
669
+ steps: list.map((t) => ({ transition: t.id, event: t.event, status: 'passed', reason: null })),
670
+ stoppedAt: null
671
+ });
672
+ }
673
+ for (const name of plan.scope.states) if (!reachedS.has(name)) reachState(name, paths.find((p) => p.kind === 'generated')?.id ?? 'generated-1');
674
+ }
675
+
676
+ const states: Record<string, StateRecord> = {};
677
+ let screenIndex = 0;
678
+ for (const state of doc.states.values()) {
679
+ if (!plan.scope.states.has(state.name)) continue;
680
+ const rec = baseState(state);
681
+ const by = reachedS.get(state.name);
682
+ const failed = failedTargets.get(state.name);
683
+ if (failed) {
684
+ rec.status = 'failed';
685
+ rec.recognizer = { matched: false, signals: [{ signal: `test ID "${rec.snapshot}"`, expected: 'visible', visible: false }] };
686
+ rec.checks = [screenCheck(state, false)];
687
+ if (showcase && isScreenLike(state)) {
688
+ rec.screenshotsByPath = { [`${failed.path} (failed)`]: w.screen(failed.path, plan.art(state, 'error'), { prefix: 'failed-' }) };
689
+ }
690
+ } else if (by) {
691
+ rec.status = 'passed';
692
+ rec.reachedBy = by;
693
+ rec.recognizer = { matched: true, signals: [{ signal: `test ID "${rec.snapshot}"`, expected: 'visible', visible: true }] };
694
+ if (isScreenLike(state)) rec.checks = [screenCheck(state, true)];
695
+ if (rec.expectedEvents?.length) {
696
+ const at = addMs(plan.startedAt, 60_000 * (screenIndex + 1));
697
+ rec.businessEvents = {
698
+ expected: rec.expectedEvents,
699
+ observed: rec.expectedEvents,
700
+ missing: [],
701
+ noSignal: [],
702
+ events: rec.expectedEvents.map((name) => cloudEvent(name, at, `order-fixture-${hashOf(id, 6)}`))
703
+ };
704
+ }
705
+ if (showcase && isScreenLike(state)) {
706
+ const choice = plan.screenshot?.(state.name, screenIndex++) ?? 'ready';
707
+ if (choice === 'ready') {
708
+ const image = w.screen(by[0]!, plan.art(state, 'normal'));
709
+ rec.screenshot = image;
710
+ rec.screenshotsByPath = { [by[0]!]: image };
711
+ } else if (choice === 'processing') {
712
+ rec.screenshot = w.screen(by[0]!, plan.art(state, 'normal'), { write: false });
713
+ rec.screenshotState = 'processing';
714
+ } else {
715
+ rec.notes = ['Shown too briefly to capture; not seeing it is not a failure.'];
716
+ }
717
+ }
718
+ }
719
+ states[state.name] = rec;
720
+ }
721
+
722
+ const transitions: Record<string, TransitionOut> = {};
723
+ let clipIndex = 0;
724
+ let seq = 0;
725
+ for (const t of doc.transitions.values()) {
726
+ if (!plan.scope.transitions.has(t.id)) continue;
727
+ const kind = kindOf(doc, t);
728
+ const hits = reachedT.get(t.id);
729
+ const rec: TransitionOut = {
730
+ id: t.id,
731
+ source: t.source,
732
+ target: t.target,
733
+ event: t.event,
734
+ kind,
735
+ how: null,
736
+ status: 'not-reached',
737
+ clip: null,
738
+ timeline: [],
739
+ reason: plan.reasons?.[t.id] ?? 'No path reached it.',
740
+ reachedBy: []
741
+ };
742
+ if (hits?.length) {
743
+ const status = worst(hits.map((h) => h.status));
744
+ const clock = addMs(plan.startedAt, seq * 3_600_000);
745
+ const timeline = timelineFor(t, kind, ++seq, clock);
746
+ rec.status = status;
747
+ rec.reason = status === 'failed' ? (failing[t.id] ?? null) : status === 'flaky' ? (flaky[t.id] ?? null) : null;
748
+ rec.reachedBy = [...new Set(hits.map((h) => h.path))];
749
+ rec.how = timeline.how;
750
+ rec.timeline = timeline.entries;
751
+ if (showcase) {
752
+ const choice = plan.clip?.(t.id, clipIndex++) ?? 'ready';
753
+ rec.clip = choice === 'ready' ? w.clip(timeline.clip, timeline.entries) : choice === 'processing' ? w.pendingClip() : null;
754
+ }
755
+ }
756
+ transitions[t.id] = rec;
757
+ }
758
+
759
+ const coverage: Manifest['coverage'] = {};
760
+ for (const chart of doc.charts) {
761
+ const s = Object.values(states).filter((r) => r.chart === chart.id).map((r) => r.status);
762
+ const tr = Object.values(transitions).filter((r) => doc.transitions.get(r.id)?.chartId === chart.id).map((r) => r.status);
763
+ if (s.length || tr.length) coverage[chart.id] = { states: countOf(s), transitions: countOf(tr) };
764
+ }
765
+ coverage.overall = { states: countOf(Object.values(states).map((r) => r.status)), transitions: countOf(Object.values(transitions).map((r) => r.status)) };
766
+
767
+ const manifest: Manifest = {
768
+ $schema: 'https://github.com/crvouga/atlas/schemas/manifest.schema.json',
769
+ schemaVersion: 1,
770
+ run: {
771
+ id,
772
+ startedAt: plan.startedAt,
773
+ finishedAt: addMs(plan.startedAt, plan.durationMs),
774
+ durationMs: plan.durationMs,
775
+ commit: hashOf(id, 40),
776
+ branch: plan.branch ?? 'main',
777
+ environment: { driver: 'playwright', baseUrl: 'http://localhost:3000', viewport: { width: 390, height: 844 }, deviceScaleFactor: 3 },
778
+ mode: plan.mode,
779
+ specVersion: spec.version,
780
+ specDirectory: spec.specDirectory,
781
+ approvals: [],
782
+ ...(plan.scopeInfo ? { scope: plan.scopeInfo } : {})
783
+ },
784
+ charts: doc.charts.map((c) => ({
785
+ id: c.id,
786
+ file: c.file,
787
+ description: c.meta.description ?? '',
788
+ parentState: c.parent?.state ?? null,
789
+ handOffs: Object.fromEntries([...doc.transitions.values()].filter((t) => t.chartId === c.id && t.handOff).map((t) => [t.source, t.event]))
790
+ })),
791
+ map: {
792
+ nodes: [...doc.states.values()].map((s) => ({ id: s.name, parent: s.parent, chart: s.chartId, type: s.type, initial: s.initial, inScope: plan.scope.states.has(s.name) })),
793
+ edges: [...doc.transitions.values()]
794
+ .filter((t) => runnable(t) && t.target)
795
+ .map((t) => ({ id: t.id, source: t.source, target: t.target, event: t.event, kind: kindOf(doc, t), inScope: plan.scope.transitions.has(t.id) }))
796
+ },
797
+ states,
798
+ transitions,
799
+ paths,
800
+ coverage,
801
+ privacy: { passed: true, findings: [] },
802
+ reports: { junit: 'junit.xml', ctrf: 'ctrf.json', ...(plan.mermaid ? { mermaid: 'chart.mmd' } : {}) }
803
+ };
804
+ return { id, runDir, writer: w, manifest, mermaid: plan.mermaid };
805
+ }
806
+
807
+ function writeManifest(runDir: string, manifest: Manifest | Record<string, unknown>, mermaid?: MachineConfig) {
808
+ writeText(path.join(runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
809
+ if ('paths' in manifest && 'run' in manifest && manifest.run) {
810
+ const m = manifest as Manifest;
811
+ writeText(path.join(runDir, 'junit.xml'), toJUnit(m));
812
+ writeText(path.join(runDir, 'ctrf.json'), `${JSON.stringify(toCtrf(m), null, 2)}\n`);
813
+ }
814
+ if (mermaid) writeText(path.join(runDir, 'chart.mmd'), toMermaid(mermaid));
815
+ }
816
+
817
+ const CHART_COLORS: Record<string, string> = { [TOP_CHART]: '#2F6FED', [CHECKOUT_CHART]: '#0F9D74' };
818
+
819
+ function exampleArt(state: ParsedState, tone: ScreenArt['tone']): ScreenArt {
820
+ return { title: state.name, subtitle: state.chartId, color: CHART_COLORS[state.chartId] ?? '#475467', tone };
821
+ }
822
+
823
+ function resetFixture(name: string) {
824
+ const dir = path.join(FIXTURES_ROOT, name);
825
+ rmSync(dir, { recursive: true, force: true });
826
+ mkdirSync(path.join(dir, 'runs'), { recursive: true });
827
+ return { dir, runs: path.join(dir, 'runs') };
828
+ }
829
+
830
+ function checkoutRun(spec: LoadedSpec, runsDir: string, plan: Omit<RunPlan, 'scope' | 'art' | 'scopeInfo'>) {
831
+ return buildRun(spec, runsDir, {
832
+ ...plan,
833
+ scope: checkoutScope(spec.doc),
834
+ scopeInfo: { chart: CHECKOUT_CHART, start: 'A customer with a drink in the cart' },
835
+ art: exampleArt
836
+ });
837
+ }
838
+
839
+ // ---------------------------------------------------------------------------------------------
840
+ // Example fixtures
841
+
842
+ function makeSpecOnly(spec: LoadedSpec) {
843
+ const f = resetFixture('spec-only');
844
+ writeSpec(f.dir, spec.files);
845
+ writeText(path.join(f.runs, '.gitkeep'), '');
846
+ }
847
+
848
+ function makePartial(spec: LoadedSpec, mermaid: MachineConfig) {
849
+ const f = resetFixture('partial');
850
+ writeSpec(f.dir, spec.files);
851
+ const run = checkoutRun(spec, f.runs, {
852
+ startedAt: '2026-10-06T14:40:00.000Z',
853
+ mode: 'showcase',
854
+ durationMs: 212_400,
855
+ journeys: ['Orders a drink and pays by card', 'Pays with a wallet', 'Card declined, then a wallet works', 'Leaves checkout and comes back'],
856
+ screenshot: (name, i) => (name === 'Waiting for the payment' ? 'processing' : i % 4 === 3 ? 'none' : 'ready'),
857
+ clip: (_id, i) => (i === 2 ? 'processing' : i % 2 === 1 ? 'none' : 'ready'),
858
+ reasons: { 'Waiting for the payment :: Payment takes longer than a minute': 'Needs a payment provider stand-in that can answer slowly; not wired yet.' },
859
+ mermaid
860
+ });
861
+ writeManifest(run.runDir, run.manifest, run.mermaid);
862
+
863
+ const running = runWriter(path.join(f.runs, runIdOf('2026-10-06T15:05:00.000Z', 'showcase')));
864
+ const cart = spec.doc.states.get('Reviewing the cart')!;
865
+ const paying = spec.doc.states.get('Choosing how to pay')!;
866
+ running.screen('journey-1', exampleArt(cart, 'normal'), { prefix: '00-start-' });
867
+ running.screen('journey-1', exampleArt(paying, 'normal'));
868
+ }
869
+
870
+ function makeFull(spec: LoadedSpec, mermaid: MachineConfig) {
871
+ const f = resetFixture('full');
872
+ writeSpec(f.dir, spec.files);
873
+ const fast = checkoutRun(spec, f.runs, {
874
+ startedAt: '2026-10-02T09:10:00.000Z',
875
+ mode: 'fast',
876
+ durationMs: 48_200,
877
+ journeys: 'all',
878
+ failing: { 'Confirming with the wallet :: Confirms with the wallet': 'Expected "Waiting for the payment", but the wallet sheet is still open' },
879
+ mermaid
880
+ });
881
+ writeManifest(fast.runDir, fast.manifest, fast.mermaid);
882
+ const older = checkoutRun(spec, f.runs, {
883
+ startedAt: '2026-10-04T16:20:00.000Z',
884
+ mode: 'showcase',
885
+ durationMs: 402_900,
886
+ journeys: 'all',
887
+ flaky: { 'Told the payment was declined :: Tries another way to pay': 'The button responded only on the second tap.' },
888
+ screenshot: (_name, i) => (i < 4 ? 'ready' : 'none'),
889
+ clip: (_id, i) => (i < 6 ? 'ready' : 'none'),
890
+ mermaid
891
+ });
892
+ writeManifest(older.runDir, older.manifest, older.mermaid);
893
+ const newest = buildRun(spec, f.runs, {
894
+ startedAt: '2026-10-06T14:40:00.000Z',
895
+ mode: 'showcase',
896
+ durationMs: 684_600,
897
+ scope: fullScope(spec.doc),
898
+ scopeInfo: { start: 'A customer on the menu' },
899
+ journeys: 'all',
900
+ generated: true,
901
+ art: exampleArt,
902
+ mermaid
903
+ });
904
+ writeManifest(newest.runDir, newest.manifest, newest.mermaid);
905
+ }
906
+
907
+ function makeFailures(spec: LoadedSpec, mermaid: MachineConfig) {
908
+ const f = resetFixture('failures');
909
+ writeSpec(f.dir, spec.files);
910
+ const { doc } = spec;
911
+ const run = checkoutRun(spec, f.runs, {
912
+ startedAt: '2026-10-06T14:40:00.000Z',
913
+ mode: 'showcase',
914
+ durationMs: 455_000,
915
+ journeys: 'all',
916
+ failing: {
917
+ 'Waiting for the payment :: Payment provider declines the payment': 'Expected "Told the payment was declined", the app looks like none of the known screens'
918
+ },
919
+ flaky: {
920
+ 'Choosing how to pay :: Picks wallet': 'Timed out waiting for "Confirming with the wallet" on the first attempt',
921
+ 'Waiting for the payment :: Payment takes longer than a minute': 'The clock jump landed before the payment check started; passed on retry'
922
+ },
923
+ reasons: {
924
+ 'Confirming with the wallet :: Cancels the wallet': 'The wallet stand-in cannot be cancelled yet.',
925
+ 'Entering card details :: Goes back to the payment options': 'No journey goes back from the card form, and generated paths are off in this run.'
926
+ },
927
+ mermaid
928
+ });
929
+ const m = run.manifest;
930
+ const subject = 'order-fixture-failures';
931
+ const slow = m.states['Told the payment is slow']!;
932
+ slow.status = 'flaky';
933
+ slow.notes = ['Passed on the second attempt: the first attempt saw the spinner still running.'];
934
+ const waiting = m.states['Waiting for the payment']!;
935
+ waiting.businessEvents = { expected: waiting.expectedEvents ?? [], observed: [], missing: waiting.expectedEvents ?? [], noSignal: [], events: [] };
936
+ const paid = m.states['Paid']!;
937
+ const paidState = doc.states.get('Paid')!;
938
+ paid.status = 'failed';
939
+ paid.businessEvents = {
940
+ expected: [...(paid.expectedEvents ?? []), 'Customer notified'],
941
+ observed: paid.expectedEvents ?? [],
942
+ missing: ['Customer notified'],
943
+ noSignal: [],
944
+ events: (paid.expectedEvents ?? []).map((name) => cloudEvent(name, '2026-10-06T14:45:00.000Z', subject))
945
+ };
946
+ paid.checks = [
947
+ screenCheck(paidState, true),
948
+ { check: 'Business event "Customer notified" is recorded', passed: false, expected: 'recorded within 10 seconds', actual: 'not recorded' }
949
+ ];
950
+ const confirm = m.transitions['Confirming with the wallet :: Confirms with the wallet']!;
951
+ confirm.status = 'flaky';
952
+ confirm.reason = 'The wallet sheet answered slowly once; passed on retry.';
953
+ confirm.clip = run.writer.clip('tap', confirm.timeline);
954
+ writeManifest(run.runDir, m, run.mermaid);
955
+ }
956
+
957
+ function makeRemoved(spec: LoadedSpec) {
958
+ const f = resetFixture('removed');
959
+ writeSpec(f.dir, spec.files);
960
+ const run = checkoutRun(spec, f.runs, {
961
+ startedAt: '2026-10-06T14:40:00.000Z',
962
+ mode: 'showcase',
963
+ durationMs: 133_000,
964
+ journeys: ['Orders a drink and pays by card', 'Pays with a wallet'],
965
+ screenshot: (_name, i) => (i < 4 ? 'ready' : 'none'),
966
+ clip: (_id, i) => (i < 4 ? 'ready' : 'none')
967
+ });
968
+ const m = run.manifest;
969
+ const gone = (name: string, status: RunStatus, description: string): StateRecord => ({
970
+ name,
971
+ chart: CHECKOUT_CHART,
972
+ description,
973
+ snapshot: slug(name),
974
+ confidence: 'confirmed',
975
+ source: [],
976
+ expectedEvents: [],
977
+ status,
978
+ screenshot: status === 'passed' ? run.writer.screen('journey-1', { title: name, subtitle: CHECKOUT_CHART, color: '#98A2B3', tone: 'normal' }) : null,
979
+ screenshotsByPath: {},
980
+ recognizer: null,
981
+ looksLike: [],
982
+ businessEvents: null,
983
+ checks: [],
984
+ reachedBy: status === 'passed' ? ['journey-1'] : [],
985
+ notes: []
986
+ });
987
+ m.states['Choosing a tip'] = gone('Choosing a tip', 'passed', 'The customer picks a tip before paying (removed from the spec since this run).');
988
+ m.states['Told the card reader is offline'] = gone('Told the card reader is offline', 'failed', 'A sheet said the card reader was offline (removed from the spec).');
989
+ const extra: [string, string, string | null, RunStatus][] = [
990
+ ['Choosing a tip', 'Picks no tip', 'Choosing how to pay', 'passed'],
991
+ ['Entering card details', 'Scans the card', 'Waiting for the payment', 'passed'],
992
+ ['Told the card reader is offline', 'Closes the message', 'Choosing how to pay', 'not-reached']
993
+ ];
994
+ for (const [source, event, target, status] of extra) {
995
+ const id = `${source} :: ${event}`;
996
+ m.transitions[id] = { id, source, target, event, kind: 'user', how: null, status, clip: null, timeline: [], reason: status === 'passed' ? null : 'No path reached it.', reachedBy: status === 'passed' ? ['journey-1'] : [] };
997
+ }
998
+ writeManifest(run.runDir, m);
999
+ }
1000
+
1001
+ function plant(text: string, anchor: string, from: string, to: string) {
1002
+ const at = anchor ? text.indexOf(anchor) : 0;
1003
+ if (at < 0) throw new Error(`Anchor not found: ${anchor}`);
1004
+ const i = text.indexOf(from, at);
1005
+ if (i < 0) throw new Error(`Text not found after ${anchor}: ${from}`);
1006
+ return text.slice(0, i) + to + text.slice(i + from.length);
1007
+ }
1008
+
1009
+ function makeMalformed(spec: LoadedSpec) {
1010
+ const f = resetFixture('malformed');
1011
+ const files = spec.files.map((file) => ({ ...file }));
1012
+ const byName = (name: string) => files.find((file) => file.path.endsWith(`/${name}`))!;
1013
+ const top = byName('machine.yaml');
1014
+ top.text = plant(top.text, 'snapshot: order-making', 'confidence: assumed', 'confidence: maybe');
1015
+ top.text = plant(top.text, 'snapshot: order-expired', " Orders again:\n target: '#Browsing the menu'\n", ' Orders again:\n');
1016
+ top.text = plant(top.text, 'snapshot: order-complete', ' events:\n - Order completed\n', ' events: Order completed\n');
1017
+ const checkout = byName('checkout.scxml');
1018
+ checkout.text = plant(checkout.text, 'atlas:name="Tries another way to pay"', 'target="choosing-how-to-pay"', 'target="choosing-a-tip"');
1019
+ const journeys = byName('journeys.yaml');
1020
+ journeys.text = plant(journeys.text, '\n Slow payment:\n', ' - Submits the card\n', ' - Submits the card: twice: over\n');
1021
+ journeys.text = `${journeys.text.trimEnd()}\n Pays before ordering:\n description: A planted journey whose second event can't happen at that point.\n events:\n - Picks a drink\n - Submits the card\n`;
1022
+ files.push({ path: `${SPEC_DIR}/loyalty.scxml`, text: '<?xml version="1.0" encoding="UTF-8"?>\n<statechart name="Loyalty">\n <state id="collecting-stamps"/>\n</statechart>\n' });
1023
+ writeSpec(f.dir, files);
1024
+
1025
+ const base = (startedAt: string) =>
1026
+ checkoutRun(spec, f.runs, { startedAt, mode: 'showcase', durationMs: 120_000, journeys: ['Orders a drink and pays by card'], screenshot: () => 'none', clip: () => 'none' });
1027
+
1028
+ const notJson = base('2026-10-06T10:00:00.000Z');
1029
+ writeText(path.join(notJson.runDir, 'manifest.json'), JSON.stringify(notJson.manifest, null, 2).slice(0, 1800));
1030
+
1031
+ const newer = base('2026-10-06T11:00:00.000Z');
1032
+ const step = Object.values(newer.manifest.transitions).find((t) => t.timeline.length > 0)!;
1033
+ writeManifest(newer.runDir, {
1034
+ ...newer.manifest,
1035
+ schemaVersion: 2,
1036
+ replays: { format: 'rrweb', files: ['media/replays/journey-1.json'] },
1037
+ transitions: {
1038
+ ...newer.manifest.transitions,
1039
+ [step.id]: { ...step, timeline: [...step.timeline, { atMs: 1200, kind: 'haptic', label: 'The phone buzzes', pattern: 'success' }] }
1040
+ }
1041
+ });
1042
+
1043
+ const invalid = base('2026-10-06T12:00:00.000Z');
1044
+ const names = Object.keys(invalid.manifest.states);
1045
+ writeManifest(invalid.runDir, {
1046
+ ...invalid.manifest,
1047
+ states: {
1048
+ ...invalid.manifest.states,
1049
+ [names[1]!]: { ...invalid.manifest.states[names[1]!], status: 'great' },
1050
+ [names[2]!]: { ...invalid.manifest.states[names[2]!], screenshot: 42 },
1051
+ [names[3]!]: 'passed'
1052
+ }
1053
+ });
1054
+
1055
+ const noRun = base('2026-10-06T13:00:00.000Z');
1056
+ const { run: _run, ...rest } = noRun.manifest;
1057
+ writeManifest(noRun.runDir, rest);
1058
+ }
1059
+
1060
+ // ---------------------------------------------------------------------------------------------
1061
+ // Large atlas
1062
+
1063
+ type Node = {
1064
+ id?: string;
1065
+ initial?: string;
1066
+ type?: 'parallel' | 'final';
1067
+ meta?: Record<string, unknown>;
1068
+ states?: Record<string, Node>;
1069
+ on?: Record<string, { target: string }>;
1070
+ invoke?: { src: string; id: string };
1071
+ };
1072
+
1073
+ /** How a context composes its child chart: a standard invoke (YAML or SCXML child) or the legacy meta. */
1074
+ type Composition = 'invoke' | 'invoke-scxml' | 'legacy';
1075
+
1076
+ type LargeContext = {
1077
+ id: string;
1078
+ name: string;
1079
+ description: string;
1080
+ owner: string;
1081
+ dir: string;
1082
+ chart: string;
1083
+ child: string;
1084
+ approver: string;
1085
+ objects: string[];
1086
+ childObjects: string[];
1087
+ linkAfter: number;
1088
+ color: string;
1089
+ composition: Composition;
1090
+ };
1091
+
1092
+ const LARGE_CONTEXTS: LargeContext[] = [
1093
+ {
1094
+ id: 'accounts', name: 'Accounts', description: 'Signing up and keeping a profile up to date.', owner: 'Accounts team',
1095
+ dir: 'accounts/sign-up', chart: 'Sign-up', child: 'Identity check', approver: 'Identity service',
1096
+ objects: ['account details', 'home address', 'date of birth', 'profile photo', 'notification preferences', 'security questions'],
1097
+ childObjects: ['photo ID', 'selfie match', 'address proof'], linkAfter: 2, color: '#2F6FED', composition: 'invoke'
1098
+ },
1099
+ {
1100
+ id: 'catalog', name: 'Catalog', description: 'Finding and comparing products.', owner: 'Catalog team',
1101
+ dir: 'catalog/browsing', chart: 'Product browsing', child: 'Product comparison', approver: 'Catalog service',
1102
+ objects: ['search filters', 'product list', 'product details', 'saved list', 'price alert', 'review draft'],
1103
+ childObjects: ['comparison list', 'comparison table', 'comparison summary'], linkAfter: 1, color: '#7A5AF8', composition: 'legacy'
1104
+ },
1105
+ {
1106
+ id: 'orders', name: 'Orders', description: 'Carts, checkout and order history.', owner: 'Orders team',
1107
+ dir: 'orders/checkout', chart: 'Order checkout', child: 'Payment', approver: 'Payment processor',
1108
+ objects: ['shopping cart', 'delivery address', 'payment method', 'promo code', 'gift message'],
1109
+ childObjects: ['payment details', 'payment authorization', 'payment receipt'], linkAfter: 1, color: '#0F9D74', composition: 'invoke-scxml'
1110
+ },
1111
+ {
1112
+ id: 'shipping', name: 'Shipping', description: 'Packing, shipping and delivery.', owner: 'Fulfillment team',
1113
+ dir: 'shipping/deliveries', chart: 'Deliveries', child: 'Delivery scheduling', approver: 'Carrier',
1114
+ objects: ['shipping label', 'package contents', 'pickup window', 'tracking number', 'delivery instructions', 'return label'],
1115
+ childObjects: ['delivery slot', 'delivery reminder', 'delivery confirmation'], linkAfter: 0, color: '#DD2590', composition: 'invoke'
1116
+ },
1117
+ {
1118
+ id: 'support', name: 'Support', description: 'Help requests, answers and escalations.', owner: 'Support team',
1119
+ dir: 'support/tickets', chart: 'Support tickets', child: 'Escalation', approver: 'Support desk',
1120
+ objects: ['help request', 'request details', 'attached screenshot', 'reply draft', 'satisfaction rating'],
1121
+ childObjects: ['escalation note', 'specialist review', 'escalation answer'], linkAfter: 2, color: '#DC6803', composition: 'legacy'
1122
+ },
1123
+ {
1124
+ id: 'billing', name: 'Billing', description: 'Invoices, receipts, refunds and credits.', owner: 'Billing team',
1125
+ dir: 'billing/invoices', chart: 'Invoices and payments', child: 'Refund handling', approver: 'Billing service',
1126
+ objects: ['monthly invoice', 'paid invoice', 'tax document', 'payment plan', 'billing dispute', 'account credit'],
1127
+ childObjects: ['refund request', 'refund approval', 'refund transfer'], linkAfter: 3, color: '#475467', composition: 'invoke-scxml'
1128
+ }
1129
+ ];
1130
+
1131
+ const go = (name: string) => ({ target: `#${name}` });
1132
+
1133
+ function stateMeta(name: string, description: string, extra: Record<string, unknown> = {}) {
1134
+ return { description, snapshot: slug(name), events: [], confidence: 'confirmed', source: [`src/screens/${slug(name)}.tsx`], ...extra };
1135
+ }
1136
+
1137
+ type StageNames = { group: string; reviewing: string; approved: string };
1138
+
1139
+ function stageNames(o: string): StageNames {
1140
+ return { group: `Handling the ${o}`, reviewing: `Reviewing the ${o}`, approved: `Told the ${o} was approved` };
1141
+ }
1142
+
1143
+ function stage(ctx: LargeContext, o: string, next: { event: string; target: string }, options: { nested: boolean; extraFirst?: Record<string, { target: string }> }): Node {
1144
+ const names = stageNames(o);
1145
+ const editing = `Editing the ${o}`;
1146
+ const details = `Editing the ${o} details`;
1147
+ const uploading = `Uploading a file for the ${o}`;
1148
+ const saveFailed = `Told the ${o} could not be saved`;
1149
+ const waiting = `Waiting for the ${o} to be approved`;
1150
+ const slow = `Told the ${o} is still under review`;
1151
+ const rejected = `Told the ${o} was rejected`;
1152
+ const editingOn = {
1153
+ [`Saves the ${o}`]: go(names.reviewing),
1154
+ [`Cancels the changes to the ${o}`]: go(names.reviewing),
1155
+ [`Saving the ${o} fails`]: go(saveFailed)
1156
+ };
1157
+ const editingNode: Node = options.nested
1158
+ ? {
1159
+ id: editing,
1160
+ initial: details,
1161
+ meta: stateMeta(editing, `The user changes the ${o}, with an optional file.`),
1162
+ states: {
1163
+ [details]: { id: details, meta: stateMeta(details, `A form with the ${o} fields.`, { hints: ['A text field per detail', 'An "Upload a file" link'] }), on: { 'Uploads a file': go(uploading) } },
1164
+ [uploading]: { id: uploading, meta: stateMeta(uploading, `A file picker for a document about the ${o}.`), on: { 'Finishes uploading the file': go(details) } }
1165
+ },
1166
+ on: editingOn
1167
+ }
1168
+ : { id: editing, meta: stateMeta(editing, `A form to change the ${o}.`), on: editingOn };
1169
+ return {
1170
+ id: names.group,
1171
+ initial: names.reviewing,
1172
+ meta: stateMeta(names.group, `Everything about the ${o}, from review to approval.`, { owner: ctx.owner }),
1173
+ states: {
1174
+ [names.reviewing]: {
1175
+ id: names.reviewing,
1176
+ meta: stateMeta(names.reviewing, `The user checks the ${o} before sending it.`, { gherkin: [`features/${ctx.dir}/${slug(o)}.feature`] }),
1177
+ on: { [`Taps edit on the ${o}`]: go(editing), [`Submits the ${o}`]: go(waiting), ...options.extraFirst }
1178
+ },
1179
+ [editing]: editingNode,
1180
+ [saveFailed]: { id: saveFailed, meta: stateMeta(saveFailed, `A message says the ${o} could not be saved, with "Try again".`), on: { [`Tries saving the ${o} again`]: go(editing) } },
1181
+ [waiting]: {
1182
+ id: waiting,
1183
+ meta: stateMeta(waiting, `A card says the ${o} is being checked.`, { events: [`${capitalize(o)} submitted`], tier: 'critical' }),
1184
+ on: {
1185
+ [`${ctx.approver} approves the ${o}`]: go(names.approved),
1186
+ [`${ctx.approver} rejects the ${o}`]: go(rejected),
1187
+ [`Approval of the ${o} takes longer than a day`]: go(slow)
1188
+ }
1189
+ },
1190
+ [slow]: { id: slow, meta: stateMeta(slow, `A card says the ${o} is still being checked.`, { confidence: 'assumed' }), on: { [`Checks the ${o} again`]: go(waiting) } },
1191
+ [rejected]: { id: rejected, meta: stateMeta(rejected, `A message explains why the ${o} was not accepted.`), on: { [`Starts the ${o} again`]: go(names.reviewing) } },
1192
+ [names.approved]: {
1193
+ id: names.approved,
1194
+ meta: stateMeta(names.approved, `A message confirms the ${o} is approved.`, { events: [`${capitalize(o)} approved`] }),
1195
+ on: { [next.event]: go(next.target) }
1196
+ }
1197
+ }
1198
+ };
1199
+ }
1200
+
1201
+ function settingsNames(ctx: LargeContext) {
1202
+ const c = ctx.chart.toLowerCase();
1203
+ return {
1204
+ settings: `${ctx.chart} settings`,
1205
+ notifications: `${ctx.chart} notifications`,
1206
+ on: `${ctx.chart} notifications on`,
1207
+ off: `${ctx.chart} notifications off`,
1208
+ support: `${ctx.chart} help`,
1209
+ none: `No open ${c} help request`,
1210
+ open: `${ctx.chart} help request open`,
1211
+ answered: `${ctx.chart} help request answered`,
1212
+ finished: `${ctx.chart} finished`,
1213
+ closeEvent: `Closes the ${c} settings`,
1214
+ offEvent: `Turns off ${c} notifications`,
1215
+ onEvent: `Turns on ${c} notifications`,
1216
+ openEvent: `Opens a ${c} help request`,
1217
+ answerEvent: `Help desk answers the ${c} help request`,
1218
+ againEvent: `Opens another ${c} help request`,
1219
+ toSettings: `Continues to the ${c} settings`
1220
+ };
1221
+ }
1222
+
1223
+ function largeCharts(ctx: LargeContext) {
1224
+ const child = ctx.child.toLowerCase();
1225
+ const link = `Going through the ${child}`;
1226
+ const completed = `${ctx.child} completed`;
1227
+ const abandoned = `${ctx.child} abandoned`;
1228
+ const completedEvent = `${ctx.child} is completed`;
1229
+ const abandonedEvent = `${ctx.child} is abandoned`;
1230
+ const startChild = `Starts the ${child}`;
1231
+ const s = settingsNames(ctx);
1232
+ const legacy = ctx.composition === 'legacy';
1233
+
1234
+ const top: Record<string, Node> = {};
1235
+ ctx.objects.forEach((o, i) => {
1236
+ const nextO = ctx.objects[i + 1];
1237
+ const next =
1238
+ i === ctx.linkAfter
1239
+ ? { event: startChild, target: link }
1240
+ : nextO
1241
+ ? { event: `Continues to the ${nextO}`, target: stageNames(nextO).group }
1242
+ : { event: s.toSettings, target: s.settings };
1243
+ top[stageNames(o).group] = stage(ctx, o, next, { nested: i === 0 });
1244
+ if (i === ctx.linkAfter) {
1245
+ const after = ctx.objects[i + 1]!;
1246
+ top[link] = {
1247
+ id: link,
1248
+ meta: legacy
1249
+ ? stateMeta(link, `The ${child} runs as its own chart.`, { childMachine: ctx.child, childFinalEvents: { [completed]: completedEvent, [abandoned]: abandonedEvent } })
1250
+ : stateMeta(link, `The ${child} runs as its own chart.`),
1251
+ ...(legacy ? {} : { invoke: { src: ctx.child, id: slug(ctx.child) } }),
1252
+ on: { [completedEvent]: go(stageNames(after).group), [abandonedEvent]: go(stageNames(o).reviewing) }
1253
+ };
1254
+ }
1255
+ });
1256
+ top[s.settings] = {
1257
+ id: s.settings,
1258
+ type: 'parallel',
1259
+ meta: stateMeta(s.settings, `Settings for ${ctx.chart.toLowerCase()}: notifications and help side by side.`),
1260
+ states: {
1261
+ [s.notifications]: {
1262
+ id: s.notifications,
1263
+ initial: s.on,
1264
+ meta: stateMeta(s.notifications, 'Whether the user gets notifications.'),
1265
+ states: {
1266
+ [s.on]: { id: s.on, meta: stateMeta(s.on, 'The notifications switch is on.'), on: { [s.offEvent]: go(s.off) } },
1267
+ [s.off]: { id: s.off, meta: stateMeta(s.off, 'The notifications switch is off.'), on: { [s.onEvent]: go(s.on) } }
1268
+ }
1269
+ },
1270
+ [s.support]: {
1271
+ id: s.support,
1272
+ initial: s.none,
1273
+ meta: stateMeta(s.support, 'Help requests about this part of the product.'),
1274
+ states: {
1275
+ [s.none]: { id: s.none, meta: stateMeta(s.none, 'A "Get help" button.'), on: { [s.openEvent]: go(s.open) } },
1276
+ [s.open]: { id: s.open, meta: stateMeta(s.open, 'A card says the help desk will answer soon.'), on: { [s.answerEvent]: go(s.answered) } },
1277
+ [s.answered]: { id: s.answered, meta: stateMeta(s.answered, 'The answer shows in the help card.'), on: { [s.againEvent]: go(s.open) } }
1278
+ }
1279
+ }
1280
+ },
1281
+ on: { [s.closeEvent]: go(s.finished) }
1282
+ };
1283
+ top[s.finished] = { id: s.finished, type: 'final', meta: stateMeta(s.finished, `The user is done with ${ctx.chart.toLowerCase()}.`, { events: [`${ctx.chart} finished`] }) };
1284
+
1285
+ const kid: Record<string, Node> = {};
1286
+ ctx.childObjects.forEach((o, i) => {
1287
+ const nextO = ctx.childObjects[i + 1];
1288
+ const next = nextO ? { event: `Continues to the ${nextO}`, target: stageNames(nextO).group } : { event: `Finishes the ${child}`, target: completed };
1289
+ kid[stageNames(o).group] = stage(ctx, o, next, { nested: i === 0, extraFirst: i === 0 ? { [`Closes the ${child}`]: go(abandoned) } : undefined });
1290
+ });
1291
+ const doneEvent = (event: string) => (legacy ? {} : { doneEvent: event });
1292
+ kid[completed] = { id: completed, type: 'final', meta: stateMeta(completed, `The ${child} is done.`, { events: [`${ctx.child} completed`], ...doneEvent(completedEvent) }) };
1293
+ kid[abandoned] = { id: abandoned, type: 'final', meta: stateMeta(abandoned, `The user left the ${child} before the end.`, doneEvent(abandonedEvent)) };
1294
+
1295
+ const firstO = ctx.objects[0]!;
1296
+ const topMeta: Record<string, unknown> = { description: ctx.description, snapshot: slug(ctx.chart), events: [], confidence: 'confirmed', source: [`src/screens/${slug(ctx.chart)}`] };
1297
+ if (ctx.id === 'orders') {
1298
+ topMeta.timeEvents = { [`Approval of the ${firstO} takes longer than a day`]: '1 day' };
1299
+ topMeta.eventKinds = { [`${ctx.approver} approves the ${firstO}`]: 'system', [`Checks the ${firstO} again`]: 'user' };
1300
+ }
1301
+ const topChart = { id: ctx.chart, initial: stageNames(firstO).group, meta: topMeta, states: top };
1302
+ const childChart = {
1303
+ id: ctx.child,
1304
+ initial: stageNames(ctx.childObjects[0]!).group,
1305
+ meta: { description: `The ${child}, a step inside ${ctx.chart.toLowerCase()}.`, snapshot: slug(ctx.child), events: [], confidence: 'confirmed', source: [] },
1306
+ states: kid
1307
+ };
1308
+ return { topChart, childChart };
1309
+ }
1310
+
1311
+ function largeJourneys(ctx: LargeContext) {
1312
+ const child = ctx.child.toLowerCase();
1313
+ const s = settingsNames(ctx);
1314
+ const approve = (o: string) => [`Submits the ${o}`, `${ctx.approver} approves the ${o}`];
1315
+ const childHappy = ctx.childObjects.flatMap((o, i) => [...approve(o), ctx.childObjects[i + 1] ? `Continues to the ${ctx.childObjects[i + 1]}` : `Finishes the ${child}`]);
1316
+ const through = (count: number) =>
1317
+ ctx.objects.slice(0, count).flatMap((o, i) => {
1318
+ const nextO = ctx.objects[i + 1];
1319
+ if (i === ctx.linkAfter) return [...approve(o), `Starts the ${child}`, ...childHappy];
1320
+ return [...approve(o), nextO ? `Continues to the ${nextO}` : s.toSettings];
1321
+ });
1322
+ const [o0, o1] = ctx.objects as [string, string];
1323
+ const oL = ctx.objects[ctx.linkAfter]!;
1324
+ const toLink = [...through(ctx.linkAfter), ...approve(oL), `Starts the ${child}`];
1325
+ const journeys: Record<string, { description: string; events: string[]; endsIn: string[]; owner?: string; tour?: boolean }> = {
1326
+ [`Happy path through ${ctx.chart.toLowerCase()}`]: {
1327
+ description: `Everything is approved the first time, through the ${child} and settings.`,
1328
+ events: [...through(ctx.objects.length), s.closeEvent],
1329
+ endsIn: [s.finished],
1330
+ owner: ctx.owner,
1331
+ tour: true
1332
+ },
1333
+ [`Edits the ${o0} with a file`]: {
1334
+ description: `The user edits the ${o0}, uploads a file, then sends it.`,
1335
+ events: [`Taps edit on the ${o0}`, 'Uploads a file', 'Finishes uploading the file', `Saves the ${o0}`, ...approve(o0)],
1336
+ endsIn: [stageNames(o0).approved]
1337
+ },
1338
+ [`Saving the ${o0} fails, then works`]: {
1339
+ description: 'The first save fails and the retry works.',
1340
+ events: [`Taps edit on the ${o0}`, `Saving the ${o0} fails`, `Tries saving the ${o0} again`, `Saves the ${o0}`],
1341
+ endsIn: [stageNames(o0).reviewing]
1342
+ },
1343
+ [`Slow approval of the ${o1}`]: {
1344
+ description: `Approval of the ${o1} takes more than a day.`,
1345
+ events: [...through(1), `Submits the ${o1}`, `Approval of the ${o1} takes longer than a day`, `Checks the ${o1} again`, `${ctx.approver} approves the ${o1}`],
1346
+ endsIn: [stageNames(o1).approved]
1347
+ },
1348
+ [`${capitalize(o0)} rejected, then approved`]: {
1349
+ description: `The ${o0} is rejected once, then approved.`,
1350
+ events: [`Submits the ${o0}`, `${ctx.approver} rejects the ${o0}`, `Starts the ${o0} again`, ...approve(o0)],
1351
+ endsIn: [stageNames(o0).approved]
1352
+ },
1353
+ [`Leaves the ${child} early`]: {
1354
+ description: `The user closes the ${child} right away and goes back a step.`,
1355
+ events: [...toLink, `Closes the ${child}`],
1356
+ endsIn: [stageNames(oL).reviewing]
1357
+ },
1358
+ [`Changes ${ctx.chart.toLowerCase()} settings`]: {
1359
+ description: 'Notifications off, then a help request that gets answered.',
1360
+ events: [...through(ctx.objects.length), s.offEvent, s.openEvent, s.answerEvent],
1361
+ endsIn: [s.off, s.answered]
1362
+ }
1363
+ };
1364
+ return { journeys };
1365
+ }
1366
+
1367
+ function makeLarge() {
1368
+ const f = resetFixture('large');
1369
+ const files: SpecText[] = [];
1370
+ const atlas = {
1371
+ schemaVersion: 1,
1372
+ title: 'Example store atlas (fixture)',
1373
+ description: 'A generated atlas with several hundred states, for checking that the map stays fast and readable.',
1374
+ contexts: LARGE_CONTEXTS.map((c) => ({ id: c.id, name: c.name, description: c.description, owner: c.owner, directories: [c.dir.split('/')[0]!] })),
1375
+ handOffs: [
1376
+ { from: 'Sign-up finished', to: 'Reviewing the search filters', event: 'Starts browsing', description: 'A new user moves on to browsing.' },
1377
+ { from: 'Product browsing finished', to: 'Reviewing the shopping cart', event: 'Adds a product to the cart', description: 'Browsing ends in the cart.' },
1378
+ { from: 'Order checkout finished', to: 'Reviewing the shipping label', event: 'Order is shipped', description: 'A paid order is shipped.' },
1379
+ { from: 'Deliveries finished', to: 'Reviewing the help request', event: 'Asks for help with a delivery', description: 'A delivery problem becomes a help request.' },
1380
+ { from: 'Order checkout finished', to: 'Reviewing the monthly invoice', event: 'Invoice is issued', description: 'Every order shows up on an invoice.' }
1381
+ ],
1382
+ excluded: ['Marketplace sellers', 'Gift cards', 'Business accounts']
1383
+ };
1384
+ files.push({ path: 'atlas.yaml', text: YAML.stringify(atlas, { lineWidth: 0 }) });
1385
+ for (const ctx of LARGE_CONTEXTS) {
1386
+ const { topChart, childChart } = largeCharts(ctx);
1387
+ files.push({ path: `${ctx.dir}/machine.yaml`, text: `# ${ctx.chart}: generated fixture chart\n${YAML.stringify(topChart, { lineWidth: 0 })}` });
1388
+ if (ctx.composition === 'invoke-scxml') {
1389
+ files.push({ path: `${ctx.dir}/${slug(ctx.child)}.scxml`, text: machineToScxml(childChart as MachineConfig) });
1390
+ } else {
1391
+ files.push({ path: `${ctx.dir}/${slug(ctx.child)}.machine.yaml`, text: `# ${ctx.child}: generated child chart\n${YAML.stringify(childChart, { lineWidth: 0 })}` });
1392
+ }
1393
+ files.push({ path: `${ctx.dir}/journeys.yaml`, text: YAML.stringify(largeJourneys(ctx), { lineWidth: 0 }) });
1394
+ }
1395
+ files.push({
1396
+ path: 'orders/checkout/open-questions.md',
1397
+ text: [
1398
+ '# Open questions',
1399
+ '',
1400
+ '## Order checkout',
1401
+ '',
1402
+ '1. **How long should approval wait?** Approval can take more than a day.',
1403
+ ' - S: Told the shopping cart is still under review, Waiting for the shopping cart to be approved.',
1404
+ ' - T: Waiting for the shopping cart to be approved → Approval of the shopping cart takes longer than a day.',
1405
+ ' - Spec today: one day.',
1406
+ '2. **Who explains a rejected promo code?** Nobody tells the user why.',
1407
+ ' - S: Told the promo code was rejected (assumed).',
1408
+ '',
1409
+ '## Not modeled',
1410
+ '',
1411
+ '3. These are outside this spec today:',
1412
+ ' - split payments',
1413
+ ' - orders placed by phone',
1414
+ ''
1415
+ ].join('\n')
1416
+ });
1417
+ files.push({
1418
+ path: 'shipping/deliveries/known-issues.md',
1419
+ text: [
1420
+ '# Known issues',
1421
+ '',
1422
+ '## Reviewing the shipping label',
1423
+ '',
1424
+ '- The label preview sometimes shows the old address.',
1425
+ '',
1426
+ '## Waiting for the package contents to be approved → Carrier rejects the package contents',
1427
+ '',
1428
+ '- The rejection reason is not shown to the user.',
1429
+ '',
1430
+ '## Other',
1431
+ '',
1432
+ '- `Told the pickup window was rejected` uses the wrong icon.',
1433
+ ''
1434
+ ].join('\n')
1435
+ });
1436
+ writeSpec(f.dir, files);
1437
+ const spec = loadSpec(files, 'specs');
1438
+ if (spec.doc.states.size < 450) throw new Error(`Large atlas has only ${spec.doc.states.size} states`);
1439
+
1440
+ const covered = LARGE_CONTEXTS.slice(0, 3);
1441
+ const scope = chartScope(spec.doc, covered.flatMap((c) => [c.chart, c.child]));
1442
+ const colors = new Map(LARGE_CONTEXTS.flatMap((c) => [[c.chart, c.color], [c.child, c.color]]));
1443
+ const orders = LARGE_CONTEXTS[2]!;
1444
+ const run = buildRun(spec, f.runs, {
1445
+ startedAt: '2026-10-06T14:40:00.000Z',
1446
+ mode: 'fast',
1447
+ durationMs: 96_300,
1448
+ scope,
1449
+ journeys: spec.doc.journeys.filter((j) => covered.some((c) => j.dir === c.dir)).map((j) => j.name),
1450
+ failing: { [`Waiting for the ${orders.objects[2]} to be approved :: ${orders.approver} approves the ${orders.objects[2]}`]: `Expected "Told the ${orders.objects[2]} was approved", but the card still says it is being checked` },
1451
+ flaky: { [`Reviewing the account details :: Submits the account details`]: 'The submit button was covered by a banner on the first attempt' },
1452
+ art: (state, tone) => ({ title: state.name, subtitle: state.chartId, color: colors.get(state.chartId) ?? '#475467', tone })
1453
+ });
1454
+ writeManifest(run.runDir, run.manifest);
1455
+ return spec;
1456
+ }
1457
+
1458
+ // ---------------------------------------------------------------------------------------------
1459
+
1460
+ function dirSize(dir: string): number {
1461
+ return readdirSync(dir).reduce((sum, name) => {
1462
+ const full = path.join(dir, name);
1463
+ const stat = statSync(full);
1464
+ return sum + (stat.isDirectory() ? dirSize(full) : stat.size);
1465
+ }, 0);
1466
+ }
1467
+
1468
+ function main() {
1469
+ const files = exampleFiles();
1470
+ const spec = loadSpec(files, 'specs');
1471
+ const mermaid = composedMachine(files, SPEC_DIR, TOP_CHART);
1472
+ log(`example spec: ${spec.doc.states.size} states, ${spec.doc.transitions.size} transitions, ${spec.doc.journeys.length} journeys`);
1473
+ makeSpecOnly(spec);
1474
+ makePartial(spec, mermaid);
1475
+ makeFull(spec, mermaid);
1476
+ makeFailures(spec, mermaid);
1477
+ makeRemoved(spec);
1478
+ makeMalformed(spec);
1479
+ const large = makeLarge();
1480
+ log(`large atlas: ${large.doc.states.size} states, ${large.doc.transitions.size} transitions, ${large.doc.journeys.length} journeys, ${large.doc.contexts.length} contexts`);
1481
+ for (const name of readdirSync(FIXTURES_ROOT).sort()) {
1482
+ const dir = path.join(FIXTURES_ROOT, name);
1483
+ if (statSync(dir).isDirectory()) log(`${name.padEnd(10)} ${(dirSize(dir) / 1024).toFixed(0).padStart(6)} KB`);
1484
+ }
1485
+ const total = dirSize(FIXTURES_ROOT);
1486
+ log(`total ${(total / 1024).toFixed(0).padStart(6)} KB`);
1487
+ if (total > SIZE_BUDGET) throw new Error(`Fixtures are ${(total / 1024 / 1024).toFixed(1)} MB, over the ${SIZE_BUDGET / 1024 / 1024} MB budget`);
1488
+ }
1489
+
1490
+ main();