@crvouga/atlas 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +191 -611
  3. package/dist/bun/src/driver.d.ts +41 -7
  4. package/dist/bun/src/driver.js +47 -6
  5. package/dist/bun/src/index.d.ts +8 -0
  6. package/dist/bun/src/index.js +8 -0
  7. package/dist/core/src/cli.d.ts +14 -1
  8. package/dist/core/src/cli.js +35 -1
  9. package/dist/core/src/config.d.ts +77 -4
  10. package/dist/core/src/config.js +110 -17
  11. package/dist/core/src/events/cloudevents.d.ts +54 -9
  12. package/dist/core/src/events/cloudevents.js +60 -9
  13. package/dist/core/src/graph.d.ts +21 -0
  14. package/dist/core/src/graph.js +26 -0
  15. package/dist/core/src/index.d.ts +11 -0
  16. package/dist/core/src/index.js +11 -0
  17. package/dist/core/src/lint.d.ts +30 -0
  18. package/dist/core/src/lint.js +13 -0
  19. package/dist/core/src/plan.d.ts +39 -1
  20. package/dist/core/src/plan.js +28 -1
  21. package/dist/core/src/privacy.d.ts +29 -2
  22. package/dist/core/src/privacy.js +19 -2
  23. package/dist/core/src/report/formats.d.ts +34 -4
  24. package/dist/core/src/report/formats.js +24 -4
  25. package/dist/core/src/report/markdown.d.ts +15 -0
  26. package/dist/core/src/report/markdown.js +8 -0
  27. package/dist/core/src/run/clients.d.ts +44 -5
  28. package/dist/core/src/run/clients.js +46 -2
  29. package/dist/core/src/run/execute.d.ts +75 -16
  30. package/dist/core/src/run/execute.js +71 -18
  31. package/dist/core/src/run/frames.d.ts +13 -4
  32. package/dist/core/src/run/frames.js +19 -4
  33. package/dist/core/src/run/http.d.ts +34 -1
  34. package/dist/core/src/run/http.js +14 -1
  35. package/dist/core/src/run/types.d.ts +76 -3
  36. package/dist/core/src/run/types.js +19 -3
  37. package/dist/core/src/signals.d.ts +49 -5
  38. package/dist/core/src/signals.js +22 -5
  39. package/dist/core/src/spec/compose.d.ts +23 -1
  40. package/dist/core/src/spec/compose.js +10 -1
  41. package/dist/core/src/spec/index.d.ts +9 -1
  42. package/dist/core/src/spec/index.js +9 -1
  43. package/dist/core/src/spec/load.d.ts +22 -1
  44. package/dist/core/src/spec/load.js +31 -1
  45. package/dist/core/src/spec/parse.d.ts +16 -2
  46. package/dist/core/src/spec/parse.js +25 -2
  47. package/dist/core/src/spec/scxml.d.ts +25 -4
  48. package/dist/core/src/spec/scxml.js +36 -4
  49. package/dist/core/src/spec/types.d.ts +52 -1
  50. package/dist/core/src/spec/types.js +24 -0
  51. package/dist/detox/src/index.d.ts +54 -11
  52. package/dist/detox/src/index.js +45 -7
  53. package/dist/playwright/src/driver.d.ts +39 -3
  54. package/dist/playwright/src/driver.js +27 -3
  55. package/dist/playwright/src/index.d.ts +10 -0
  56. package/dist/playwright/src/index.js +10 -0
  57. package/dist/playwright/src/media.d.ts +50 -4
  58. package/dist/playwright/src/media.js +55 -4
  59. package/dist/playwright/src/signals.d.ts +44 -5
  60. package/dist/playwright/src/signals.js +29 -5
  61. package/dist/reports/src/attachment-files.d.ts +44 -0
  62. package/dist/reports/src/attachment-files.js +121 -0
  63. package/dist/reports/src/attachment-response.d.ts +31 -0
  64. package/dist/reports/src/attachment-response.js +55 -0
  65. package/dist/reports/src/aws-credentials.d.ts +48 -0
  66. package/dist/reports/src/aws-credentials.js +221 -0
  67. package/dist/reports/src/browser-navigation.d.ts +14 -0
  68. package/dist/reports/src/browser-navigation.js +7 -0
  69. package/dist/reports/src/bundle.d.ts +34 -2
  70. package/dist/reports/src/bundle.js +38 -2
  71. package/dist/reports/src/catalog.d.ts +34 -3
  72. package/dist/reports/src/catalog.js +17 -3
  73. package/dist/reports/src/cli-stores.d.ts +36 -0
  74. package/dist/reports/src/cli-stores.js +73 -0
  75. package/dist/reports/src/clock.d.ts +7 -2
  76. package/dist/reports/src/clock.js +5 -2
  77. package/dist/reports/src/completion-order.d.ts +18 -5
  78. package/dist/reports/src/completion-order.js +18 -5
  79. package/dist/reports/src/compose.d.ts +80 -0
  80. package/dist/reports/src/compose.js +117 -0
  81. package/dist/reports/src/copy-store.d.ts +29 -0
  82. package/dist/reports/src/copy-store.js +52 -0
  83. package/dist/reports/src/directory-outline.d.ts +38 -0
  84. package/dist/reports/src/directory-outline.js +103 -0
  85. package/dist/reports/src/directory-store.d.ts +20 -0
  86. package/dist/reports/src/directory-store.js +166 -0
  87. package/dist/reports/src/doctor.d.ts +29 -0
  88. package/dist/reports/src/doctor.js +85 -0
  89. package/dist/reports/src/effective-delta.d.ts +34 -0
  90. package/dist/reports/src/effective-delta.js +25 -0
  91. package/dist/reports/src/evidence-detail.d.ts +64 -0
  92. package/dist/reports/src/evidence-detail.js +92 -0
  93. package/dist/reports/src/evidence-live.d.ts +35 -5
  94. package/dist/reports/src/evidence-live.js +18 -2
  95. package/dist/reports/src/evidence-types.d.ts +68 -1
  96. package/dist/reports/src/evidence-types.js +5 -1
  97. package/dist/reports/src/evidence-view.d.ts +43 -4
  98. package/dist/reports/src/evidence-view.js +13 -2
  99. package/dist/reports/src/explorer-types.d.ts +40 -3
  100. package/dist/reports/src/explorer-types.js +8 -0
  101. package/dist/reports/src/explorer-versions.d.ts +9 -1
  102. package/dist/reports/src/explorer-versions.js +18 -1
  103. package/dist/reports/src/export-evidence.d.ts +16 -1
  104. package/dist/reports/src/export-evidence.js +52 -1
  105. package/dist/reports/src/fetcher.d.ts +27 -3
  106. package/dist/reports/src/fetcher.js +11 -3
  107. package/dist/reports/src/format-date.d.ts +6 -0
  108. package/dist/reports/src/format-date.js +6 -0
  109. package/dist/reports/src/format.d.ts +16 -0
  110. package/dist/reports/src/format.js +10 -0
  111. package/dist/reports/src/frames.d.ts +36 -3
  112. package/dist/reports/src/frames.js +22 -3
  113. package/dist/reports/src/hash.d.ts +18 -0
  114. package/dist/reports/src/hash.js +13 -0
  115. package/dist/reports/src/host.d.ts +29 -0
  116. package/dist/reports/src/host.js +64 -0
  117. package/dist/reports/src/html-frame.d.ts +5 -6
  118. package/dist/reports/src/html-frame.js +12 -1
  119. package/dist/reports/src/http-store.d.ts +56 -0
  120. package/dist/reports/src/http-store.js +148 -0
  121. package/dist/reports/src/http.d.ts +7 -0
  122. package/dist/reports/src/http.js +6 -0
  123. package/dist/reports/src/ingest.d.ts +54 -0
  124. package/dist/reports/src/ingest.js +210 -0
  125. package/dist/reports/src/init.d.ts +40 -0
  126. package/dist/reports/src/init.js +369 -0
  127. package/dist/reports/src/junit.d.ts +33 -6
  128. package/dist/reports/src/junit.js +87 -21
  129. package/dist/reports/src/keyboard.d.ts +20 -0
  130. package/dist/reports/src/keyboard.js +11 -0
  131. package/dist/reports/src/keys.d.ts +21 -3
  132. package/dist/reports/src/keys.js +21 -3
  133. package/dist/reports/src/layer-axes.d.ts +35 -5
  134. package/dist/reports/src/layer-axes.js +25 -5
  135. package/dist/reports/src/layer-catalog.d.ts +28 -1
  136. package/dist/reports/src/layer-catalog.js +18 -1
  137. package/dist/reports/src/layer-merge.d.ts +98 -9
  138. package/dist/reports/src/layer-merge.js +147 -31
  139. package/dist/reports/src/layer-source.d.ts +18 -0
  140. package/dist/reports/src/layer-source.js +11 -0
  141. package/dist/reports/src/layer-store.d.ts +42 -0
  142. package/dist/reports/src/layer-store.js +63 -5
  143. package/dist/reports/src/layer-types.d.ts +32 -1
  144. package/dist/reports/src/layers-route.d.ts +47 -4
  145. package/dist/reports/src/layers-route.js +86 -7
  146. package/dist/reports/src/live-reporter.d.ts +34 -0
  147. package/dist/reports/src/live-reporter.js +122 -0
  148. package/dist/reports/src/media-types.d.ts +11 -0
  149. package/dist/reports/src/media-types.js +22 -0
  150. package/dist/reports/src/namespace-guard.d.ts +10 -1
  151. package/dist/reports/src/namespace-guard.js +10 -1
  152. package/dist/reports/src/navigation.d.ts +12 -0
  153. package/dist/reports/src/navigation.js +7 -0
  154. package/dist/reports/src/object-io.d.ts +11 -4
  155. package/dist/reports/src/object-io.js +4 -4
  156. package/dist/reports/src/object-store-conformance.d.ts +27 -0
  157. package/dist/reports/src/object-store-conformance.js +87 -0
  158. package/dist/reports/src/object-store.d.ts +33 -24
  159. package/dist/reports/src/object-store.js +17 -24
  160. package/dist/reports/src/parse-with.d.ts +16 -0
  161. package/dist/reports/src/parse-with.js +14 -0
  162. package/dist/reports/src/performance-part.d.ts +25 -1
  163. package/dist/reports/src/performance-part.js +15 -1
  164. package/dist/reports/src/performance.d.ts +22 -2
  165. package/dist/reports/src/performance.js +12 -2
  166. package/dist/reports/src/persist.d.ts +12 -3
  167. package/dist/reports/src/persist.js +12 -3
  168. package/dist/reports/src/phone.d.ts +18 -1
  169. package/dist/reports/src/phone.js +18 -1
  170. package/dist/reports/src/protocol.d.ts +26 -4
  171. package/dist/reports/src/protocol.js +15 -4
  172. package/dist/reports/src/publication.d.ts +21 -1
  173. package/dist/reports/src/publication.js +21 -1
  174. package/dist/reports/src/publish-checks.d.ts +35 -0
  175. package/dist/reports/src/publish-checks.js +122 -0
  176. package/dist/reports/src/publish.d.ts +42 -9
  177. package/dist/reports/src/publish.js +36 -7
  178. package/dist/reports/src/read.d.ts +46 -16
  179. package/dist/reports/src/read.js +38 -14
  180. package/dist/reports/src/recording-bundle.d.ts +13 -2
  181. package/dist/reports/src/recording-bundle.js +4 -2
  182. package/dist/reports/src/recording-keys.d.ts +15 -1
  183. package/dist/reports/src/recording-keys.js +15 -1
  184. package/dist/reports/src/remote-report.d.ts +28 -7
  185. package/dist/reports/src/remote-report.js +15 -7
  186. package/dist/reports/src/report-cli.d.ts +51 -0
  187. package/dist/reports/src/report-cli.js +486 -0
  188. package/dist/reports/src/report-index.d.ts +99 -0
  189. package/dist/reports/src/report-index.js +337 -0
  190. package/dist/reports/src/report-loader.d.ts +10 -0
  191. package/dist/reports/src/report-loader.js +6 -0
  192. package/dist/reports/src/report-parts.d.ts +29 -3
  193. package/dist/reports/src/report-parts.js +37 -3
  194. package/dist/reports/src/report-status.d.ts +48 -0
  195. package/dist/reports/src/report-status.js +156 -0
  196. package/dist/reports/src/report-types.d.ts +73 -0
  197. package/dist/reports/src/report-types.js +3 -0
  198. package/dist/reports/src/reporter.d.ts +67 -0
  199. package/dist/reports/src/reporter.js +128 -0
  200. package/dist/reports/src/reports-explorer-route.d.ts +541 -0
  201. package/dist/reports/src/reports-explorer-route.js +11 -0
  202. package/dist/reports/src/reports-list-route.d.ts +9 -0
  203. package/dist/reports/src/reports-list-route.js +9 -0
  204. package/dist/reports/src/reports-route.d.ts +561 -22
  205. package/dist/reports/src/reports-route.js +11 -3
  206. package/dist/reports/src/reports-store.d.ts +19 -2
  207. package/dist/reports/src/reports-store.js +10 -2
  208. package/dist/reports/src/resilient-cache.d.ts +14 -1
  209. package/dist/reports/src/resilient-cache.js +8 -1
  210. package/dist/reports/src/retention.d.ts +17 -1
  211. package/dist/reports/src/retention.js +5 -1
  212. package/dist/reports/src/run-bundle.d.ts +63 -0
  213. package/dist/reports/src/run-bundle.js +159 -0
  214. package/dist/reports/src/s3-store.d.ts +79 -0
  215. package/dist/reports/src/s3-store.js +295 -0
  216. package/dist/reports/src/scenario.d.ts +14 -4
  217. package/dist/reports/src/scenario.js +5 -4
  218. package/dist/reports/src/sigv4.d.ts +72 -0
  219. package/dist/reports/src/sigv4.js +96 -0
  220. package/dist/reports/src/snapshot-bundle.d.ts +24 -1
  221. package/dist/reports/src/snapshot-bundle.js +27 -2
  222. package/dist/reports/src/snapshot-surface.d.ts +20 -9
  223. package/dist/reports/src/snapshot-surface.js +32 -19
  224. package/dist/reports/src/source-content.d.ts +23 -0
  225. package/dist/reports/src/source-content.js +30 -1
  226. package/dist/reports/src/source-entries.d.ts +13 -0
  227. package/dist/reports/src/source-entries.js +6 -0
  228. package/dist/reports/src/source-part.d.ts +15 -0
  229. package/dist/reports/src/source-part.js +8 -0
  230. package/dist/reports/src/source-reader.d.ts +11 -0
  231. package/dist/reports/src/source-reader.js +5 -0
  232. package/dist/reports/src/spec-evidence.d.ts +12 -2
  233. package/dist/reports/src/spec-evidence.js +6 -2
  234. package/dist/reports/src/spec-inventory.d.ts +15 -1
  235. package/dist/reports/src/spec-inventory.js +22 -1
  236. package/dist/reports/src/spec-link.d.ts +28 -0
  237. package/dist/reports/src/spec-link.js +70 -0
  238. package/dist/reports/src/spec-set.d.ts +63 -0
  239. package/dist/reports/src/spec-set.js +177 -0
  240. package/dist/reports/src/status.d.ts +10 -0
  241. package/dist/reports/src/status.js +5 -0
  242. package/dist/reports/src/store-prefix.d.ts +28 -0
  243. package/dist/reports/src/store-prefix.js +22 -0
  244. package/dist/reports/src/store-url.d.ts +89 -0
  245. package/dist/reports/src/store-url.js +160 -0
  246. package/dist/reports/src/test-catalog-entity.d.ts +11 -0
  247. package/dist/reports/src/test-catalog-entity.js +6 -0
  248. package/dist/reports/src/types.d.ts +6 -1
  249. package/dist/reports/src/use-report-nav.d.ts +35 -0
  250. package/dist/reports/src/use-report-nav.js +12 -0
  251. package/dist/runners/src/codecept.d.ts +14 -0
  252. package/dist/runners/src/codecept.js +16 -0
  253. package/dist/runners/src/jest.d.ts +50 -0
  254. package/dist/runners/src/jest.js +52 -0
  255. package/dist/runners/src/mocha.d.ts +40 -0
  256. package/dist/runners/src/mocha.js +39 -0
  257. package/dist/runners/src/node-test.d.ts +28 -0
  258. package/dist/runners/src/node-test.js +44 -0
  259. package/dist/runners/src/publish.d.ts +30 -0
  260. package/dist/runners/src/publish.js +68 -0
  261. package/dist/schema/src/api.d.ts +800 -0
  262. package/dist/schema/src/api.js +201 -0
  263. package/dist/schema/src/cli-results.d.ts +210 -0
  264. package/dist/schema/src/cli-results.js +124 -0
  265. package/dist/schema/src/common.d.ts +53 -0
  266. package/dist/schema/src/common.js +42 -0
  267. package/dist/schema/src/index.d.ts +16 -0
  268. package/dist/schema/src/index.js +16 -0
  269. package/dist/schema/src/layer.d.ts +2273 -0
  270. package/dist/schema/src/layer.js +462 -0
  271. package/dist/schema/src/live-run.d.ts +123 -0
  272. package/dist/schema/src/live-run.js +30 -0
  273. package/dist/schema/src/manifest.d.ts +97 -2
  274. package/dist/schema/src/manifest.js +84 -0
  275. package/dist/schema/src/project.d.ts +20 -0
  276. package/dist/schema/src/project.js +10 -0
  277. package/dist/schema/src/report-sources.d.ts +29 -0
  278. package/dist/schema/src/report-sources.js +27 -1
  279. package/dist/schema/src/runs-index.d.ts +17 -1
  280. package/dist/schema/src/runs-index.js +15 -0
  281. package/dist/schema/src/server-config.d.ts +177 -0
  282. package/dist/schema/src/server-config.js +85 -0
  283. package/dist/schema/src/spec.d.ts +69 -0
  284. package/dist/schema/src/spec.js +58 -0
  285. package/dist/server/src/app.d.ts +72 -0
  286. package/dist/server/src/app.js +557 -0
  287. package/dist/server/src/auth.d.ts +41 -0
  288. package/dist/server/src/auth.js +59 -0
  289. package/dist/server/src/config.d.ts +83 -0
  290. package/dist/server/src/config.js +204 -0
  291. package/dist/server/src/dev.d.ts +12 -0
  292. package/dist/server/src/dev.js +89 -0
  293. package/dist/server/src/events.d.ts +29 -0
  294. package/dist/server/src/events.js +101 -0
  295. package/dist/server/src/host.d.ts +30 -0
  296. package/dist/server/src/host.js +115 -0
  297. package/dist/server/src/index.d.ts +18 -0
  298. package/dist/server/src/index.js +18 -0
  299. package/dist/server/src/live-runs.d.ts +52 -0
  300. package/dist/server/src/live-runs.js +229 -0
  301. package/dist/server/src/llms.d.ts +19 -0
  302. package/dist/server/src/llms.js +63 -0
  303. package/dist/server/src/main.d.ts +30 -0
  304. package/dist/server/src/main.js +114 -0
  305. package/dist/server/src/openapi.d.ts +56 -0
  306. package/dist/server/src/openapi.js +438 -0
  307. package/dist/server/src/problem.d.ts +8 -0
  308. package/dist/server/src/problem.js +39 -0
  309. package/dist/server/src/projects.d.ts +3 -0
  310. package/dist/server/src/projects.js +154 -0
  311. package/dist/server/src/settings-page.d.ts +9 -0
  312. package/dist/server/src/settings-page.js +93 -0
  313. package/dist/server/src/settings.d.ts +23 -0
  314. package/dist/server/src/settings.js +72 -0
  315. package/dist/server/src/site.d.ts +15 -0
  316. package/dist/server/src/site.js +78 -0
  317. package/dist/server/src/store-api.d.ts +28 -0
  318. package/dist/server/src/store-api.js +180 -0
  319. package/dist/server/src/validate.d.ts +71 -0
  320. package/dist/server/src/validate.js +197 -0
  321. package/dist/vitest/src/index.d.ts +27 -5
  322. package/dist/vitest/src/index.js +18 -5
  323. package/dist/webdriver/src/driver.d.ts +63 -11
  324. package/dist/webdriver/src/driver.js +57 -6
  325. package/dist/webdriver/src/index.d.ts +8 -0
  326. package/dist/webdriver/src/index.js +1 -0
  327. package/dist/webdriver/src/protocol.d.ts +75 -6
  328. package/dist/webdriver/src/protocol.js +79 -6
  329. package/docs/agents.md +137 -0
  330. package/docs/deploying.md +345 -0
  331. package/docs/dogfooding.md +175 -0
  332. package/docs/extending.md +242 -0
  333. package/docs/integrating.md +485 -0
  334. package/docs/reference/bun.md +141 -0
  335. package/docs/reference/cli.md +148 -0
  336. package/docs/reference/core.md +2432 -0
  337. package/docs/reference/detox.md +150 -0
  338. package/docs/reference/http-api.md +799 -0
  339. package/docs/reference/playwright.md +303 -0
  340. package/docs/reference/reports.md +7152 -0
  341. package/docs/reference/runners.md +169 -0
  342. package/docs/reference/schema.md +1762 -0
  343. package/docs/reference/server.md +825 -0
  344. package/docs/reference/vitest.md +45 -0
  345. package/docs/reference/webdriver.md +330 -0
  346. package/docs/report-backends.md +144 -0
  347. package/docs/report-contract.md +400 -0
  348. package/docs/reporting-issues.md +160 -0
  349. package/docs/runners.md +69 -0
  350. package/docs/server.md +321 -0
  351. package/docs/statecharts.md +643 -0
  352. package/llms-full.txt +4114 -0
  353. package/llms.txt +49 -0
  354. package/package.json +61 -11
  355. package/packages/core/README.md +2 -1
  356. package/packages/reports/README.md +65 -0
  357. package/packages/reports/STORAGE.md +4 -0
  358. package/packages/runners/README.md +6 -0
  359. package/packages/runners/compat/codecept.cjs +2 -0
  360. package/packages/runners/compat/jest.cjs +2 -0
  361. package/packages/runners/compat/mocha.cjs +2 -0
  362. package/packages/schema/README.md +14 -0
  363. package/packages/schema/schemas/atlas-change.schema.json +4 -0
  364. package/packages/schema/schemas/check-detail.schema.json +1032 -0
  365. package/packages/schema/schemas/checks-result.schema.json +26 -0
  366. package/packages/schema/schemas/command-error.schema.json +35 -0
  367. package/packages/schema/schemas/doctor-result.schema.json +60 -0
  368. package/packages/schema/schemas/effective-delta.schema.json +490 -0
  369. package/packages/schema/schemas/effective-evidence.schema.json +471 -0
  370. package/packages/schema/schemas/evidence-page.schema.json +271 -0
  371. package/packages/schema/schemas/hello-event.schema.json +26 -0
  372. package/packages/schema/schemas/init-plan.schema.json +347 -0
  373. package/packages/schema/schemas/layer-rows.schema.json +199 -0
  374. package/packages/schema/schemas/layer.schema.json +2888 -0
  375. package/packages/schema/schemas/live-run-start.schema.json +42 -0
  376. package/packages/schema/schemas/live-run-update.schema.json +93 -0
  377. package/packages/schema/schemas/live-run.schema.json +225 -0
  378. package/packages/schema/schemas/merge-result.schema.json +40 -0
  379. package/packages/schema/schemas/outline.schema.json +62 -0
  380. package/packages/schema/schemas/problem.schema.json +37 -0
  381. package/packages/schema/schemas/project-slug.schema.json +8 -0
  382. package/packages/schema/schemas/project.schema.json +22 -0
  383. package/packages/schema/schemas/projects.schema.json +33 -0
  384. package/packages/schema/schemas/publish-checks.schema.json +1413 -0
  385. package/packages/schema/schemas/publish-result.schema.json +168 -0
  386. package/packages/schema/schemas/ready.schema.json +68 -0
  387. package/packages/schema/schemas/report-event.schema.json +100 -0
  388. package/packages/schema/schemas/report-status.schema.json +370 -0
  389. package/packages/schema/schemas/server-config.schema.json +208 -0
  390. package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
  391. package/packages/schema/schemas/server-info.schema.json +114 -0
  392. package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
  393. package/packages/schema/schemas/spec-set.schema.json +103 -0
  394. package/packages/schema/schemas/store-listing.schema.json +29 -0
  395. package/packages/schema/schemas/store-write-result.schema.json +28 -0
  396. package/packages/schema/schemas/stream-result.schema.json +59 -0
  397. package/packages/server/README.md +43 -0
  398. package/packages/server/openapi.json +11021 -0
  399. package/packages/visualizer/README.md +32 -11
  400. package/packages/visualizer/bin/atlas-client.mjs +176 -0
  401. package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
  402. package/packages/visualizer/src/app/AppShell.tsx +32 -16
  403. package/packages/visualizer/src/app/report-sources.tsx +7 -4
  404. package/packages/visualizer/src/app/router.tsx +3 -3
  405. package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
  406. package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
  407. package/packages/visualizer/src/components/Header.module.css +101 -192
  408. package/packages/visualizer/src/components/Header.tsx +88 -13
  409. package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
  410. package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
  411. package/packages/visualizer/src/data/adapters/index.ts +10 -1
  412. package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
  413. package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
  414. package/packages/visualizer/src/data/queries.ts +14 -7
  415. package/packages/visualizer/src/evidence/api.ts +16 -3
  416. package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
  417. package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
  418. package/packages/visualizer/src/evidence/check-detail.tsx +21 -4
  419. package/packages/visualizer/src/evidence/document-page.tsx +6 -2
  420. package/packages/visualizer/src/evidence/evidence-context.tsx +79 -12
  421. package/packages/visualizer/src/evidence/evidence.module.css +11 -1
  422. package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
  423. package/packages/visualizer/src/evidence/model-page.tsx +6 -2
  424. package/packages/visualizer/src/evidence/project-home.tsx +45 -0
  425. package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
  426. package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
  427. package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
  428. package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
  429. package/packages/visualizer/src/find/Finder.tsx +1 -7
  430. package/packages/visualizer/src/find/open.ts +4 -0
  431. package/packages/visualizer/src/live/live-query.ts +21 -0
  432. package/packages/visualizer/src/live/live-runs.tsx +69 -0
  433. package/packages/visualizer/src/live/live-stream.ts +225 -0
  434. package/packages/visualizer/src/live/report-progress.ts +62 -0
  435. package/packages/visualizer/src/live/test-progress.tsx +34 -0
  436. package/packages/visualizer/src/runtime.ts +35 -0
  437. package/packages/visualizer/src/state/ui-store.ts +3 -2
  438. package/packages/visualizer/src/views/ChartPage.tsx +2 -0
  439. package/packages/visualizer/src/views/ChartTests.tsx +43 -0
  440. package/packages/visualizer/src/views/SidePanel.tsx +2 -0
  441. package/packages/visualizer/vite/atlas-files.test.ts +4 -4
  442. package/packages/visualizer/vite/atlas-files.ts +1 -5
  443. package/packages/visualizer/vite/atlas-server-proxy.ts +37 -0
  444. package/packages/visualizer/vite.config.ts +11 -3
