@crvouga/atlas 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (429) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +178 -604
  3. package/dist/bun/src/driver.d.ts +41 -7
  4. package/dist/bun/src/driver.js +47 -6
  5. package/dist/bun/src/index.d.ts +8 -0
  6. package/dist/bun/src/index.js +8 -0
  7. package/dist/core/src/cli.d.ts +14 -1
  8. package/dist/core/src/cli.js +30 -1
  9. package/dist/core/src/config.d.ts +70 -4
  10. package/dist/core/src/config.js +62 -3
  11. package/dist/core/src/events/cloudevents.d.ts +54 -9
  12. package/dist/core/src/events/cloudevents.js +60 -9
  13. package/dist/core/src/graph.d.ts +21 -0
  14. package/dist/core/src/graph.js +26 -0
  15. package/dist/core/src/index.d.ts +11 -0
  16. package/dist/core/src/index.js +11 -0
  17. package/dist/core/src/lint.d.ts +30 -0
  18. package/dist/core/src/lint.js +13 -0
  19. package/dist/core/src/plan.d.ts +39 -1
  20. package/dist/core/src/plan.js +28 -1
  21. package/dist/core/src/privacy.d.ts +29 -2
  22. package/dist/core/src/privacy.js +19 -2
  23. package/dist/core/src/report/formats.d.ts +34 -4
  24. package/dist/core/src/report/formats.js +24 -4
  25. package/dist/core/src/report/markdown.d.ts +15 -0
  26. package/dist/core/src/report/markdown.js +8 -0
  27. package/dist/core/src/run/clients.d.ts +44 -5
  28. package/dist/core/src/run/clients.js +46 -2
  29. package/dist/core/src/run/execute.d.ts +69 -16
  30. package/dist/core/src/run/execute.js +67 -16
  31. package/dist/core/src/run/frames.d.ts +13 -4
  32. package/dist/core/src/run/frames.js +19 -4
  33. package/dist/core/src/run/http.d.ts +34 -1
  34. package/dist/core/src/run/http.js +14 -1
  35. package/dist/core/src/run/types.d.ts +76 -3
  36. package/dist/core/src/run/types.js +19 -3
  37. package/dist/core/src/signals.d.ts +49 -5
  38. package/dist/core/src/signals.js +22 -5
  39. package/dist/core/src/spec/compose.d.ts +23 -1
  40. package/dist/core/src/spec/compose.js +10 -1
  41. package/dist/core/src/spec/index.d.ts +9 -1
  42. package/dist/core/src/spec/index.js +9 -1
  43. package/dist/core/src/spec/load.d.ts +22 -1
  44. package/dist/core/src/spec/load.js +31 -1
  45. package/dist/core/src/spec/parse.d.ts +16 -2
  46. package/dist/core/src/spec/parse.js +25 -2
  47. package/dist/core/src/spec/scxml.d.ts +25 -4
  48. package/dist/core/src/spec/scxml.js +36 -4
  49. package/dist/core/src/spec/types.d.ts +52 -1
  50. package/dist/core/src/spec/types.js +24 -0
  51. package/dist/detox/src/index.d.ts +54 -11
  52. package/dist/detox/src/index.js +45 -7
  53. package/dist/playwright/src/driver.d.ts +39 -3
  54. package/dist/playwright/src/driver.js +27 -3
  55. package/dist/playwright/src/index.d.ts +10 -0
  56. package/dist/playwright/src/index.js +10 -0
  57. package/dist/playwright/src/media.d.ts +50 -4
  58. package/dist/playwright/src/media.js +55 -4
  59. package/dist/playwright/src/signals.d.ts +44 -5
  60. package/dist/playwright/src/signals.js +29 -5
  61. package/dist/reports/src/attachment-files.d.ts +44 -0
  62. package/dist/reports/src/attachment-files.js +121 -0
  63. package/dist/reports/src/attachment-response.d.ts +31 -0
  64. package/dist/reports/src/attachment-response.js +55 -0
  65. package/dist/reports/src/aws-credentials.d.ts +48 -0
  66. package/dist/reports/src/aws-credentials.js +221 -0
  67. package/dist/reports/src/browser-navigation.d.ts +14 -0
  68. package/dist/reports/src/browser-navigation.js +7 -0
  69. package/dist/reports/src/bundle.d.ts +34 -2
  70. package/dist/reports/src/bundle.js +38 -2
  71. package/dist/reports/src/catalog.d.ts +34 -3
  72. package/dist/reports/src/catalog.js +17 -3
  73. package/dist/reports/src/cli-stores.d.ts +36 -0
  74. package/dist/reports/src/cli-stores.js +73 -0
  75. package/dist/reports/src/clock.d.ts +7 -2
  76. package/dist/reports/src/clock.js +5 -2
  77. package/dist/reports/src/completion-order.d.ts +18 -5
  78. package/dist/reports/src/completion-order.js +18 -5
  79. package/dist/reports/src/compose.d.ts +80 -0
  80. package/dist/reports/src/compose.js +117 -0
  81. package/dist/reports/src/copy-store.d.ts +29 -0
  82. package/dist/reports/src/copy-store.js +52 -0
  83. package/dist/reports/src/directory-outline.d.ts +38 -0
  84. package/dist/reports/src/directory-outline.js +103 -0
  85. package/dist/reports/src/directory-store.d.ts +20 -0
  86. package/dist/reports/src/directory-store.js +166 -0
  87. package/dist/reports/src/doctor.d.ts +29 -0
  88. package/dist/reports/src/doctor.js +83 -0
  89. package/dist/reports/src/effective-delta.d.ts +34 -0
  90. package/dist/reports/src/effective-delta.js +25 -0
  91. package/dist/reports/src/evidence-detail.d.ts +64 -0
  92. package/dist/reports/src/evidence-detail.js +92 -0
  93. package/dist/reports/src/evidence-live.d.ts +40 -4
  94. package/dist/reports/src/evidence-live.js +52 -4
  95. package/dist/reports/src/evidence-types.d.ts +70 -1
  96. package/dist/reports/src/evidence-types.js +5 -1
  97. package/dist/reports/src/evidence-view.d.ts +58 -6
  98. package/dist/reports/src/evidence-view.js +44 -5
  99. package/dist/reports/src/explorer-types.d.ts +40 -3
  100. package/dist/reports/src/explorer-types.js +8 -0
  101. package/dist/reports/src/explorer-versions.d.ts +9 -1
  102. package/dist/reports/src/explorer-versions.js +18 -1
  103. package/dist/reports/src/export-evidence.d.ts +16 -1
  104. package/dist/reports/src/export-evidence.js +52 -1
  105. package/dist/reports/src/fetcher.d.ts +27 -3
  106. package/dist/reports/src/fetcher.js +11 -3
  107. package/dist/reports/src/format-date.d.ts +6 -0
  108. package/dist/reports/src/format-date.js +6 -0
  109. package/dist/reports/src/format.d.ts +16 -0
  110. package/dist/reports/src/format.js +10 -0
  111. package/dist/reports/src/frames.d.ts +36 -3
  112. package/dist/reports/src/frames.js +22 -3
  113. package/dist/reports/src/hash.d.ts +18 -0
  114. package/dist/reports/src/hash.js +13 -0
  115. package/dist/reports/src/host.d.ts +29 -0
  116. package/dist/reports/src/host.js +64 -0
  117. package/dist/reports/src/html-frame.d.ts +5 -6
  118. package/dist/reports/src/html-frame.js +12 -1
  119. package/dist/reports/src/http-store.d.ts +52 -0
  120. package/dist/reports/src/http-store.js +145 -0
  121. package/dist/reports/src/http.d.ts +7 -0
  122. package/dist/reports/src/http.js +6 -0
  123. package/dist/reports/src/ingest.d.ts +54 -0
  124. package/dist/reports/src/ingest.js +210 -0
  125. package/dist/reports/src/init.d.ts +40 -0
  126. package/dist/reports/src/init.js +364 -0
  127. package/dist/reports/src/junit.d.ts +33 -6
  128. package/dist/reports/src/junit.js +87 -21
  129. package/dist/reports/src/keyboard.d.ts +20 -0
  130. package/dist/reports/src/keyboard.js +11 -0
  131. package/dist/reports/src/keys.d.ts +21 -3
  132. package/dist/reports/src/keys.js +21 -3
  133. package/dist/reports/src/layer-axes.d.ts +48 -0
  134. package/dist/reports/src/layer-axes.js +71 -0
  135. package/dist/reports/src/layer-catalog.d.ts +28 -1
  136. package/dist/reports/src/layer-catalog.js +19 -2
  137. package/dist/reports/src/layer-merge.d.ts +98 -9
  138. package/dist/reports/src/layer-merge.js +154 -33
  139. package/dist/reports/src/layer-source.d.ts +18 -0
  140. package/dist/reports/src/layer-source.js +14 -0
  141. package/dist/reports/src/layer-store.d.ts +42 -0
  142. package/dist/reports/src/layer-store.js +63 -5
  143. package/dist/reports/src/layer-types.d.ts +34 -1
  144. package/dist/reports/src/layers-route.d.ts +47 -4
  145. package/dist/reports/src/layers-route.js +88 -12
  146. package/dist/reports/src/media-types.d.ts +11 -0
  147. package/dist/reports/src/media-types.js +22 -0
  148. package/dist/reports/src/namespace-guard.d.ts +10 -1
  149. package/dist/reports/src/namespace-guard.js +10 -1
  150. package/dist/reports/src/navigation.d.ts +12 -0
  151. package/dist/reports/src/navigation.js +7 -0
  152. package/dist/reports/src/object-io.d.ts +11 -4
  153. package/dist/reports/src/object-io.js +4 -4
  154. package/dist/reports/src/object-store-conformance.d.ts +27 -0
  155. package/dist/reports/src/object-store-conformance.js +87 -0
  156. package/dist/reports/src/object-store.d.ts +33 -24
  157. package/dist/reports/src/object-store.js +17 -24
  158. package/dist/reports/src/parse-with.d.ts +16 -0
  159. package/dist/reports/src/parse-with.js +14 -0
  160. package/dist/reports/src/performance-part.d.ts +25 -1
  161. package/dist/reports/src/performance-part.js +15 -1
  162. package/dist/reports/src/performance.d.ts +22 -2
  163. package/dist/reports/src/performance.js +12 -2
  164. package/dist/reports/src/persist.d.ts +12 -3
  165. package/dist/reports/src/persist.js +12 -3
  166. package/dist/reports/src/phone.d.ts +18 -1
  167. package/dist/reports/src/phone.js +18 -1
  168. package/dist/reports/src/protocol.d.ts +30 -4
  169. package/dist/reports/src/protocol.js +15 -4
  170. package/dist/reports/src/publication.d.ts +21 -1
  171. package/dist/reports/src/publication.js +21 -1
  172. package/dist/reports/src/publish-checks.d.ts +35 -0
  173. package/dist/reports/src/publish-checks.js +122 -0
  174. package/dist/reports/src/publish.d.ts +44 -9
  175. package/dist/reports/src/publish.js +40 -7
  176. package/dist/reports/src/read.d.ts +46 -16
  177. package/dist/reports/src/read.js +38 -14
  178. package/dist/reports/src/recording-bundle.d.ts +13 -2
  179. package/dist/reports/src/recording-bundle.js +4 -2
  180. package/dist/reports/src/recording-keys.d.ts +15 -1
  181. package/dist/reports/src/recording-keys.js +15 -1
  182. package/dist/reports/src/remote-report.d.ts +28 -7
  183. package/dist/reports/src/remote-report.js +15 -7
  184. package/dist/reports/src/report-cli.d.ts +51 -0
  185. package/dist/reports/src/report-cli.js +485 -0
  186. package/dist/reports/src/report-index.d.ts +99 -0
  187. package/dist/reports/src/report-index.js +337 -0
  188. package/dist/reports/src/report-loader.d.ts +10 -0
  189. package/dist/reports/src/report-loader.js +6 -0
  190. package/dist/reports/src/report-parts.d.ts +29 -3
  191. package/dist/reports/src/report-parts.js +40 -3
  192. package/dist/reports/src/report-status.d.ts +48 -0
  193. package/dist/reports/src/report-status.js +156 -0
  194. package/dist/reports/src/report-types.d.ts +73 -0
  195. package/dist/reports/src/report-types.js +3 -0
  196. package/dist/reports/src/reporter.d.ts +67 -0
  197. package/dist/reports/src/reporter.js +128 -0
  198. package/dist/reports/src/reports-explorer-route.d.ts +541 -0
  199. package/dist/reports/src/reports-explorer-route.js +11 -0
  200. package/dist/reports/src/reports-list-route.d.ts +9 -0
  201. package/dist/reports/src/reports-list-route.js +9 -0
  202. package/dist/reports/src/reports-route.d.ts +561 -22
  203. package/dist/reports/src/reports-route.js +11 -3
  204. package/dist/reports/src/reports-store.d.ts +19 -2
  205. package/dist/reports/src/reports-store.js +10 -2
  206. package/dist/reports/src/resilient-cache.d.ts +14 -1
  207. package/dist/reports/src/resilient-cache.js +8 -1
  208. package/dist/reports/src/retention.d.ts +17 -1
  209. package/dist/reports/src/retention.js +5 -1
  210. package/dist/reports/src/run-bundle.d.ts +63 -0
  211. package/dist/reports/src/run-bundle.js +159 -0
  212. package/dist/reports/src/s3-store.d.ts +79 -0
  213. package/dist/reports/src/s3-store.js +295 -0
  214. package/dist/reports/src/scenario.d.ts +14 -4
  215. package/dist/reports/src/scenario.js +5 -4
  216. package/dist/reports/src/sigv4.d.ts +72 -0
  217. package/dist/reports/src/sigv4.js +96 -0
  218. package/dist/reports/src/snapshot-bundle.d.ts +24 -1
  219. package/dist/reports/src/snapshot-bundle.js +27 -2
  220. package/dist/reports/src/snapshot-surface.d.ts +20 -9
  221. package/dist/reports/src/snapshot-surface.js +32 -19
  222. package/dist/reports/src/source-content.d.ts +23 -0
  223. package/dist/reports/src/source-content.js +30 -1
  224. package/dist/reports/src/source-entries.d.ts +13 -0
  225. package/dist/reports/src/source-entries.js +6 -0
  226. package/dist/reports/src/source-part.d.ts +15 -0
  227. package/dist/reports/src/source-part.js +8 -0
  228. package/dist/reports/src/source-reader.d.ts +11 -0
  229. package/dist/reports/src/source-reader.js +5 -0
  230. package/dist/reports/src/spec-evidence.d.ts +12 -2
  231. package/dist/reports/src/spec-evidence.js +6 -2
  232. package/dist/reports/src/spec-inventory.d.ts +15 -1
  233. package/dist/reports/src/spec-inventory.js +22 -1
  234. package/dist/reports/src/spec-link.d.ts +28 -0
  235. package/dist/reports/src/spec-link.js +70 -0
  236. package/dist/reports/src/spec-set.d.ts +63 -0
  237. package/dist/reports/src/spec-set.js +177 -0
  238. package/dist/reports/src/status.d.ts +10 -0
  239. package/dist/reports/src/status.js +5 -0
  240. package/dist/reports/src/store-prefix.d.ts +28 -0
  241. package/dist/reports/src/store-prefix.js +22 -0
  242. package/dist/reports/src/store-url.d.ts +89 -0
  243. package/dist/reports/src/store-url.js +160 -0
  244. package/dist/reports/src/test-catalog-entity.d.ts +11 -0
  245. package/dist/reports/src/test-catalog-entity.js +6 -0
  246. package/dist/reports/src/types.d.ts +6 -1
  247. package/dist/reports/src/use-report-nav.d.ts +35 -0
  248. package/dist/reports/src/use-report-nav.js +12 -0
  249. package/dist/schema/src/api.d.ts +799 -0
  250. package/dist/schema/src/api.js +201 -0
  251. package/dist/schema/src/cli-results.d.ts +210 -0
  252. package/dist/schema/src/cli-results.js +124 -0
  253. package/dist/schema/src/common.d.ts +53 -0
  254. package/dist/schema/src/common.js +42 -0
  255. package/dist/schema/src/index.d.ts +14 -0
  256. package/dist/schema/src/index.js +14 -0
  257. package/dist/schema/src/layer.d.ts +2273 -0
  258. package/dist/schema/src/layer.js +462 -0
  259. package/dist/schema/src/manifest.d.ts +95 -0
  260. package/dist/schema/src/manifest.js +84 -0
  261. package/dist/schema/src/report-sources.d.ts +28 -0
  262. package/dist/schema/src/report-sources.js +25 -0
  263. package/dist/schema/src/runs-index.d.ts +16 -0
  264. package/dist/schema/src/runs-index.js +15 -0
  265. package/dist/schema/src/server-config.d.ts +93 -0
  266. package/dist/schema/src/server-config.js +73 -0
  267. package/dist/schema/src/spec.d.ts +69 -0
  268. package/dist/schema/src/spec.js +58 -0
  269. package/dist/server/src/app.d.ts +62 -0
  270. package/dist/server/src/app.js +419 -0
  271. package/dist/server/src/auth.d.ts +41 -0
  272. package/dist/server/src/auth.js +59 -0
  273. package/dist/server/src/config.d.ts +83 -0
  274. package/dist/server/src/config.js +187 -0
  275. package/dist/server/src/dev.d.ts +12 -0
  276. package/dist/server/src/dev.js +80 -0
  277. package/dist/server/src/events.d.ts +29 -0
  278. package/dist/server/src/events.js +101 -0
  279. package/dist/server/src/host.d.ts +30 -0
  280. package/dist/server/src/host.js +115 -0
  281. package/dist/server/src/index.d.ts +16 -0
  282. package/dist/server/src/index.js +16 -0
  283. package/dist/server/src/llms.d.ts +19 -0
  284. package/dist/server/src/llms.js +63 -0
  285. package/dist/server/src/main.d.ts +30 -0
  286. package/dist/server/src/main.js +112 -0
  287. package/dist/server/src/openapi.d.ts +59 -0
  288. package/dist/server/src/openapi.js +374 -0
  289. package/dist/server/src/problem.d.ts +8 -0
  290. package/dist/server/src/problem.js +39 -0
  291. package/dist/server/src/site.d.ts +13 -0
  292. package/dist/server/src/site.js +43 -0
  293. package/dist/server/src/store-api.d.ts +28 -0
  294. package/dist/server/src/store-api.js +159 -0
  295. package/dist/server/src/validate.d.ts +71 -0
  296. package/dist/server/src/validate.js +197 -0
  297. package/dist/vitest/src/index.d.ts +27 -5
  298. package/dist/vitest/src/index.js +18 -5
  299. package/dist/webdriver/src/driver.d.ts +63 -11
  300. package/dist/webdriver/src/driver.js +57 -6
  301. package/dist/webdriver/src/index.d.ts +8 -0
  302. package/dist/webdriver/src/index.js +1 -0
  303. package/dist/webdriver/src/protocol.d.ts +75 -6
  304. package/dist/webdriver/src/protocol.js +79 -6
  305. package/docs/agents.md +137 -0
  306. package/docs/deploying.md +345 -0
  307. package/docs/dogfooding.md +175 -0
  308. package/docs/extending.md +242 -0
  309. package/docs/integrating.md +485 -0
  310. package/docs/reference/bun.md +141 -0
  311. package/docs/reference/cli.md +146 -0
  312. package/docs/reference/core.md +2426 -0
  313. package/docs/reference/detox.md +150 -0
  314. package/docs/reference/http-api.md +303 -0
  315. package/docs/reference/playwright.md +303 -0
  316. package/docs/reference/reports.md +7098 -0
  317. package/docs/reference/schema.md +1651 -0
  318. package/docs/reference/server.md +685 -0
  319. package/docs/reference/vitest.md +45 -0
  320. package/docs/reference/webdriver.md +330 -0
  321. package/docs/report-backends.md +144 -0
  322. package/docs/report-contract.md +400 -0
  323. package/docs/reporting-issues.md +160 -0
  324. package/docs/server.md +285 -0
  325. package/docs/statecharts.md +641 -0
  326. package/llms-full.txt +3506 -0
  327. package/llms.txt +47 -0
  328. package/package.json +33 -5
  329. package/packages/core/README.md +2 -1
  330. package/packages/reports/README.md +69 -2
  331. package/packages/reports/STORAGE.md +5 -1
  332. package/packages/schema/README.md +14 -0
  333. package/packages/schema/schemas/check-detail.schema.json +1032 -0
  334. package/packages/schema/schemas/checks-result.schema.json +26 -0
  335. package/packages/schema/schemas/command-error.schema.json +35 -0
  336. package/packages/schema/schemas/doctor-result.schema.json +60 -0
  337. package/packages/schema/schemas/effective-delta.schema.json +490 -0
  338. package/packages/schema/schemas/effective-evidence.schema.json +471 -0
  339. package/packages/schema/schemas/evidence-page.schema.json +271 -0
  340. package/packages/schema/schemas/hello-event.schema.json +22 -0
  341. package/packages/schema/schemas/init-plan.schema.json +347 -0
  342. package/packages/schema/schemas/layer-rows.schema.json +199 -0
  343. package/packages/schema/schemas/layer.schema.json +2888 -0
  344. package/packages/schema/schemas/merge-result.schema.json +40 -0
  345. package/packages/schema/schemas/outline.schema.json +62 -0
  346. package/packages/schema/schemas/problem.schema.json +37 -0
  347. package/packages/schema/schemas/publish-checks.schema.json +1413 -0
  348. package/packages/schema/schemas/publish-result.schema.json +168 -0
  349. package/packages/schema/schemas/ready.schema.json +68 -0
  350. package/packages/schema/schemas/report-event.schema.json +100 -0
  351. package/packages/schema/schemas/report-status.schema.json +370 -0
  352. package/packages/schema/schemas/server-config.schema.json +194 -0
  353. package/packages/schema/schemas/server-info.schema.json +114 -0
  354. package/packages/schema/schemas/spec-set.schema.json +103 -0
  355. package/packages/schema/schemas/store-listing.schema.json +29 -0
  356. package/packages/schema/schemas/store-write-result.schema.json +28 -0
  357. package/packages/schema/schemas/stream-result.schema.json +59 -0
  358. package/packages/server/README.md +43 -0
  359. package/packages/server/openapi.json +6699 -0
  360. package/packages/visualizer/README.md +27 -5
  361. package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
  362. package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  363. package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
  364. package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
  365. package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
  366. package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
  367. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
  368. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  369. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
  370. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
  371. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
  372. package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
  373. package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
  374. package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  375. package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
  376. package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
  377. package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
  378. package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
  379. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
  380. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  381. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
  382. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
  383. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
  384. package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
  385. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
  386. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  387. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
  388. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
  389. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
  390. package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
  391. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
  392. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  393. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
  394. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
  395. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
  396. package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
  397. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
  398. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
  399. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
  400. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
  401. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
  402. package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
  403. package/packages/visualizer/scripts/make-fixtures.ts +2 -0
  404. package/packages/visualizer/scripts/publish-data.ts +1 -2
  405. package/packages/visualizer/src/app/AppShell.tsx +9 -2
  406. package/packages/visualizer/src/app/report-sources.tsx +6 -4
  407. package/packages/visualizer/src/app/router.tsx +3 -3
  408. package/packages/visualizer/src/components/Header.module.css +35 -0
  409. package/packages/visualizer/src/components/Header.tsx +41 -4
  410. package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
  411. package/packages/visualizer/src/data/adapters/index.ts +10 -1
  412. package/packages/visualizer/src/evidence/api.ts +14 -2
  413. package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
  414. package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
  415. package/packages/visualizer/src/evidence/check-detail.tsx +38 -4
  416. package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
  417. package/packages/visualizer/src/evidence/evidence.module.css +20 -0
  418. package/packages/visualizer/src/evidence/marks.tsx +2 -0
  419. package/packages/visualizer/src/evidence/overview.tsx +3 -3
  420. package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
  421. package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
  422. package/packages/visualizer/src/find/Finder.tsx +1 -7
  423. package/packages/visualizer/src/find/open.ts +4 -0
  424. package/packages/visualizer/src/live/live-stream.ts +204 -0
  425. package/packages/visualizer/src/runtime.ts +19 -0
  426. package/packages/visualizer/src/state/ui-store.ts +6 -2
  427. package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
  428. package/packages/visualizer/vite/example-reports.ts +475 -0
  429. package/packages/visualizer/vite.config.ts +18 -2
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @module Server configuration
3
+ *
4
+ * Loads how the server runs: `atlas.server.json`, then environment variables, then flags, each overriding the last, checked
5
+ * against `ServerConfigSchema` and resolved (paths made absolute, backends named, tokens read). Every mistake is a
6
+ * `ConfigError` that says what to change, and a server that would listen beyond this machine with no tokens refuses to
7
+ * start. `SERVER_SETTINGS` documents every setting once; the table in docs/server.md is generated from it and a test proves
8
+ * each row.
9
+ *
10
+ * @see docs/server.md#configuration
11
+ */
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { parseArgs } from 'node:util';
15
+ import { ServerConfigSchema } from '../../schema/src/server-config.js';
16
+ /** A configuration that cannot be used. The message says what to change; `atlas serve` prints it and exits 2. */
17
+ export class ConfigError extends Error {
18
+ }
19
+ const DEFAULT_FILE = 'atlas.server.json';
20
+ const MIN_TOKEN = 12;
21
+ const OPTIONS = {
22
+ config: { type: 'string' }, data: { type: 'string' }, store: { type: 'string', multiple: true }, write: { type: 'string' },
23
+ host: { type: 'string' }, port: { type: 'string' }, site: { type: 'string' }, 'outline-branch': { type: 'string' },
24
+ branch: { type: 'string', multiple: true }, 'ignore-modified': { type: 'boolean' }, refresh: { type: 'string' },
25
+ insecure: { type: 'boolean' }, 'no-log': { type: 'boolean' }, 'anonymous-read': { type: 'boolean' }
26
+ };
27
+ /** Every flag `atlas serve` accepts, with its `--`. */
28
+ export const SERVER_FLAGS = Object.keys(OPTIONS).map(name => `--${name}`);
29
+ /**
30
+ * Every setting that can be given by flag or environment, documented once. `docs/server.md` renders its table from
31
+ * this list, and a test proves each row: its example, through the flag and through the environment, changes the
32
+ * configuration as the row says. A setting added to the loader without a row here is therefore not documented in the
33
+ * table; one documented here that the loader ignores fails the test.
34
+ */
35
+ export const SERVER_SETTINGS = [
36
+ { summary: 'Address to listen on', key: 'host', flag: '--host <address>', env: ['ATLAS_HOST', 'HOST'], default: '`127.0.0.1`', example: { value: '0.0.0.0', read: c => c.host, expected: '0.0.0.0' } },
37
+ { summary: 'Port (`0` picks a free one)', key: 'port', flag: '--port <n>', env: ['ATLAS_PORT', 'PORT'], default: '`8080`', example: { value: '9191', read: c => c.port, expected: 9191 } },
38
+ { summary: 'The server\'s own folder: the default backend and write target', key: 'data', flag: '--data <dir>', env: ['ATLAS_DATA'], default: '`./atlas-data`', example: { value: 'results', read: c => path.basename(c.data), expected: 'results' } },
39
+ { summary: 'More backends, read together with the data folder', key: 'backends', flag: '--store <[id=]url>` (repeatable)', env: ['ATLAS_STORE'], default: 'none', example: { value: 'lab=https://lab.example.com', read: c => c.backends.map(backend => backend.id), expected: ['data', 'lab'] } },
40
+ { summary: 'The backend uploads go to', key: 'write', flag: '--write <id>', env: ['ATLAS_WRITE'], default: '`data`', example: { value: 'data', read: c => c.writeId, expected: 'data' } },
41
+ { summary: 'The built viewer', key: 'site', flag: '--site <dir>', env: ['ATLAS_SITE'], default: '`./atlas-site`, else built once', example: { value: 'viewer', read: c => path.basename(c.site ?? ''), expected: 'viewer' } },
42
+ { summary: 'Whose specs the report is laid over', key: 'outlineBranch', flag: '--outline-branch <branch>', env: ['ATLAS_OUTLINE_BRANCH'], default: '`main`, then `master`, then the newest', example: { value: 'trunk', read: c => c.outlineBranch, expected: 'trunk' } },
43
+ { summary: 'Only layers recorded on these branches take part', key: 'include.branches', flag: '--branch <name>` (repeatable)', env: ['ATLAS_ONLY_BRANCH'], default: 'all', example: { value: 'main', read: c => c.include.branches, expected: ['main'] } },
44
+ { summary: 'Leave out layers from modified checkouts and unknown origins', key: 'include.ignoreModified', flag: '--ignore-modified', env: ['ATLAS_IGNORE_MODIFIED'], default: 'no', example: { value: '1', read: c => c.include.ignoreModified, expected: true } },
45
+ { summary: 'How often (seconds) the backends are looked at for layers written by something else', key: 'refreshSeconds', flag: '--refresh <seconds>', env: ['ATLAS_REFRESH_SECONDS'], default: '`10`', example: { value: '30', read: c => c.refreshSeconds, expected: 30 } },
46
+ { summary: 'Let anyone look, not upload', key: 'auth.anonymousRead', flag: '--anonymous-read', env: ['ATLAS_ANONYMOUS_READ'], default: 'no', example: { value: '1', read: c => c.anonymousRead, expected: true } },
47
+ { summary: 'Listen publicly with no tokens (something in front signs people in)', key: 'auth.insecure', flag: '--insecure', env: ['ATLAS_INSECURE'], default: 'no', example: { value: '1', read: c => c.insecure, expected: true } },
48
+ { summary: 'The admin token (also `ATLAS_PUBLISH_TOKEN` and `ATLAS_READ_TOKEN` for the other roles, or `auth.tokens` in the file)', key: 'auth.tokens', flag: null, env: ['ATLAS_TOKEN'], default: 'none: the server is open and listens on this machine only', example: { value: 'admin-token-123456', read: c => c.tokens.map(token => token.role), expected: ['admin'] } },
49
+ { summary: 'The configuration file', key: '(the file itself)', flag: '--config <file>', env: ['ATLAS_CONFIG'], default: '`./atlas.server.json` when it exists', example: { value: 'custom.json', read: c => path.basename(c.configFile ?? ''), expected: 'custom.json' } },
50
+ { summary: 'No request log', key: 'log', flag: '--no-log', env: [], default: 'one JSON line per request', example: { value: '', read: c => c.log, expected: false } }
51
+ ];
52
+ /** The text `atlas serve --help` prints. Also the source of the `atlas serve` section of `docs/reference/cli.md`. */
53
+ export const SERVER_USAGE = `atlas serve: the Atlas server
54
+
55
+ atlas serve [--config atlas.server.json] [--data <dir>] [--port 8080] [--host 127.0.0.1]
56
+ [--store <url|id=url>]... [--write <id>] [--site <dir>]
57
+ [--outline-branch <b>] [--branch <name>]... [--ignore-modified] [--refresh <seconds>]
58
+ [--anonymous-read] [--insecure] [--no-log]
59
+
60
+ One process serves the viewer and the API. Test results are uploaded to it from anywhere (laptops, CI) and it
61
+ shows everyone the combined report, live. It keeps its data in --data (default ./atlas-data) and can read more
62
+ backends (--store): S3, other servers, folders. Uploads go to the data folder unless --write names another.
63
+
64
+ Tokens (ATLAS_TOKEN admin, ATLAS_PUBLISH_TOKEN, ATLAS_READ_TOKEN, or "auth.tokens" in the config) are required
65
+ to listen beyond this machine unless --insecure. Every option has an environment variable (docs/deploying.md).
66
+ `;
67
+ function parseFlags(argv) {
68
+ try {
69
+ return parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: false, strict: true }).values;
70
+ }
71
+ catch (error) {
72
+ throw new ConfigError(error instanceof Error ? error.message : String(error));
73
+ }
74
+ }
75
+ const list = (text) => (text ?? '').split(/[\n,]+/).map(item => item.trim()).filter(Boolean);
76
+ const truthy = (value) => value === '1' || value === 'true';
77
+ /** `id=url`, or a bare url with an id made from it. */
78
+ function backendOf(spec, taken) {
79
+ const named = /^([a-z0-9-]+)=(.+)$/.exec(spec);
80
+ if (named)
81
+ return { id: named[1], url: named[2] };
82
+ const base = spec.replace(/^[a-z0-9+.-]+:\/\//i, '').replace(/[?#].*$/, '').replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase().slice(0, 40) || 'store';
83
+ let id = base;
84
+ for (let n = 2; taken.has(id); n++)
85
+ id = `${base}-${n}`;
86
+ return { id, url: spec };
87
+ }
88
+ /**
89
+ * Read the file named by `--config`/`ATLAS_CONFIG`, or `atlas.server.json` in the folder if there is one, lay the
90
+ * environment and then the flags over it, and check the result. Every problem is a {@link ConfigError} that says
91
+ * what to change.
92
+ */
93
+ export function loadServerConfig(argv, env, cwd, read = file => readFileSync(file, 'utf8')) {
94
+ const flags = parseFlags(argv);
95
+ const named = flags.config ?? env.ATLAS_CONFIG;
96
+ const file = named ? path.resolve(cwd, named) : existsSync(path.join(cwd, DEFAULT_FILE)) ? path.join(cwd, DEFAULT_FILE) : null;
97
+ let raw = {};
98
+ if (file) {
99
+ try {
100
+ raw = JSON.parse(read(file));
101
+ }
102
+ catch (error) {
103
+ throw new ConfigError(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
104
+ }
105
+ }
106
+ const base = file ? path.dirname(file) : cwd;
107
+ const auth = { ...raw.auth };
108
+ const include = { ...raw.include };
109
+ const merged = { ...raw, auth, include };
110
+ const set = (key, value) => { if (value !== undefined)
111
+ merged[key] = value; };
112
+ // ATLAS_HOST and ATLAS_PORT first: HOST is often the machine's own name, and PORT belongs to whatever else is running here.
113
+ set('host', flags.host ?? env.ATLAS_HOST ?? env.HOST);
114
+ const port = env.ATLAS_PORT ?? env.PORT;
115
+ set('port', flags.port !== undefined ? Number(flags.port) : port !== undefined ? Number(port) : undefined);
116
+ set('data', flags.data ?? env.ATLAS_DATA);
117
+ set('write', flags.write ?? env.ATLAS_WRITE);
118
+ set('site', flags.site ?? env.ATLAS_SITE);
119
+ set('outlineBranch', flags['outline-branch'] ?? env.ATLAS_OUTLINE_BRANCH);
120
+ set('refreshSeconds', flags.refresh !== undefined ? Number(flags.refresh) : env.ATLAS_REFRESH_SECONDS !== undefined ? Number(env.ATLAS_REFRESH_SECONDS) : undefined);
121
+ if (flags['no-log'])
122
+ merged.log = false;
123
+ if (flags.insecure || truthy(env.ATLAS_INSECURE))
124
+ auth.insecure = true;
125
+ if (flags['anonymous-read'] || truthy(env.ATLAS_ANONYMOUS_READ))
126
+ auth.anonymousRead = true;
127
+ const branches = [...(flags.branch ?? []), ...list(env.ATLAS_ONLY_BRANCH)];
128
+ if (branches.length)
129
+ include.branches = branches;
130
+ if (flags['ignore-modified'] || truthy(env.ATLAS_IGNORE_MODIFIED))
131
+ include.ignoreModified = true;
132
+ const parsed = ServerConfigSchema.safeParse(merged);
133
+ if (!parsed.success)
134
+ throw new ConfigError(`${file ? `${file}: ` : 'Configuration: '}${parsed.error.issues.map(issue => `${issue.path.join('.') || '(config)'}: ${issue.message}`).join('; ')}`);
135
+ const config = parsed.data;
136
+ // Backends: the data folder, then those in the file, then --store and ATLAS_STORE.
137
+ const taken = new Set(['data']);
138
+ const backends = [{ id: 'data', label: 'Server data', url: path.resolve(base, config.data) }];
139
+ const extra = config.backends.map(item => ({ id: item.id, label: item.label ?? item.id, url: item.url, write: item.write === true }));
140
+ for (const item of extra) {
141
+ if (taken.has(item.id))
142
+ throw new ConfigError(`Two backends are called "${item.id}" ("data" is the data folder)`);
143
+ taken.add(item.id);
144
+ backends.push({ id: item.id, label: item.label, url: item.url });
145
+ }
146
+ for (const spec of [...(flags.store ?? []), ...(flags.store ? [] : list(env.ATLAS_STORE))]) {
147
+ const item = backendOf(spec, taken);
148
+ if (taken.has(item.id))
149
+ throw new ConfigError(`Two backends are called "${item.id}"`);
150
+ taken.add(item.id);
151
+ backends.push({ id: item.id, label: item.id, url: item.url });
152
+ }
153
+ const marked = extra.filter(item => item.write).map(item => item.id);
154
+ if (marked.length > 1)
155
+ throw new ConfigError(`Only one backend can take uploads; ${marked.join(', ')} are all marked write`);
156
+ const writeId = config.write ?? marked[0] ?? 'data';
157
+ if (!backends.some(item => item.id === writeId))
158
+ throw new ConfigError(`write names "${writeId}", which is not a backend (${backends.map(item => item.id).join(', ')})`);
159
+ // Tokens: the file's, then the environment's.
160
+ const tokens = [];
161
+ for (const item of config.auth.tokens) {
162
+ const token = item.token ?? env[item.tokenEnv];
163
+ if (!token)
164
+ throw new ConfigError(`Token "${item.name}" reads ${item.tokenEnv} and it is not set`);
165
+ tokens.push({ name: item.name, role: item.role, token });
166
+ }
167
+ for (const [variable, role, name] of [['ATLAS_TOKEN', 'admin', 'admin'], ['ATLAS_PUBLISH_TOKEN', 'publish', 'publish'], ['ATLAS_READ_TOKEN', 'read', 'read']]) {
168
+ if (env[variable])
169
+ tokens.push({ name, role, token: env[variable].trim() });
170
+ }
171
+ for (const item of tokens)
172
+ if (item.token.length < MIN_TOKEN)
173
+ throw new ConfigError(`Token "${item.name}" is shorter than ${MIN_TOKEN} characters: use a long random one (openssl rand -hex 24)`);
174
+ if (new Set(tokens.map(item => item.token)).size !== tokens.length)
175
+ throw new ConfigError('Two tokens are the same: each token must say who it is');
176
+ const { backends: _backends, auth: _auth, write: _write, ...rest } = config;
177
+ const resolved = { ...rest, data: backends[0].url, backends, writeId, tokens, anonymousRead: config.auth.anonymousRead, insecure: config.auth.insecure, configFile: file, ...(config.site ? { site: path.resolve(base, config.site) } : {}) };
178
+ assertSafeToListen(resolved);
179
+ return resolved;
180
+ }
181
+ const LOOPBACK = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
182
+ /** A server that anyone on the network can upload to must say who may. */
183
+ export function assertSafeToListen(config) {
184
+ if (config.tokens.length || config.insecure || LOOPBACK.has(config.host))
185
+ return;
186
+ throw new ConfigError(`Listening on ${config.host} with no tokens would let anyone on the network upload results. Set ATLAS_TOKEN (and ATLAS_PUBLISH_TOKEN / ATLAS_READ_TOKEN), or listen on 127.0.0.1, or pass --insecure if something else in front of it does the signing in.`);
187
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @module `atlas dev`
3
+ *
4
+ * Runs the same server as `atlas serve` on a port of its own, with the viewer's Vite dev server in front of it. Vite proxies
5
+ * `/api` (server-sent events included) to the server and writes the same address book into the page, so the browser, the
6
+ * uploaders and the viewer all use one host, exactly as in production, and a change to the viewer shows at once.
7
+ */
8
+ import { type ServeIo } from './main.js';
9
+ /** The text `atlas dev --help` prints. Also the source of the `atlas dev` section of `docs/reference/cli.md`. */
10
+ export declare const DEV_USAGE = "atlas dev: the Atlas server and the viewer, for working on either\n\n atlas dev [--port 5180] [--host 127.0.0.1] [--data <dir>] [--store <url|id=url>]... [--config atlas.server.json]\n\n Starts the server (API only, on a port of its own) and the viewer's dev server, which proxies /api to it, so the\n browser talks to one host and a change to the viewer shows at once. In production `atlas serve` does both from\n one process.\n";
11
+ /** `atlas dev`. */
12
+ export declare function devMain(argv: readonly string[], io: ServeIo): Promise<number>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @module `atlas dev`
3
+ *
4
+ * Runs the same server as `atlas serve` on a port of its own, with the viewer's Vite dev server in front of it. Vite proxies
5
+ * `/api` (server-sent events included) to the server and writes the same address book into the page, so the browser, the
6
+ * uploaders and the viewer all use one host, exactly as in production, and a change to the viewer shows at once.
7
+ */
8
+ import path from 'node:path';
9
+ import { openConfiguredBackends, serverOptionsOf } from './main.js';
10
+ import { createAtlasServer } from './app.js';
11
+ import { ConfigError, loadServerConfig, SERVER_USAGE } from './config.js';
12
+ import { listen } from './host.js';
13
+ import { visualizerRoot } from './site.js';
14
+ /** The text `atlas dev --help` prints. Also the source of the `atlas dev` section of `docs/reference/cli.md`. */
15
+ export const DEV_USAGE = `atlas dev: the Atlas server and the viewer, for working on either
16
+
17
+ atlas dev [--port 5180] [--host 127.0.0.1] [--data <dir>] [--store <url|id=url>]... [--config atlas.server.json]
18
+
19
+ Starts the server (API only, on a port of its own) and the viewer's dev server, which proxies /api to it, so the
20
+ browser talks to one host and a change to the viewer shows at once. In production \`atlas serve\` does both from
21
+ one process.
22
+ `;
23
+ /** `atlas dev`. */
24
+ export async function devMain(argv, io) {
25
+ const cwd = io.cwd ?? process.cwd();
26
+ const env = io.env ?? process.env;
27
+ if (argv.includes('--help') || argv.includes('-h')) {
28
+ io.out(DEV_USAGE);
29
+ return 0;
30
+ }
31
+ const root = visualizerRoot();
32
+ if (!root) {
33
+ io.out('atlas dev: the viewer is not installed alongside the server.\n');
34
+ return 2;
35
+ }
36
+ // --port is the one the browser uses (the viewer's); the API takes another that nobody needs to know.
37
+ const flagged = argv.findIndex(item => item === '--port');
38
+ const publicPort = flagged >= 0 ? Number(argv[flagged + 1]) : Number(env.ATLAS_PORT ?? env.PORT ?? 5180);
39
+ const forServer = argv.filter((_, i) => i !== flagged && i !== flagged + 1 || flagged < 0);
40
+ let config;
41
+ try {
42
+ // The API itself listens on a port of its own on this machine; what the user said about --host is for the viewer, the one door.
43
+ config = loadServerConfig([...forServer, '--port', '0'], { ...env, PORT: undefined, ATLAS_PORT: undefined }, cwd);
44
+ }
45
+ catch (error) {
46
+ if (error instanceof ConfigError) {
47
+ io.out(`atlas dev: ${error.message}\n${SERVER_USAGE.split('\n')[0]}\n`);
48
+ return 2;
49
+ }
50
+ throw error;
51
+ }
52
+ const backends = openConfiguredBackends(config, env, cwd);
53
+ const api = createAtlasServer(serverOptionsOf(config, backends, line => io.out(`${JSON.stringify(line)}\n`)));
54
+ const running = await listen(api.fetch, { port: 0, host: '127.0.0.1', maxBodyBytes: config.limits.maxObjectMiB * 1024 * 1024 });
55
+ await api.ready;
56
+ // The viewer's config reads this to proxy /api, and to write the same page config a server would.
57
+ process.env.ATLAS_API_URL = running.url.replace('localhost', '127.0.0.1');
58
+ process.env.INIT_CWD = cwd;
59
+ const { createServer } = await import('vite');
60
+ const vite = await createServer({
61
+ configFile: path.join(root, 'vite.config.ts'),
62
+ configLoader: 'runner',
63
+ root,
64
+ server: { port: publicPort, host: config.host, strictPort: false }
65
+ });
66
+ await vite.listen();
67
+ vite.printUrls();
68
+ io.out(`Data in ${path.relative(cwd, config.data) || config.data}. Publish with: ATLAS_STORE=${vite.resolvedUrls?.local[0] ?? `http://localhost:${publicPort}`} atlas report publish …\n`);
69
+ await new Promise(resolve => {
70
+ io.signal?.addEventListener('abort', () => resolve(), { once: true });
71
+ if (!io.signal) {
72
+ process.once('SIGTERM', () => resolve());
73
+ process.once('SIGINT', () => resolve());
74
+ }
75
+ });
76
+ api.close();
77
+ await vite.close();
78
+ await running.close(2_000);
79
+ return 0;
80
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @module Server-sent events
3
+ *
4
+ * What the server tells every connected viewer the moment it happens. A viewer that misses some (a dropped connection, a
5
+ * laptop that slept) loses nothing: events say "something changed", the state is always fetched, and a viewer reads again
6
+ * when it reconnects. `EventHub` encodes each event once for all viewers, sends a named `ping` every heartbeat so proxies
7
+ * keep the connection and a quiet server is told apart from a dead one, refuses past a maximum number of viewers (they poll
8
+ * instead), and lets go of a viewer that has stopped reading rather than hold its backlog.
9
+ *
10
+ * @see docs/server.md#streaming
11
+ */
12
+ /** The events the server publishes: `hello` (sent first to a new viewer), `report` (layers arrived) and `atlas:change` (specs or runs changed). `ping` is the heartbeat, sent by the hub itself. */
13
+ export type EventName = 'hello' | 'report' | 'atlas:change';
14
+ /** The viewers connected to `/api/events` and how to reach them all. One hub per server. */
15
+ export declare class EventHub {
16
+ #private;
17
+ constructor(options?: {
18
+ readonly maxSubscribers?: number;
19
+ readonly heartbeatMs?: number;
20
+ });
21
+ /** How many viewers are connected right now. */
22
+ get count(): number;
23
+ /** Tell every viewer. `data` is JSON. */
24
+ publish(name: EventName, data: unknown): void;
25
+ /** A stream for one viewer, starting with `hello` (what `greeting` returns). It ends when the viewer goes. */
26
+ stream(request: Request, greeting: () => unknown): Response;
27
+ /** End every stream, for shutting down. */
28
+ close(): void;
29
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @module Server-sent events
3
+ *
4
+ * What the server tells every connected viewer the moment it happens. A viewer that misses some (a dropped connection, a
5
+ * laptop that slept) loses nothing: events say "something changed", the state is always fetched, and a viewer reads again
6
+ * when it reconnects. `EventHub` encodes each event once for all viewers, sends a named `ping` every heartbeat so proxies
7
+ * keep the connection and a quiet server is told apart from a dead one, refuses past a maximum number of viewers (they poll
8
+ * instead), and lets go of a viewer that has stopped reading rather than hold its backlog.
9
+ *
10
+ * @see docs/server.md#streaming
11
+ */
12
+ /** Events a viewer may be behind by before it is let go. */
13
+ const MAX_BACKLOG = 256;
14
+ /** The viewers connected to `/api/events` and how to reach them all. One hub per server. */
15
+ export class EventHub {
16
+ #subscribers = new Set();
17
+ #max;
18
+ #heartbeatMs;
19
+ #encoder = new TextEncoder();
20
+ #id = 0;
21
+ #timer = null;
22
+ constructor(options = {}) {
23
+ this.#max = options.maxSubscribers ?? 500;
24
+ this.#heartbeatMs = options.heartbeatMs ?? 15_000;
25
+ }
26
+ /** How many viewers are connected right now. */
27
+ get count() {
28
+ return this.#subscribers.size;
29
+ }
30
+ /** Tell every viewer. `data` is JSON. */
31
+ publish(name, data) {
32
+ this.#id += 1;
33
+ // Written once, however many are listening.
34
+ const frame = this.#encoder.encode(`id: ${this.#id}\nevent: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
35
+ for (const subscriber of [...this.#subscribers])
36
+ subscriber.send(frame);
37
+ }
38
+ /** A stream for one viewer, starting with `hello` (what `greeting` returns). It ends when the viewer goes. */
39
+ stream(request, greeting) {
40
+ if (this.#subscribers.size >= this.#max)
41
+ return Response.json({ error: 'Too many viewers are connected for live updates; they will poll instead.' }, { status: 503, headers: { 'retry-after': '30' } });
42
+ let subscriber = null;
43
+ const body = new ReadableStream({
44
+ start: (controller) => {
45
+ const send = (chunk) => {
46
+ try {
47
+ // A viewer that has stopped reading (a laptop asleep, a dead connection) must not make the server hold its backlog: it reconnects and reads everything again.
48
+ if ((controller.desiredSize ?? 1) < -MAX_BACKLOG) {
49
+ drop();
50
+ controller.close();
51
+ return;
52
+ }
53
+ controller.enqueue(chunk);
54
+ }
55
+ catch {
56
+ drop();
57
+ }
58
+ };
59
+ const drop = () => { if (subscriber)
60
+ this.#subscribers.delete(subscriber); subscriber = null; if (!this.#subscribers.size)
61
+ this.#stopTimer(); };
62
+ subscriber = { send, close: () => { drop(); try {
63
+ controller.close();
64
+ }
65
+ catch { /* already closed */ } } };
66
+ this.#subscribers.add(subscriber);
67
+ this.#startTimer();
68
+ // `retry` is how long the browser waits before reconnecting by itself.
69
+ send(this.#encoder.encode(`retry: 3000\n\n`));
70
+ this.#id += 1;
71
+ send(this.#encoder.encode(`id: ${this.#id}\nevent: hello\ndata: ${JSON.stringify(greeting())}\n\n`));
72
+ request.signal.addEventListener('abort', drop, { once: true });
73
+ },
74
+ cancel: () => { if (subscriber)
75
+ this.#subscribers.delete(subscriber); subscriber = null; if (!this.#subscribers.size)
76
+ this.#stopTimer(); }
77
+ });
78
+ return new Response(body, { headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive', 'x-accel-buffering': 'no' } });
79
+ }
80
+ #startTimer() {
81
+ if (this.#timer)
82
+ return;
83
+ // A named event, not a comment: proxies see traffic, and the viewer (which a browser does not tell about comments) can tell a quiet
84
+ // server from a dead connection.
85
+ const ping = this.#encoder.encode('event: ping\ndata: {}\n\n');
86
+ this.#timer = setInterval(() => { for (const subscriber of [...this.#subscribers])
87
+ subscriber.send(ping); }, this.#heartbeatMs);
88
+ this.#timer.unref?.();
89
+ }
90
+ #stopTimer() {
91
+ if (this.#timer)
92
+ clearInterval(this.#timer);
93
+ this.#timer = null;
94
+ }
95
+ /** End every stream, for shutting down. */
96
+ close() {
97
+ for (const subscriber of [...this.#subscribers])
98
+ subscriber.close();
99
+ this.#stopTimer();
100
+ }
101
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @module HTTP host for a fetch handler
3
+ *
4
+ * `listen` serves any `(Request) => Promise<Response>` over HTTP: with `Bun.serve` when running on Bun, with a `node:http`
5
+ * bridge (streaming bodies in and out, so server-sent events work) everywhere else. The server's behaviour is the same on
6
+ * both; `bun run release:smoke` proves it under each. Bodies over `maxBodyBytes` are refused by the runtime before the
7
+ * handler sees them.
8
+ */
9
+ /** A server that is listening: the `port` it got (useful when asked for `0`), its `url`, and `close`. */
10
+ export type RunningServer = {
11
+ readonly port: number;
12
+ readonly url: string;
13
+ /** Stop accepting, let requests in flight finish for up to `graceMs`, then end what is left. */
14
+ close(graceMs?: number): Promise<void>;
15
+ };
16
+ /** Where to listen (`port`, `host`) and the largest request body the runtime will accept. */
17
+ export type ListenOptions = {
18
+ readonly port: number;
19
+ readonly host: string;
20
+ /** The largest request body to read, in bytes. */
21
+ readonly maxBodyBytes: number;
22
+ };
23
+ type Handler = (request: Request) => Promise<Response> | Response;
24
+ /**
25
+ * Serve a fetch handler. On Bun, which is what Atlas is made for, that is `Bun.serve`: one native server for the
26
+ * API, the live streams and the page. On Node it is the same handler behind `node:http`, so the package still runs
27
+ * where Bun is not installed.
28
+ */
29
+ export declare function listen(handler: Handler, options: ListenOptions): Promise<RunningServer>;
30
+ export {};
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @module HTTP host for a fetch handler
3
+ *
4
+ * `listen` serves any `(Request) => Promise<Response>` over HTTP: with `Bun.serve` when running on Bun, with a `node:http`
5
+ * bridge (streaming bodies in and out, so server-sent events work) everywhere else. The server's behaviour is the same on
6
+ * both; `bun run release:smoke` proves it under each. Bodies over `maxBodyBytes` are refused by the runtime before the
7
+ * handler sees them.
8
+ */
9
+ import { createServer } from 'node:http';
10
+ import { Readable } from 'node:stream';
11
+ const display = (host, port) => `http://${host === '0.0.0.0' || host === '::' ? 'localhost' : host}:${port}`;
12
+ /**
13
+ * Serve a fetch handler. On Bun, which is what Atlas is made for, that is `Bun.serve`: one native server for the
14
+ * API, the live streams and the page. On Node it is the same handler behind `node:http`, so the package still runs
15
+ * where Bun is not installed.
16
+ */
17
+ export async function listen(handler, options) {
18
+ const bun = getBun();
19
+ if (bun)
20
+ return listenBun(bun, handler, options);
21
+ return listenNode(handler, options);
22
+ }
23
+ const getBun = () => globalThis.Bun;
24
+ async function listenBun(bun, handler, options) {
25
+ const server = bun.serve({
26
+ port: options.port,
27
+ hostname: options.host,
28
+ // Live streams are quiet between heartbeats; the longest idle time Bun allows keeps them open.
29
+ idleTimeout: 120,
30
+ maxRequestBodySize: options.maxBodyBytes + 1024 * 1024,
31
+ fetch: handler,
32
+ error: (error) => Response.json({ error: error.message }, { status: 500 })
33
+ });
34
+ return {
35
+ port: server.port,
36
+ url: display(options.host, server.port),
37
+ close: async (graceMs = 10_000) => {
38
+ // Stop taking new connections and let requests in flight finish; end the rest after the grace period.
39
+ const closing = Promise.resolve(server.stop(false));
40
+ const timer = setTimeout(() => { void server.stop(true); }, graceMs);
41
+ timer.unref?.();
42
+ await closing;
43
+ clearTimeout(timer);
44
+ }
45
+ };
46
+ }
47
+ function toRequest(req, host, maxBodyBytes) {
48
+ const headers = new Headers();
49
+ for (const [name, value] of Object.entries(req.headers))
50
+ if (value !== undefined)
51
+ headers.set(name, Array.isArray(value) ? value.join(', ') : value);
52
+ const method = req.method ?? 'GET';
53
+ const hasBody = method !== 'GET' && method !== 'HEAD';
54
+ let size = 0;
55
+ const body = hasBody ? Readable.toWeb(req).pipeThrough(new TransformStream({ transform(chunk, controller) { size += chunk.byteLength; if (size > maxBodyBytes)
56
+ controller.error(new Error('The request body is too large'));
57
+ else
58
+ controller.enqueue(chunk); } })) : null;
59
+ return new Request(`http://${req.headers.host ?? host}${req.url ?? '/'}`, { method, headers, ...(body ? { body, duplex: 'half' } : {}) });
60
+ }
61
+ async function send(response, res, head) {
62
+ const headers = {};
63
+ response.headers.forEach((value, name) => { headers[name] = value; });
64
+ const cookies = response.headers.getSetCookie?.();
65
+ if (cookies?.length)
66
+ headers['set-cookie'] = cookies;
67
+ res.writeHead(response.status, headers);
68
+ if (head || !response.body) {
69
+ res.end();
70
+ return;
71
+ }
72
+ const reader = response.body.getReader();
73
+ res.on('close', () => { void reader.cancel().catch(() => { }); });
74
+ for (;;) {
75
+ const { done, value } = await reader.read();
76
+ if (done)
77
+ break;
78
+ if (!res.write(value))
79
+ await new Promise(resolve => res.once('drain', resolve));
80
+ }
81
+ res.end();
82
+ }
83
+ async function listenNode(handler, options) {
84
+ const server = createServer((req, res) => {
85
+ void (async () => {
86
+ const controller = new AbortController();
87
+ res.on('close', () => controller.abort());
88
+ try {
89
+ const request = toRequest(req, options.host, options.maxBodyBytes);
90
+ const abortable = new Request(request, { signal: controller.signal });
91
+ await send(await handler(abortable), res, req.method === 'HEAD');
92
+ }
93
+ catch (error) {
94
+ if (!res.headersSent)
95
+ await send(Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 }), res, false).catch(() => { });
96
+ else
97
+ res.destroy();
98
+ }
99
+ })();
100
+ });
101
+ server.keepAliveTimeout = 65_000;
102
+ server.requestTimeout = 0;
103
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(options.port, options.host, resolve); });
104
+ const port = server.address().port;
105
+ return {
106
+ port,
107
+ url: display(options.host, port),
108
+ close: (graceMs = 10_000) => new Promise(resolve => {
109
+ server.close(() => resolve());
110
+ server.closeIdleConnections?.();
111
+ const timer = setTimeout(() => { server.closeAllConnections?.(); }, graceMs);
112
+ timer.unref?.();
113
+ })
114
+ };
115
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @module Server package entry point (`@crvouga/atlas/server`)
3
+ *
4
+ * Everything needed to run the Atlas server from code: `createAtlasServer` (a `fetch` handler with the live report, uploads
5
+ * and the viewer), `listen` (serve it over HTTP on Bun or Node), `loadServerConfig` (the file, the environment and the flags
6
+ * as one checked configuration), `serveMain` (what `atlas serve` runs), `EventHub`, `createAuth`, and the upload checks.
7
+ *
8
+ * @see docs/server.md
9
+ */
10
+ export { createAtlasServer, type AtlasServer, type AtlasServerOptions } from './app.js';
11
+ export { createAuth, type Auth, type Identity } from './auth.js';
12
+ export { assertSafeToListen, ConfigError, loadServerConfig, SERVER_USAGE, type ResolvedServerConfig, type ResolvedToken } from './config.js';
13
+ export { EventHub, type EventName } from './events.js';
14
+ export { listen, type RunningServer } from './host.js';
15
+ export { serveMain } from './main.js';
16
+ export { classify, validateUpload, validKey, Refusal } from './validate.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @module Server package entry point (`@crvouga/atlas/server`)
3
+ *
4
+ * Everything needed to run the Atlas server from code: `createAtlasServer` (a `fetch` handler with the live report, uploads
5
+ * and the viewer), `listen` (serve it over HTTP on Bun or Node), `loadServerConfig` (the file, the environment and the flags
6
+ * as one checked configuration), `serveMain` (what `atlas serve` runs), `EventHub`, `createAuth`, and the upload checks.
7
+ *
8
+ * @see docs/server.md
9
+ */
10
+ export { createAtlasServer } from './app.js';
11
+ export { createAuth } from './auth.js';
12
+ export { assertSafeToListen, ConfigError, loadServerConfig, SERVER_USAGE } from './config.js';
13
+ export { EventHub } from './events.js';
14
+ export { listen } from './host.js';
15
+ export { serveMain } from './main.js';
16
+ export { classify, validateUpload, validKey, Refusal } from './validate.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module llms.txt of a running server
3
+ *
4
+ * The plain-text guide ([llms.txt](https://llmstxt.org)) a server serves about itself at `GET /llms.txt`, for an AI
5
+ * agent that was handed nothing but the server's address. It is written from this server's own settings (its
6
+ * address, whether it needs a token), so what it says is true of the server that says it, and it points at the
7
+ * machine-readable description (`/api/openapi.json`) rather than repeating it.
8
+ */
9
+ /** What the guide says about the server that serves it. */
10
+ export type LlmsContext = {
11
+ /** The address clients use, such as `https://atlas.example.com`, no trailing slash. */
12
+ readonly url: string;
13
+ /** True when the server has no tokens: anyone who can reach it may upload. */
14
+ readonly open: boolean;
15
+ /** The id of the backend uploads go to. */
16
+ readonly write: string;
17
+ };
18
+ /** The guide for one server, Markdown. Stable in structure: an agent may read it by its headings. */
19
+ export declare function llmsTxt(context: LlmsContext): string;