@@ -0,0 +1,369 @@
1
+ /**
2
+ * @module Project detection and `atlas init`
3
+ *
4
+ * Looks at a project (its package manager, test runners, spec files, CI and git state) and produces a plan to send its
5
+ * test results to an Atlas server: for each suite the command that runs it and the command that publishes (or streams)
6
+ * its results, a CI workflow, and a section of `AGENTS.md` that tells the next agent how to use Atlas in this project.
7
+ * It changes nothing unless asked (`--write`), and applying it twice changes nothing the second time: files are
8
+ * rewritten only between their `atlas:begin` / `atlas:end` markers (or created whole), never merged by guesswork.
9
+ *
10
+ * The plan is one JSON document (`InitPlanSchema`) so an agent can run `atlas init --format json`, read it, and act. See
11
+ * docs/agents.md for the whole flow. Detection is deliberately conservative: it names what it found and why
12
+ * (`evidence`), says what it could not tell, and never invents a suite.
13
+ */
14
+ import { execFileSync } from 'node:child_process';
15
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
16
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
17
+ import path from 'node:path';
18
+ import { InitPlanSchema } from '../../schema/src/cli-results.js';
19
+ import { collectSpecFiles, specKindOf } from './directory-outline.js';
20
+ import { httpStore } from './http-store.js';
21
+ import { serverToken } from './store-url.js';
22
+ const BEGIN = '<!-- atlas:begin -->';
23
+ const END = '<!-- atlas:end -->';
24
+ function defaultGit(args, cwd) {
25
+ try {
26
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function readJson(file) {
33
+ try {
34
+ return JSON.parse(readFileSync(file, 'utf8'));
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ /** The package manager a project uses, from its lockfile (or `packageManager` in package.json). */
41
+ function packageManagerOf(directory, pkg) {
42
+ if (existsSync(path.join(directory, 'bun.lock')) || existsSync(path.join(directory, 'bun.lockb')))
43
+ return 'bun';
44
+ if (existsSync(path.join(directory, 'pnpm-lock.yaml')))
45
+ return 'pnpm';
46
+ if (existsSync(path.join(directory, 'yarn.lock')))
47
+ return 'yarn';
48
+ if (existsSync(path.join(directory, 'package-lock.json')))
49
+ return 'npm';
50
+ const declared = typeof pkg?.packageManager === 'string' ? pkg.packageManager.split('@')[0] : null;
51
+ return declared === 'bun' || declared === 'pnpm' || declared === 'yarn' || declared === 'npm' ? declared : pkg ? 'npm' : null;
52
+ }
53
+ /** How the package manager runs a package's binary without installing it: `bunx`, `pnpm dlx`, `yarn dlx`, `npx`. */
54
+ const EXEC = { bun: 'bunx', pnpm: 'pnpm dlx', yarn: 'yarn dlx', npm: 'npx' };
55
+ const INSTALL_DEV = { bun: 'bun add -d', pnpm: 'pnpm add -D', yarn: 'yarn add -D', npm: 'npm install -D' };
56
+ /** The spec files of a project by kind, and the folder to pass as `--specs`. */
57
+ async function specsOf(directory) {
58
+ const candidates = ['specs', 'spec', 'features', 'docs/specs', 'test/features', 'tests/features'];
59
+ let best = null;
60
+ for (const candidate of candidates) {
61
+ const full = path.join(directory, candidate);
62
+ if (!existsSync(full) || !statSync(full).isDirectory())
63
+ continue;
64
+ const files = await collectSpecFiles(full).catch(() => []);
65
+ const kinds = {};
66
+ // Prose and data files are specs only because they sit in a specs folder: count the notations that mean something.
67
+ for (const file of files) {
68
+ const kind = specKindOf(file.path);
69
+ if (['gherkin', 'quint', 'dmn', 'alloy', 'openslo', 'scxml'].includes(kind))
70
+ kinds[kind] = (kinds[kind] ?? 0) + 1;
71
+ }
72
+ const total = Object.values(kinds).reduce((sum, count) => sum + count, 0);
73
+ if (total > (best?.total ?? 0))
74
+ best = { dir: candidate, kinds, total };
75
+ }
76
+ return best ? { directory: best.dir, kinds: best.kinds } : { directory: null, kinds: {} };
77
+ }
78
+ /** The test runners a project declares, with where each was seen. Order: the more specific first. */
79
+ function runnersOf(directory, pkg) {
80
+ const found = [];
81
+ const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
82
+ const scripts = Object.values(pkg?.scripts ?? {}).join('\n');
83
+ const dep = (name, runner) => { if (deps[name])
84
+ found.push({ runner, evidence: `package.json dependencies: ${name}` }); };
85
+ dep('vitest', 'vitest');
86
+ dep('@playwright/test', 'playwright');
87
+ dep('@cucumber/cucumber', 'cucumber');
88
+ dep('cypress', 'cypress');
89
+ dep('jest', 'jest');
90
+ dep('mocha', 'mocha');
91
+ dep('codeceptjs', 'codeceptjs');
92
+ if (/\bnode\s+--test\b/.test(scripts))
93
+ found.push({ runner: 'node-test', evidence: 'package.json scripts: node --test' });
94
+ if (!found.some(item => item.runner === 'vitest' || item.runner === 'jest' || item.runner === 'mocha') && /\bbun test\b/.test(scripts))
95
+ found.push({ runner: 'bun-test', evidence: 'package.json scripts: bun test' });
96
+ else if (!found.length && pkg && (existsSync(path.join(directory, 'bun.lock')) || existsSync(path.join(directory, 'bunfig.toml'))) && readdirSafe(directory).some(name => /\.(test|spec)\.[cm]?[tj]sx?$/.test(name) || name === 'tests' || name === 'test'))
97
+ found.push({ runner: 'bun-test', evidence: 'bun.lock and test files' });
98
+ const python = ['pyproject.toml', 'pytest.ini', 'tox.ini', 'setup.cfg', 'requirements.txt', 'requirements-dev.txt'].map(name => path.join(directory, name)).filter(existsSync);
99
+ for (const file of python)
100
+ if (/pytest/.test(readFileSync(file, 'utf8'))) {
101
+ found.push({ runner: 'pytest', evidence: `${path.basename(file)} mentions pytest` });
102
+ break;
103
+ }
104
+ if (existsSync(path.join(directory, 'go.mod')))
105
+ found.push({ runner: 'go-test', evidence: 'go.mod' });
106
+ if (existsSync(path.join(directory, 'atlas.config.ts')) || existsSync(path.join(directory, 'atlas.config.js')))
107
+ found.push({ runner: 'atlas-statecharts', evidence: 'atlas.config.ts' });
108
+ return found;
109
+ }
110
+ function readdirSafe(directory) {
111
+ try {
112
+ return readdirSync(directory);
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ }
118
+ /** The suite for a detected runner: how to run it so it writes a JUnit file (or the run manifest), and how to publish it. */
119
+ function suiteOf(detected, context) {
120
+ const specs = context.specs ? ` --specs ${context.specs}` : '';
121
+ const publishCommand = (producer, input, environment) => `${context.exec} @crvouga/atlas report publish --producer ${producer} ${input}${specs}${environment ? ` --environment ${environment}` : ''}`;
122
+ const make = (id, output, run, environment, prerequisites = [], format = 'junit', stream = null) => ({
123
+ id, runner: detected.runner, evidence: detected.evidence, run, output, format, environment,
124
+ publish: publishCommand(id, format === 'manifest' ? `--manifest ${output} --upload-run` : `--${format} ${output}`, environment), stream, prerequisites
125
+ });
126
+ const out = (id) => `atlas-${id}.xml`;
127
+ switch (detected.runner) {
128
+ case 'vitest': return make('vitest', out('vitest'), `${context.exec} vitest run --reporter=default --reporter=junit --outputFile.junit=${out('vitest')}`, null);
129
+ case 'jest': return make('jest', out('jest'), `JEST_JUNIT_OUTPUT_FILE=${out('jest')} ${context.exec} jest --reporters=default --reporters=jest-junit`, null, [`${context.install} jest-junit`]);
130
+ case 'playwright': return make('playwright', out('playwright'), `PLAYWRIGHT_JUNIT_OUTPUT_NAME=${out('playwright')} ${context.exec} playwright test --reporter=list,junit`, 'desktop-web');
131
+ case 'cypress': return make('cypress', out('cypress'), `${context.exec} cypress run --reporter junit --reporter-options mochaFile=${out('cypress')}`, 'desktop-web');
132
+ case 'cucumber': return make('cucumber', out('cucumber'), `${context.exec} cucumber-js --format summary --format junit:${out('cucumber')}`, null);
133
+ case 'mocha': return make('mocha', out('mocha'), `MOCHA_FILE=${out('mocha')} ${context.exec} mocha --reporter mocha-junit-reporter`, null, [`${context.install} mocha-junit-reporter`]);
134
+ case 'codeceptjs': return make('codeceptjs', out('codeceptjs'), `MOCHA_FILE=${out('codeceptjs')} ${context.exec} codeceptjs run --reporter mocha-junit-reporter`, 'desktop-web', [`${context.install} mocha-junit-reporter`]);
135
+ case 'node-test': return make('node-test', out('node-test'), `node --test --test-reporter=junit --test-reporter-destination=${out('node-test')}`, null);
136
+ case 'bun-test': return make('bun-test', out('bun'), `bun test --reporter=junit --reporter-outfile=${out('bun')}`, null);
137
+ case 'pytest': return make('pytest', out('pytest'), `pytest --junitxml=${out('pytest')}`, null);
138
+ case 'go-test': return make('go-test', out('go'), `go test -v ./... 2>&1 | go-junit-report > ${out('go')}`, null, ['go install github.com/jstemmer/go-junit-report/v2@latest']);
139
+ default: return make('statecharts', 'atlas-runs/latest/manifest.json', `${context.exec} @crvouga/atlas run --mode fast`, null, [], 'manifest');
140
+ }
141
+ }
142
+ /** A suite that does not set its own exit-code handling must still publish: a failure is evidence. */
143
+ function githubWorkflow(suites, context) {
144
+ const setup = context.manager === 'bun'
145
+ ? ' - uses: oven-sh/setup-bun@v2\n - run: bun install --frozen-lockfile\n'
146
+ : ` - uses: actions/setup-node@v4\n with: { node-version: 22 }\n - run: ${context.manager === 'pnpm' ? 'corepack enable && pnpm install --frozen-lockfile' : context.manager === 'yarn' ? 'corepack enable && yarn install --immutable' : 'npm ci'}\n`;
147
+ const exec = EXEC[context.manager ?? 'npm'];
148
+ const jobs = suites.map(suite => ` ${suite.id.replace(/[^a-z0-9]+/g, '-')}:
149
+ runs-on: ubuntu-latest
150
+ steps:
151
+ - uses: actions/checkout@v4
152
+ ${setup} # A failing suite must still publish: a failure is evidence. The job's own result is the gate's, below.
153
+ - run: ${suite.run}
154
+ continue-on-error: true
155
+ - run: ${suite.publish}
156
+ if: always()
157
+ `).join('\n');
158
+ return `# Sends every suite's results to the project's Atlas server and gates on the combined report.
159
+ # Generated by \`atlas init\`; safe to edit. Needs the repository variable ATLAS_URL (https://atlas.example.com) and the
160
+ # secrets ATLAS_PUBLISH_TOKEN (a \`publish\` token) and ATLAS_READ_TOKEN (a \`read\` token) of that server.
161
+ name: atlas
162
+ on: [push, pull_request]
163
+ permissions:
164
+ contents: read
165
+ env:
166
+ ATLAS_STORE: \${{ vars.ATLAS_URL }}
167
+ ATLAS_TOKEN: \${{ secrets.ATLAS_PUBLISH_TOKEN }}
168
+ jobs:
169
+ ${jobs}
170
+ gate:
171
+ needs: [${suites.map(suite => suite.id.replace(/[^a-z0-9]+/g, '-')).join(', ')}]
172
+ if: always()
173
+ runs-on: ubuntu-latest
174
+ env:
175
+ ATLAS_TOKEN: \${{ secrets.ATLAS_READ_TOKEN }}
176
+ steps:
177
+ - uses: actions/checkout@v4
178
+ - uses: oven-sh/setup-bun@v2
179
+ # This branch laid over ${context.defaultBranch}: what this run did not re-run is inherited from ${context.defaultBranch}'s latest
180
+ # results, and a laptop's work in progress or another branch's results cannot turn this red.
181
+ - run: bunx @crvouga/atlas report status --branch "\${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" --branch ${context.defaultBranch} --ignore-modified${context.specs ? ' --require-all-specs' : ''}
182
+ `;
183
+ }
184
+ /** The `AGENTS.md` section: what Atlas is in this project and the commands to use, replaced between its markers. */
185
+ function agentsSection(plan) {
186
+ const url = plan.server.url ?? '<the project\'s Atlas server>';
187
+ const lines = [
188
+ BEGIN,
189
+ '## Atlas: test evidence for this project',
190
+ '',
191
+ `This project sends the results of its test suites to an [Atlas](https://github.com/crvouga/atlas) server (${url}), which shows everyone one combined, live report. A check is identified by a stable id; the newest completed result of each check wins; unknown is never a pass. Set \`ATLAS_STORE=${url}\` and \`ATLAS_TOKEN\` (a \`publish\` token for sending, a \`read\` token for asking).`,
192
+ '',
193
+ '- **Machine-readable API:** `' + `${url}/api/openapi.json` + '` (OpenAPI 3.1). **Guide for agents:** `' + `${url}/llms.txt` + '`.',
194
+ '- **After you change behaviour,** run the suite, then send its results (publish even when it fails: a failure is evidence).',
195
+ ...plan.suites.flatMap(suite => ['', ` - **${suite.id}**: \`${suite.run}\``, ` then \`${suite.publish}\`` + (suite.stream ? ` (or stream: \`${suite.stream}\`)` : '')]),
196
+ '',
197
+ `- **Is it passing?** \`bunx @crvouga/atlas report status --branch ${plan.git.defaultBranch} --ignore-modified --format json\` (exit 0 passes, 1 does not, 2 means the command was wrong). The failing checks are in \`failing\`.`,
198
+ plan.specs.directory ? `- **Specs** live in \`${plan.specs.directory}/\` (${Object.entries(plan.specs.kinds).map(([kind, count]) => `${count} ${kind}`).join(', ')}). Results are laid over them by path.` : '- No spec files were found. Pass `--spec <path>` or `--specs <dir>` to `publish` to link checks to specs.',
199
+ '- Keep each check\'s `id` stable across runs: that is what lets a new result replace an old one.',
200
+ '- Evidence (screenshots, videos, traces, logs) rides on checks: https://github.com/crvouga/atlas/blob/main/docs/extending.md',
201
+ END
202
+ ];
203
+ return `${lines.join('\n')}\n`;
204
+ }
205
+ /** The new text of a file that holds a marked section: the section replaced in place, or appended, or the file created. */
206
+ function withSection(current, section) {
207
+ if (current === null)
208
+ return section;
209
+ const start = current.indexOf(BEGIN);
210
+ const end = current.indexOf(END);
211
+ if (start >= 0 && end > start)
212
+ return `${current.slice(0, start)}${section.trimEnd()}${current.slice(end + END.length)}`;
213
+ return `${current.trimEnd()}\n\n${section}`;
214
+ }
215
+ /**
216
+ * Detects the project in `options.directory` and returns the plan, writing its files when `options.write` is set.
217
+ * Never throws for a project it cannot understand: it says so in `warnings` and plans what it can (the `AGENTS.md`
218
+ * section with no suites, so the next agent knows to add them). Throws only for a directory that does not exist.
219
+ *
220
+ * @example
221
+ * const plan = await planInit({ directory: process.cwd(), server: 'https://atlas.example.com' });
222
+ * for (const suite of plan.suites) console.log(suite.run, '→', suite.publish);
223
+ */
224
+ export async function planInit(options) {
225
+ const directory = path.resolve(options.directory);
226
+ if (!existsSync(directory) || !statSync(directory).isDirectory())
227
+ throw new Error(`Not a directory: ${options.directory}`);
228
+ const env = options.env ?? {};
229
+ const git = options.git ?? defaultGit;
230
+ const pkg = readJson(path.join(directory, 'package.json'));
231
+ const manager = packageManagerOf(directory, pkg);
232
+ const specs = await specsOf(directory);
233
+ const configured = env.ATLAS_STORE?.split(/[\n,]+/).map(item => item.trim()).find(item => /^https?:\/\//i.test(item));
234
+ const serverUrl = (options.server ?? configured ?? null)?.replace(/\/+$/, '') ?? null;
235
+ const warnings = [];
236
+ const exec = EXEC[manager ?? 'npm'];
237
+ const context = { exec, install: INSTALL_DEV[manager ?? 'npm'], specs: specs.directory, server: serverUrl };
238
+ const suites = runnersOf(directory, pkg).map(found => suiteOf(found, context));
239
+ if (!suites.length)
240
+ warnings.push('No test runner was recognised (looked for vitest, jest, playwright, cypress, cucumber, mocha, codeceptjs, node --test, bun test, pytest, go test and Atlas statecharts). Add one suite by hand: run it so it writes a JUnit XML or CTRF file, then `atlas report publish --producer <name> --junit <file>`.');
241
+ if (!serverUrl)
242
+ warnings.push('No server given. Pass --server https://… (or set ATLAS_STORE) so the plan can name it; until then the generated text says "the project\'s Atlas server".');
243
+ if (!specs.directory)
244
+ warnings.push('No spec files (Gherkin, Quint, DMN, Alloy, OpenSLO, SCXML) were found in specs/, features/ or the usual places. Results are still reported; they belong to no spec until `--specs <dir>` or `--spec <path>` links them.');
245
+ if (existsSync(path.join(directory, 'atlas.config.ts')) && suites.some(suite => suite.runner === 'atlas-statecharts'))
246
+ warnings.push('atlas.config.ts found: the statechart suite runs the whole plan (`atlas run --mode fast`); see docs/statecharts.md to choose modes and shards.');
247
+ const branch = git(['branch', '--show-current'], directory);
248
+ const remoteHead = git(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], directory)?.replace(/^origin\//, '') ?? null;
249
+ const defaultBranch = remoteHead ?? (git(['rev-parse', '--verify', 'main'], directory) ? 'main' : git(['rev-parse', '--verify', 'master'], directory) ? 'master' : 'main');
250
+ const ci = existsSync(path.join(directory, '.github')) || /github\.com/.test(git(['remote', 'get-url', 'origin'], directory) ?? '') ? 'github-actions' : existsSync(path.join(directory, '.gitlab-ci.yml')) ? 'gitlab-ci' : existsSync(path.join(directory, '.circleci')) ? 'circleci' : 'none';
251
+ if (ci === 'gitlab-ci' || ci === 'circleci')
252
+ warnings.push(`${ci} detected: no workflow is generated for it. In its jobs run each suite, then its publish command (with ATLAS_STORE and ATLAS_TOKEN set from secrets), then the gate command from AGENTS.md.`);
253
+ const base = { suites, specs, server: { url: serverUrl, tokenEnv: 'ATLAS_TOKEN' }, git: { branch, defaultBranch } };
254
+ const wanted = [
255
+ { path: 'AGENTS.md', purpose: 'Tells every agent working in this repository how to send and read test evidence.', next: current => withSection(current, agentsSection(base)) }
256
+ ];
257
+ if (ci === 'github-actions' && suites.length)
258
+ wanted.push({ path: '.github/workflows/atlas.yml', purpose: 'Runs each suite in CI, sends its results to the server even when it fails, and gates on the combined report.', next: () => githubWorkflow(suites, { manager, defaultBranch, specs: specs.directory }) });
259
+ const files = [];
260
+ for (const item of wanted) {
261
+ const full = path.join(directory, item.path);
262
+ const current = existsSync(full) ? await readFile(full, 'utf8') : null;
263
+ const content = item.next(current);
264
+ // A workflow that exists and was not generated by this tool is the project's own: never overwritten.
265
+ const foreign = item.path.startsWith('.github/') && current !== null && !current.startsWith('# Sends every suite');
266
+ if (foreign) {
267
+ warnings.push(`${item.path} already exists and was not generated by atlas init; left alone. The generated version is in this plan's files.`);
268
+ files.push({ path: item.path, action: 'unchanged', purpose: item.purpose, content });
269
+ continue;
270
+ }
271
+ files.push({ path: item.path, action: current === null ? 'create' : current === content ? 'unchanged' : 'update', purpose: item.purpose, content });
272
+ }
273
+ const written = [];
274
+ if (options.write) {
275
+ for (const file of files) {
276
+ if (file.action === 'unchanged')
277
+ continue;
278
+ await mkdir(path.dirname(path.join(directory, file.path)), { recursive: true });
279
+ await writeFile(path.join(directory, file.path), file.content);
280
+ written.push(file.path);
281
+ }
282
+ }
283
+ let verification = null;
284
+ if (options.verify && serverUrl) {
285
+ const token = serverToken(env);
286
+ const answer = await httpStore({ url: serverUrl, ...(token ? { token } : {}), retries: 1, ...(options.fetch ? { fetch: options.fetch } : {}) }).ping();
287
+ verification = { reachable: answer.ok || /\b(401|403)\b|wants a token|may not do that/.test(answer.error ?? ''), role: answer.role ?? null, canPublish: answer.role === 'publish' || answer.role === 'admin', error: answer.ok ? null : (answer.error ?? 'no answer') };
288
+ if (answer.ok && !verification.canPublish)
289
+ verification = { ...verification, error: `This token's role is "${answer.role}": it can look but not upload. Use a publish token for sending results.` };
290
+ }
291
+ else if (options.verify)
292
+ warnings.push('--verify needs a server: pass --server or set ATLAS_STORE.');
293
+ const nextSteps = [
294
+ ...(serverUrl ? [] : ['Decide which server to send results to (run one with `atlas serve`), then run this again with --server <url>.']),
295
+ ...(options.write ? [] : ['Review the plan, then run again with --write to create the files in `files`.']),
296
+ ...suites.flatMap(suite => suite.prerequisites.map(command => `Install what ${suite.id} needs: ${command}`)),
297
+ ...(serverUrl ? [`Set ATLAS_STORE=${serverUrl} and ATLAS_TOKEN (a publish token) in CI and on each machine that should send results.`, 'Run `atlas report doctor` to prove the server can be written to and read.'] : []),
298
+ ...suites.map(suite => `Prove ${suite.id}: run \`${suite.run}\`, then \`${suite.publish}\`, then open ${serverUrl ?? 'the server'} and find its results.`)
299
+ ];
300
+ const ok = verification ? verification.reachable && verification.canPublish && verification.error === null : true;
301
+ return InitPlanSchema.parse({ ok, directory, project: { name: typeof pkg?.name === 'string' ? pkg.name : path.basename(directory), packageManager: manager, languages: languagesOf(directory, pkg) }, git: base.git, ci, server: base.server, specs, suites, files, written, verification, nextSteps, warnings });
302
+ }
303
+ function languagesOf(directory, pkg) {
304
+ const languages = [];
305
+ if (pkg)
306
+ languages.push(existsSync(path.join(directory, 'tsconfig.json')) ? 'typescript' : 'javascript');
307
+ if (['pyproject.toml', 'requirements.txt', 'setup.py'].some(name => existsSync(path.join(directory, name))))
308
+ languages.push('python');
309
+ if (existsSync(path.join(directory, 'go.mod')))
310
+ languages.push('go');
311
+ return languages;
312
+ }
313
+ /** The plan as the text a person reads: what was found, what is planned, and what to do next. */
314
+ export function formatPlan(plan) {
315
+ const lines = [`Project ${plan.project.name ?? plan.directory}${plan.project.packageManager ? ` (${plan.project.packageManager})` : ''}, CI: ${plan.ci}, server: ${plan.server.url ?? 'not given'}`];
316
+ lines.push(plan.suites.length ? 'Suites:' : 'Suites: none recognised');
317
+ for (const suite of plan.suites)
318
+ lines.push(` ${suite.id} (${suite.evidence})`, ` run: ${suite.run}`, ` publish: ${suite.publish}`);
319
+ lines.push(plan.specs.directory ? `Specs: ${plan.specs.directory}/ (${Object.entries(plan.specs.kinds).map(([kind, count]) => `${count} ${kind}`).join(', ')})` : 'Specs: none found');
320
+ lines.push('Files:');
321
+ for (const file of plan.files)
322
+ lines.push(` ${plan.written.includes(file.path) ? 'wrote ' : file.action.padEnd(9)} ${file.path} ${file.action === 'unchanged' ? '' : `- ${file.purpose}`}`);
323
+ if (plan.verification)
324
+ lines.push(`Server: ${plan.verification.reachable ? 'reachable' : 'NOT reachable'}${plan.verification.role ? `, token role ${plan.verification.role}` : ''}${plan.verification.error ? ` — ${plan.verification.error}` : ''}`);
325
+ for (const warning of plan.warnings)
326
+ lines.push(`! ${warning}`);
327
+ lines.push('Next:');
328
+ plan.nextSteps.forEach((step, i) => lines.push(` ${i + 1}. ${step}`));
329
+ return `${lines.join('\n')}\n`;
330
+ }
331
+ /** The usage of `atlas init`, printed by `atlas init --help`. */
332
+ export const INIT_USAGE = `atlas init — integrate a project with an Atlas server
333
+
334
+ atlas init [--dir <project>] [--server <url>] [--write] [--verify] [--format json]
335
+
336
+ Looks at the project (package manager, test runners, spec files, CI) and prints a plan: for each suite the command
337
+ that runs it and the command that sends its results, a CI workflow, and an AGENTS.md section that tells the next
338
+ agent how to use Atlas here. Nothing is changed until --write; running it again changes nothing more.
339
+
340
+ --dir <path> the project (default: the current directory)
341
+ --server <url> the Atlas server results go to (default: an http(s) store in ATLAS_STORE)
342
+ --write create or update the files of the plan (AGENTS.md between its atlas markers, .github/workflows/atlas.yml)
343
+ --verify ask the server who this token is and whether it may upload (token: ATLAS_TOKEN)
344
+ --format json print the plan as one JSON document (schema: init-plan.schema.json)
345
+
346
+ Exit codes: 0 planned (and verified), 1 --verify failed, 2 the command was wrong.
347
+ `;
348
+ /** `atlas init`: parses the arguments, plans, prints the plan as text or JSON, and returns the exit code. */
349
+ export async function initMain(argv, io) {
350
+ if (argv.includes('--help') || argv.includes('-h')) {
351
+ io.out(INIT_USAGE);
352
+ return 0;
353
+ }
354
+ const flag = (name) => { const i = argv.indexOf(`--${name}`); return i >= 0 ? argv[i + 1] : undefined; };
355
+ const format = flag('format');
356
+ const json = format === 'json' || argv.includes('--json');
357
+ try {
358
+ if (format !== undefined && format !== 'json' && format !== 'text')
359
+ throw new Error(`--format takes text|json (got "${format}")`);
360
+ const plan = await planInit({ directory: path.resolve(io.cwd ?? process.cwd(), flag('dir') ?? '.'), ...(flag('server') ? { server: flag('server') } : {}), write: argv.includes('--write'), verify: argv.includes('--verify'), env: io.env ?? process.env, ...(io.fetch ? { fetch: io.fetch } : {}) });
361
+ io.out(json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlan(plan));
362
+ return plan.ok ? 0 : 1;
363
+ }
364
+ catch (error) {
365
+ const message = error instanceof Error ? error.message : String(error);
366
+ process.stderr.write(json ? `${JSON.stringify({ ok: false, error: { message, code: 'usage' } })}\n` : `atlas init: ${message}\n`);
367
+ return 2;
368
+ }
369
+ }
@@ -1,23 +1,50 @@
1
1
  /**
2
- * Overlay test-runner outcomes (JUnit XML) onto a report bundle. A harness run
3
- * only sees failures raised inside its own steps; assertions in the test body
4
- * surface through the runner's report instead.
2
+ * @module JUnit XML parsing
3
+ *
4
+ * Reads the `<testcase>` elements of a JUnit XML document without an XML library, and overlays test-runner
5
+ * outcomes (JUnit XML) onto a report bundle. A harness run only sees failures raised inside its own steps;
6
+ * assertions in the test body surface through the runner's report instead. Parsing is tolerant by design
7
+ * (nested suites, self-closing elements, CDATA) and never drops a case silently, because a result that goes
8
+ * missing reads as a pass. `ingest.ts` turns the cases into checks.
5
9
  */
6
10
  import type { HtmlReportBundle } from './recording-bundle.js';
11
+ /** One `<testcase>` as read, with the suite it is in. */
7
12
  export type JunitCase = {
13
+ /** The case's `file` attribute, else its suite's, else empty. */
8
14
  readonly file: string;
15
+ /** The `classname` attribute, which many runners use for the suite or the file. */
16
+ readonly classname: string;
17
+ /** The `name` of the `<testsuite>` the case is in. */
18
+ readonly suite: string;
19
+ /** Seconds, when the runner emits it. */
20
+ readonly time: number | null;
9
21
  readonly name: string;
10
22
  /** 1-based line of the test declaration, when the runner emits it. */
11
23
  readonly line: number | null;
24
+ /** `failed` when the case has a `<failure>` or an `<error>`, `skipped` when it has `<skipped>`, otherwise `passed`. */
12
25
  readonly status: 'passed' | 'failed' | 'skipped';
26
+ /** The failure's text (else its `message`, else `failed`), or null when the case did not fail. */
13
27
  readonly failure: string | null;
28
+ /** The case's `<system-out>`, when it has one. */
29
+ readonly stdout?: string;
30
+ /** The case's `<system-err>`, when it has one. */
31
+ readonly stderr?: string;
32
+ /** The case's `<properties>`, then its suite's: names a runner chose to record. */
33
+ readonly properties?: Readonly<Record<string, string>>;
14
34
  };
15
- /** Parse the `<testcase>` elements of a JUnit XML document. */
35
+ /** Parses every `<testcase>` of a JUnit XML document, in document order, whatever the nesting of its suites. Returns `[]` for text with none; never throws. */
16
36
  export declare function parseJunit(xml: string): JunitCase[];
17
37
  /**
18
38
  * The test case whose declaration precedes `sourceLine` in the same file and
19
- * is the nearest one (bodies follow declarations).
39
+ * is the nearest one (bodies follow declarations). Files match when equal or when one path ends with the other
40
+ * (after normalising backslashes). With one case in the file, or no line, the first case in the file is used.
41
+ * Null when no case is in the file, or none declared at or before the line.
20
42
  */
21
43
  export declare function matchJunitCase(cases: readonly JunitCase[], sourceFile: string, sourceLine: number | undefined): JunitCase | null;
22
- /** Apply runner outcomes / titles to every test with a known source file. */
44
+ /**
45
+ * Applies runner outcomes and titles to every test of a bundle that has a known source file and a matching case:
46
+ * the test takes the case's name as title, its failure as error (a harness error is kept), and fails if either the
47
+ * runner or the harness failed. The bundle fails if any test does. Tests without a match are returned unchanged;
48
+ * the input is not mutated.
49
+ */
23
50
  export declare function applyJunitOutcomes(bundle: HtmlReportBundle, cases: readonly JunitCase[]): HtmlReportBundle;
@@ -1,7 +1,11 @@
1
1
  /**
2
- * Overlay test-runner outcomes (JUnit XML) onto a report bundle. A harness run
3
- * only sees failures raised inside its own steps; assertions in the test body
4
- * surface through the runner's report instead.
2
+ * @module JUnit XML parsing
3
+ *
4
+ * Reads the `<testcase>` elements of a JUnit XML document without an XML library, and overlays test-runner
5
+ * outcomes (JUnit XML) onto a report bundle. A harness run only sees failures raised inside its own steps;
6
+ * assertions in the test body surface through the runner's report instead. Parsing is tolerant by design
7
+ * (nested suites, self-closing elements, CDATA) and never drops a case silently, because a result that goes
8
+ * missing reads as a pass. `ingest.ts` turns the cases into checks.
5
9
  */
6
10
  const ENTITIES = {
7
11
  '&lt;': '<',
@@ -21,24 +25,49 @@ function attr(tag, name) {
21
25
  return m === null ? null : decodeXml(m[1] ?? '');
22
26
  }
23
27
  function failureText(body) {
24
- const m = /<failure\b([^>]*)(?:\/>|>([\s\S]*?)<\/failure>)/.exec(body);
28
+ // `<failure>` is an assertion that did not hold, `<error>` an unexpected exception: both are a check that failed.
29
+ const m = /<(failure|error)\b([^>]*)(?:\/>|>([\s\S]*?)<\/\1>)/.exec(body);
25
30
  if (m === null)
26
31
  return null;
27
- const message = attr(m[1] ?? '', 'message');
28
- const inner = (m[2] ?? '').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
32
+ const message = attr(m[2] ?? '', 'message');
33
+ const inner = (m[3] ?? '').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
29
34
  const text = decodeXml(inner).trim();
30
35
  return text.length > 0 ? text : (message ?? 'failed');
31
36
  }
32
- function parseCase(tag, inner, suiteFile) {
37
+ function streamText(body, name) {
38
+ const found = new RegExp(`<${name}\\b[^>]*>([\\s\\S]*?)</${name}>`).exec(body);
39
+ if (!found)
40
+ return undefined;
41
+ const text = decodeXml((found[1] ?? '').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')).trim();
42
+ return text.length ? text : undefined;
43
+ }
44
+ function propertiesOf(body) {
45
+ const out = {};
46
+ for (const match of body.matchAll(/<property\b([^>]*?)\/?>/g)) {
47
+ const name = attr(match[1] ?? '', 'name');
48
+ const value = attr(match[1] ?? '', 'value');
49
+ if (name)
50
+ out[name] = value ?? '';
51
+ }
52
+ return out;
53
+ }
54
+ function parseCase(tag, inner, suiteFile, suiteName, suiteProperties = {}) {
33
55
  const lineRaw = attr(tag, 'line');
34
56
  const line = lineRaw === null ? null : Number(lineRaw);
35
57
  const failure = failureText(inner);
58
+ const time = Number(attr(tag, 'time'));
36
59
  return {
37
60
  file: attr(tag, 'file') ?? suiteFile,
61
+ classname: attr(tag, 'classname') ?? '',
62
+ suite: suiteName,
63
+ time: attr(tag, 'time') !== null && Number.isFinite(time) ? time : null,
38
64
  name: attr(tag, 'name') ?? '',
39
65
  line: line !== null && Number.isFinite(line) ? line : null,
40
66
  status: caseStatus(failure, inner),
41
67
  failure,
68
+ ...(streamText(inner, 'system-out') ? { stdout: streamText(inner, 'system-out') } : {}),
69
+ ...(streamText(inner, 'system-err') ? { stderr: streamText(inner, 'system-err') } : {}),
70
+ ...(Object.keys({ ...suiteProperties, ...propertiesOf(inner) }).length ? { properties: { ...suiteProperties, ...propertiesOf(inner) } } : {}),
42
71
  };
43
72
  }
44
73
  function caseStatus(failure, inner) {
@@ -48,22 +77,52 @@ function caseStatus(failure, inner) {
48
77
  return 'skipped';
49
78
  return 'passed';
50
79
  }
51
- function parseSuite(tag, body, out) {
52
- const suiteFile = attr(tag, 'file') ?? '';
53
- const caseRe = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
54
- for (let c = caseRe.exec(body); c; c = caseRe.exec(body)) {
55
- out.push(parseCase(c[1] ?? '', c[2] ?? '', suiteFile));
56
- }
57
- }
58
- /** Parse the `<testcase>` elements of a JUnit XML document. */
59
- export function parseJunit(xml) {
80
+ /**
81
+ * Every `<testcase>` of the document, whatever the nesting of its suites: a suite inside a suite, a case after
82
+ * an inner suite, an empty self-closing suite. Each case is read with the suite it is in, and that suite's own
83
+ * `<properties>`. A case is never skipped silently, because a result that goes missing reads as a pass.
84
+ */
85
+ function allCases(xml) {
60
86
  const cases = [];
61
- const suiteRe = /<testsuite\b([^>]*)>([\s\S]*?)<\/testsuite>/g;
62
- for (let suite = suiteRe.exec(xml); suite; suite = suiteRe.exec(xml)) {
63
- parseSuite(suite[1] ?? '', suite[2] ?? '', cases);
87
+ const stack = [];
88
+ const tags = /<(\/?)(testsuite|testcase|properties)\b([^>]*?)(\/?)>/g;
89
+ for (let tag = tags.exec(xml); tag; tag = tags.exec(xml)) {
90
+ const [, closing, name, attributes = '', selfClosing] = tag;
91
+ if (name === 'testsuite') {
92
+ if (closing) {
93
+ stack.pop();
94
+ continue;
95
+ }
96
+ const suite = { file: attr(attributes, 'file') ?? '', name: attr(attributes, 'name') ?? '', properties: {} };
97
+ if (!selfClosing)
98
+ stack.push(suite);
99
+ }
100
+ else if (name === 'testcase' && !closing) {
101
+ let inner = '';
102
+ if (!selfClosing) {
103
+ const end = xml.indexOf('</testcase>', tags.lastIndex);
104
+ if (end < 0)
105
+ continue;
106
+ inner = xml.slice(tags.lastIndex, end);
107
+ tags.lastIndex = end + '</testcase>'.length;
108
+ }
109
+ const suite = stack.at(-1);
110
+ cases.push(parseCase(attributes, inner, suite?.file ?? '', suite?.name ?? '', Object.assign({}, ...stack.map(item => item.properties))));
111
+ }
112
+ else if (name === 'properties' && !closing && !selfClosing && stack.length) {
113
+ const end = xml.indexOf('</properties>', tags.lastIndex);
114
+ if (end < 0)
115
+ continue;
116
+ Object.assign(stack.at(-1).properties, propertiesOf(xml.slice(tags.lastIndex, end)));
117
+ tags.lastIndex = end + '</properties>'.length;
118
+ }
64
119
  }
65
120
  return cases;
66
121
  }
122
+ /** Parses every `<testcase>` of a JUnit XML document, in document order, whatever the nesting of its suites. Returns `[]` for text with none; never throws. */
123
+ export function parseJunit(xml) {
124
+ return allCases(xml);
125
+ }
67
126
  function sameFile(a, b) {
68
127
  const na = a.replace(/\\/g, '/');
69
128
  const nb = b.replace(/\\/g, '/');
@@ -71,7 +130,9 @@ function sameFile(a, b) {
71
130
  }
72
131
  /**
73
132
  * The test case whose declaration precedes `sourceLine` in the same file and
74
- * is the nearest one (bodies follow declarations).
133
+ * is the nearest one (bodies follow declarations). Files match when equal or when one path ends with the other
134
+ * (after normalising backslashes). With one case in the file, or no line, the first case in the file is used.
135
+ * Null when no case is in the file, or none declared at or before the line.
75
136
  */
76
137
  export function matchJunitCase(cases, sourceFile, sourceLine) {
77
138
  const inFile = cases.filter((c) => sameFile(c.file, sourceFile));
@@ -105,7 +166,12 @@ function overlayTest(test, c) {
105
166
  },
106
167
  };
107
168
  }
108
- /** Apply runner outcomes / titles to every test with a known source file. */
169
+ /**
170
+ * Applies runner outcomes and titles to every test of a bundle that has a known source file and a matching case:
171
+ * the test takes the case's name as title, its failure as error (a harness error is kept), and fails if either the
172
+ * runner or the harness failed. The bundle fails if any test does. Tests without a match are returned unchanged;
173
+ * the input is not mutated.
174
+ */
109
175
  export function applyJunitOutcomes(bundle, cases) {
110
176
  const tests = bundle.tests.map((test) => {
111
177
  const { sourceFile, sourceLine } = test.report.meta;
@@ -1,16 +1,36 @@
1
1
  import type { FlatFrame } from './frames.js';
2
2
  import type { Route } from './hash.js';
3
3
  import type { ReportData } from './types.js';
4
+ /** What {@link onReportKeyDown} may ask the host to do, and what it reads from it. */
4
5
  export type KeyNavHandlers = {
6
+ /** Opens or closes the shortcut help. */
5
7
  toggleHelp: () => void;
8
+ /** Whether the help is open. */
6
9
  showHelp: () => boolean;
10
+ /** Whether the command palette is open; while it is, no other shortcut acts. */
7
11
  showPalette: () => boolean;
8
12
  togglePalette: () => void;
13
+ /** Stops frame playback. */
9
14
  stopPlay: () => void;
15
+ /** Goes to a route (writes the URL hash). */
10
16
  navigate: (route: Route) => void;
17
+ /** Selects a frame, optionally of a given client (`node`). */
11
18
  goFrame: (frame: FlatFrame | null, node?: string) => void;
12
19
  togglePlay: () => void;
20
+ /** Sets a URL param on the current route; the empty string removes it. */
13
21
  setParam: (key: string, value: string) => void;
22
+ /** The current route. */
14
23
  route: () => Route;
15
24
  };
25
+ /**
26
+ * Handles one `keydown` for the viewer. Keys: Cmd/Ctrl+K palette; `?` help; Escape closes help or the palette,
27
+ * else stops playback and returns to the list. On a test page: Left/Right/Home/End scrub frames, Space plays,
28
+ * Up/Down jump between steps, `n`/`N` between failures, `[`/`]` to the previous/next test (keeping the list's
29
+ * params), `1`-`9` pick a client, and, when the frame has a UI snapshot, `m` single/multi view, `a` aside, `p`
30
+ * press targets, `f` cycles zoom (fit, 50, 75, 100).
31
+ *
32
+ * Cmd/Ctrl+K and Escape-closing-the-palette work anywhere. Every other key is ignored while the palette is open and
33
+ * while focus is in an input, textarea, select, button, link or editable element; the test-page keys also need the
34
+ * route to be a test that is in `data`.
35
+ */
16
36
  export declare function onReportKeyDown(e: KeyboardEvent, data: ReportData, h: KeyNavHandlers): void;
@@ -85,6 +85,17 @@ function viewKeys(e, ctx) {
85
85
  ctx.h.goFrame(ctx.sel, ids[n - 1] ?? '');
86
86
  }
87
87
  }
88
+ /**
89
+ * Handles one `keydown` for the viewer. Keys: Cmd/Ctrl+K palette; `?` help; Escape closes help or the palette,
90
+ * else stops playback and returns to the list. On a test page: Left/Right/Home/End scrub frames, Space plays,
91
+ * Up/Down jump between steps, `n`/`N` between failures, `[`/`]` to the previous/next test (keeping the list's
92
+ * params), `1`-`9` pick a client, and, when the frame has a UI snapshot, `m` single/multi view, `a` aside, `p`
93
+ * press targets, `f` cycles zoom (fit, 50, 75, 100).
94
+ *
95
+ * Cmd/Ctrl+K and Escape-closing-the-palette work anywhere. Every other key is ignored while the palette is open and
96
+ * while focus is in an input, textarea, select, button, link or editable element; the test-page keys also need the
97
+ * route to be a test that is in `data`.
98
+ */
88
99
  export function onReportKeyDown(e, data, h) {
89
100
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
90
101
  e.preventDefault();