@crvouga/atlas 0.4.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 (379) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +178 -617
  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 +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 +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 +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/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 +26 -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 +42 -9
  175. package/dist/reports/src/publish.js +36 -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 +37 -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 +32 -4
  329. package/packages/core/README.md +2 -1
  330. package/packages/reports/README.md +65 -0
  331. package/packages/reports/STORAGE.md +4 -0
  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 +21 -0
  361. package/packages/visualizer/src/app/AppShell.tsx +9 -2
  362. package/packages/visualizer/src/app/report-sources.tsx +6 -4
  363. package/packages/visualizer/src/app/router.tsx +3 -3
  364. package/packages/visualizer/src/components/Header.module.css +35 -0
  365. package/packages/visualizer/src/components/Header.tsx +40 -3
  366. package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
  367. package/packages/visualizer/src/data/adapters/index.ts +10 -1
  368. package/packages/visualizer/src/evidence/api.ts +14 -2
  369. package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
  370. package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
  371. package/packages/visualizer/src/evidence/check-detail.tsx +3 -1
  372. package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
  373. package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
  374. package/packages/visualizer/src/find/Finder.tsx +1 -7
  375. package/packages/visualizer/src/find/open.ts +4 -0
  376. package/packages/visualizer/src/live/live-stream.ts +204 -0
  377. package/packages/visualizer/src/runtime.ts +19 -0
  378. package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
  379. package/packages/visualizer/vite.config.ts +11 -3
@@ -0,0 +1,485 @@
1
+ /**
2
+ * @module The `atlas report` command
3
+ *
4
+ * `reportMain` is `atlas report publish | stream | status | merge | doctor | policy`, written so it can be called from code and
5
+ * tests as well as a terminal: it takes its arguments, environment, working directory, clock, git and standard input as
6
+ * parameters and returns the exit code. Every command can print a JSON document (`--format json`) instead of prose, and a
7
+ * command that is wrong throws, which `runReportCommand` turns into a message (or a JSON error) and exit code 2.
8
+ *
9
+ * Exit codes: 0 it worked, 1 it ran and the answer is no (a report that does not pass, a store that needs attention, checks
10
+ * that could not be sent), 2 the command itself was wrong.
11
+ *
12
+ * @see docs/integrating.md
13
+ * @see docs/agents.md
14
+ */
15
+ import { spawnSync } from 'node:child_process';
16
+ import { readFileSync, statSync } from 'node:fs';
17
+ import { readFile } from 'node:fs/promises';
18
+ import path from 'node:path';
19
+ import { PublishChecksSchema, ReportStatusSchema } from '../../schema/src/layer.js';
20
+ import { readAttachmentFiles } from './attachment-files.js';
21
+ import { backendsOf, layerFilter, value, values, writeStoreOf } from './cli-stores.js';
22
+ import { combineBackends } from './compose.js';
23
+ import { copyStore } from './copy-store.js';
24
+ import { loadDirectoryOutline } from './directory-outline.js';
25
+ import { iamPolicy, inspectStore } from './doctor.js';
26
+ import { checksFromCtrf, checksFromJunit, checksFromManifest } from './ingest.js';
27
+ import { parseWith } from './parse-with.js';
28
+ import { publishChecks } from './publish-checks.js';
29
+ import { createReporter } from './reporter.js';
30
+ import { formatStatusMarkdown, formatStatusText, reportStatus } from './report-status.js';
31
+ import { publishRunBundle } from './run-bundle.js';
32
+ import { collectSpecFiles, latestSpecSet, publishSpecSet } from './spec-set.js';
33
+ import { linkChecksToSpecs } from './spec-link.js';
34
+ import { openStore, storeSpecs } from './store-url.js';
35
+ /** The text `atlas report --help` prints: every subcommand, its flags, what each does and the exit codes. Also the source of the command reference in `docs/reference/cli.md`. */
36
+ export const REPORT_USAGE = `atlas report — layered evidence for any kind of spec
37
+
38
+ atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]
39
+ ( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )
40
+ [--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]
41
+ [--spec <path> | --specs <dir>] [--upload-run]
42
+ [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]
43
+ [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...
44
+ [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]
45
+ atlas report stream [--store <dir|s3://…|https://atlas.example.com>] --producer <name> [--environment <name>]
46
+ [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...
47
+ [--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)
48
+ atlas report status [--store <dir|s3://…|id=url>]... [--backends <module>]
49
+ [--specs <dir>] [--format text|json|markdown]
50
+ [--require-environments a,b] [--require-all-specs]
51
+ [--branch <name>]... [--ignore-modified]
52
+ atlas report merge [--store …]... [--backends <module>] --into <dir|s3://…>
53
+ atlas report doctor [--store …]... [--role publisher|reader|both]
54
+ atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]
55
+
56
+ A store is a directory or s3://bucket/prefix (add ?region=…&endpoint=… for R2, MinIO and the
57
+ rest). When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set
58
+ AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with \`aws sso login\`, or use a role.
59
+
60
+ publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.
61
+ --producer is required, unless the --checks file names one
62
+ --spec <path> every check in the report belongs to this spec (a report per feature file)
63
+ --specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or
64
+ file name the report gives; also publishes the specs themselves, so the
65
+ node can show them without a checkout
66
+ --upload-run with --manifest: also upload the run's screenshots and clips for the viewer
67
+ --attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive
68
+ (screenshots, videos and traces of a single test go in its CTRF or checks file)
69
+ --label k=v a fact about the run, not part of what it checks: build, os, browser
70
+ --link [n=]<url> a link to the run, a dashboard, an issue
71
+ Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own
72
+ folder and each --attach-root, and never from a hidden file or a link out: a report is data from a
73
+ test run, not instructions. One that is missing or not allowed is skipped with a warning.
74
+ From a checkout with uncommitted changes the layer is marked "modified".
75
+ stream Send checks as they finish: one JSON object per line on standard input (the fields of one
76
+ check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server
77
+ shows the run while it is still going. A check sent again with the same id (running, then
78
+ passed) is replaced by the later. Ends when the input does; exit 1 if anything could not be sent.
79
+ Point it at an Atlas server with --store https://… and a token in ATLAS_TOKEN.
80
+ status Combine every layer of every store, and exit 1 unless the report passes.
81
+ --specs <dir> lay the results over these spec files (default: the specs in the store)
82
+ --require-all-specs fail while any spec has no results
83
+ --branch <name> only layers recorded on this branch (repeatable)
84
+ --ignore-modified leave out layers from modified checkouts and unknown origins
85
+ merge Copy the layers of several stores into one.
86
+ doctor Check that a store works: credentials, listing, writing, and what it holds.
87
+ policy Print the least-privilege IAM policy for publishers, readers, or both.
88
+
89
+ --backends <module> A TypeScript or JavaScript module whose default export is a list of
90
+ { id, label?, store } (or a function returning it): a device lab, anything
91
+ that implements ObjectStore.
92
+
93
+ --format json (alias --json) publish, stream, merge, doctor and status print one JSON document instead of prose
94
+ (schemas: publish-result, stream-result, merge-result, doctor-result, report-status). A command
95
+ that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
96
+ A store may be an Atlas server (https://…): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.
97
+
98
+ Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.
99
+ `;
100
+ function gitOutput(args) {
101
+ const result = spawnSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
102
+ return result.status === 0 ? result.stdout.trim() || null : null;
103
+ }
104
+ /** Where the checks ran, from the CI's own variables and then git. Explicit flags win over both. */
105
+ export function detectOrigin(env, git = gitOutput) {
106
+ const first = (...names) => names.map(name => env[name]).find(value => value) ?? null;
107
+ const runUrl = env.GITHUB_RUN_ID && env.GITHUB_REPOSITORY ? `${env.GITHUB_SERVER_URL ?? 'https://github.com'}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : first('CI_PIPELINE_URL', 'BUILDKITE_BUILD_URL', 'CIRCLE_BUILD_URL');
108
+ const fromCi = first('GITHUB_SHA', 'CI_COMMIT_SHA', 'BUILDKITE_COMMIT', 'CIRCLE_SHA1');
109
+ return {
110
+ sha: fromCi ?? git(['rev-parse', 'HEAD']),
111
+ branch: first('GITHUB_HEAD_REF', 'GITHUB_REF_NAME', 'CI_COMMIT_REF_NAME', 'BUILDKITE_BRANCH', 'CIRCLE_BRANCH') ?? git(['branch', '--show-current']),
112
+ runUrl,
113
+ ci: fromCi !== null
114
+ };
115
+ }
116
+ /**
117
+ * Whether the checks ran on exactly the commit they name. A CI checkout does. A developer's worktree
118
+ * with edits to tracked files does not: its layer is `modified`, shown with a warning and never mistaken
119
+ * for the commit's own result. Outside git the origin is `unknown`.
120
+ */
121
+ export function detectFreshness(origin, git = gitOutput) {
122
+ if (origin.ci)
123
+ return undefined;
124
+ if (!origin.sha)
125
+ return 'unknown';
126
+ // `git diff` is empty (so null) when clean; untracked files (test output, build products) do not count.
127
+ return git(['status', '--porcelain', '--untracked-files=no']) ? 'modified' : undefined;
128
+ }
129
+ function keyValue(pair, flag) {
130
+ const at = pair.indexOf('=');
131
+ if (at < 1)
132
+ throw new Error(`${flag} takes key=value (got "${pair}")`);
133
+ return [pair.slice(0, at), pair.slice(at + 1)];
134
+ }
135
+ /** `--attach <file>`, or `--attach <role>=<file>` when the part before the `=` is not itself a file. */
136
+ function attachmentSpec(spec, base) {
137
+ const role = /^([a-z][a-z0-9._-]*)=(.+)$/.exec(spec);
138
+ const exists = (file) => { try {
139
+ return statSync(path.resolve(base, file)).isFile();
140
+ }
141
+ catch {
142
+ return false;
143
+ } };
144
+ if (role && !exists(spec))
145
+ return { name: path.basename(role[2]), role: role[1], path: role[2] };
146
+ return { name: path.basename(spec), path: spec };
147
+ }
148
+ /** `--link <url>` or `--link <name>=<url>`. */
149
+ function linkSpec(spec) {
150
+ const named = /^([^=/:]+)=(.+)$/.exec(spec);
151
+ return named ? { name: named[1], url: named[2] } : { url: spec };
152
+ }
153
+ function parseTime(text) {
154
+ if (text === undefined)
155
+ return null;
156
+ const numeric = Number(text);
157
+ const parsed = Number.isFinite(numeric) ? numeric : Date.parse(text);
158
+ if (!Number.isFinite(parsed) || parsed < 0)
159
+ throw new Error(`--recorded-at takes an ISO date or epoch milliseconds (got "${text}")`);
160
+ return parsed;
161
+ }
162
+ function ingest(argv, base) {
163
+ const read = (file) => readFileSync(path.resolve(base, file), 'utf8');
164
+ const sources = ['ctrf', 'junit', 'manifest', 'checks'].filter(name => value(argv, name) !== undefined);
165
+ if (sources.length !== 1)
166
+ throw new Error('Give exactly one of --ctrf, --junit, --manifest or --checks');
167
+ const [source] = sources;
168
+ const file = value(argv, source);
169
+ // When the report does not say when it ran, the file does: the same file is the same time.
170
+ const modifiedAtMs = Math.floor(statSync(path.resolve(base, file)).mtimeMs);
171
+ const sourceDir = path.dirname(path.resolve(base, file));
172
+ if (source === 'junit')
173
+ return { ingested: checksFromJunit(read(file)), extra: {}, modifiedAtMs, sourceDir };
174
+ const json = JSON.parse(read(file));
175
+ if (source === 'ctrf')
176
+ return { ingested: checksFromCtrf(json), extra: {}, modifiedAtMs, sourceDir };
177
+ if (source === 'manifest')
178
+ return { ingested: checksFromManifest(json), extra: {}, modifiedAtMs, manifestFile: path.resolve(base, file), sourceDir };
179
+ const given = parseWith(PublishChecksSchema.partial({ producer: true, recordedAtMs: true }), json, file);
180
+ return { ingested: { checks: given.checks, recordedAtMs: given.recordedAtMs ?? null, tool: null }, extra: given, modifiedAtMs, sourceDir };
181
+ }
182
+ /**
183
+ * `atlas report …` for a terminal: a mistake in the command or a path is a message on stderr and exit
184
+ * code 2, so a CI job can tell a broken invocation (2) from a report that does not pass (1).
185
+ */
186
+ export async function runReportCommand(argv) {
187
+ try {
188
+ return await reportMain(argv);
189
+ }
190
+ catch (error) {
191
+ const message = error instanceof Error ? error.message : String(error);
192
+ // With --format json the mistake is a document too, so a caller never has to read prose.
193
+ process.stderr.write(wantsJson(argv) ? `${JSON.stringify({ ok: false, error: { message, code: 'usage' } })}\n` : `atlas report: ${message}\n`);
194
+ return 2;
195
+ }
196
+ }
197
+ /** Does the command ask for a JSON document (`--format json` or `--json`) instead of prose? */
198
+ function wantsJson(argv) {
199
+ return value(argv, 'format') === 'json' || argv.includes('--json');
200
+ }
201
+ function printChecks(out, label, checks) {
202
+ out(`${label}\n`);
203
+ for (const check of checks) {
204
+ out(` ${check.ok ? '✓' : '✗'} ${check.name.padEnd(9)} ${check.detail}\n`);
205
+ if (!check.ok && check.fix)
206
+ out(` → ${check.fix}\n`);
207
+ }
208
+ return checks.every(check => check.ok);
209
+ }
210
+ /** `atlas report …`. Returns the exit code: 0 passes, 1 does not. Throws on a mistake in the command (see {@link runReportCommand}). */
211
+ export async function reportMain(argv, io = { out: text => process.stdout.write(text) }) {
212
+ const [command, ...rest] = argv;
213
+ const base = io.cwd ?? process.cwd();
214
+ const env = io.env ?? process.env;
215
+ const clock = io.clock ?? Date.now;
216
+ if (command === 'publish') {
217
+ const target = writeStoreOf(value(rest, 'store'), base, env);
218
+ const { ingested, extra, modifiedAtMs, manifestFile, sourceDir } = ingest(rest, base);
219
+ // Checked before anything is written: a command that is wrong must not leave the store changed.
220
+ if (rest.includes('--upload-run') && !manifestFile)
221
+ throw new Error('--upload-run goes with --manifest <run>/manifest.json');
222
+ const origin = detectOrigin(env, io.git);
223
+ const producer = value(rest, 'producer') ?? extra.producer;
224
+ if (!producer)
225
+ throw new Error('publish needs --producer <name>: a stable name for this suite, such as "web-e2e" or "gherkin"');
226
+ const recordedAtMs = parseTime(value(rest, 'recorded-at')) ?? extra.recordedAtMs ?? ingested.recordedAtMs ?? modifiedAtMs;
227
+ const parameters = Object.fromEntries(values(rest, 'param').map(pair => keyValue(pair, '--param')));
228
+ const specsDirectory = value(rest, 'specs');
229
+ const linked = await linkChecksToSpecs(ingested, { base, ...(specsDirectory ? { specsDirectory } : {}), ...(value(rest, 'spec') ? { spec: value(rest, 'spec') } : {}) });
230
+ const given = extra.specs ?? [];
231
+ const sha = value(rest, 'sha') ?? extra.sha ?? origin.sha;
232
+ const branch = value(rest, 'branch') ?? extra.branch ?? origin.branch;
233
+ const freshness = extra.freshness ?? detectFreshness({ sha, ci: origin.ci && !value(rest, 'sha') }, io.git);
234
+ // Evidence: the files checks name, the files given with --attach, the labels and links given as flags.
235
+ // What a report names is data from a test run: only its own folder and the roots given may be attached from.
236
+ // What `--attach` names is what the person typed.
237
+ const roots = [sourceDir, ...values(rest, 'attach-root').map(root => path.resolve(base, root))];
238
+ const maxBytes = Number(value(rest, 'max-attachment-mb') ?? 512) * 1024 * 1024;
239
+ const budget = { remaining: Number(value(rest, 'max-total-attachment-mb') ?? 2048) * 1024 * 1024 };
240
+ const warnings = [];
241
+ let attachedFiles = 0, attachedBytes = 0;
242
+ const checksWithFiles = [];
243
+ for (const check of linked.checks) {
244
+ const read = await readAttachmentFiles(check, roots, { maxBytes, budget });
245
+ checksWithFiles.push(read.value);
246
+ attachedFiles += read.attached;
247
+ attachedBytes += read.bytes;
248
+ warnings.push(...read.warnings);
249
+ }
250
+ const fromFile = await readAttachmentFiles({ attachments: extra.attachments ?? [] }, roots, { maxBytes, budget });
251
+ const fromFlags = await readAttachmentFiles({ attachments: values(rest, 'attach').map(spec => attachmentSpec(spec, base)) }, [base, ...roots], { maxBytes, budget, trusted: true });
252
+ const layerAttachments = [...fromFile.value.attachments, ...fromFlags.value.attachments];
253
+ for (const read of [fromFile, fromFlags]) {
254
+ attachedFiles += read.attached;
255
+ attachedBytes += read.bytes;
256
+ warnings.push(...read.warnings);
257
+ }
258
+ const labels = { ...(ingested.labels ?? {}), ...(extra.labels ?? {}), ...Object.fromEntries(values(rest, 'label').map(pair => keyValue(pair, '--label'))) };
259
+ const links = [...(extra.links ?? []), ...values(rest, 'link').map(linkSpec)];
260
+ const layer = await publishChecks(target.store, {
261
+ ...extra,
262
+ producer,
263
+ checks: checksWithFiles,
264
+ // Always set, so a file that was skipped is not passed on by name.
265
+ attachments: layerAttachments,
266
+ ...(Object.keys(labels).length ? { labels } : {}),
267
+ ...(links.length ? { links: links } : {}),
268
+ specs: [...given, ...linked.specs.filter(entry => !given.some(other => other.path === entry.path))],
269
+ recordedAtMs,
270
+ kind: value(rest, 'kind') ?? extra.kind ?? 'checks',
271
+ ...(value(rest, 'title') ?? extra.title ? { title: (value(rest, 'title') ?? extra.title) } : {}),
272
+ sha, branch,
273
+ runUrl: value(rest, 'run-url') ?? extra.runUrl ?? origin.runUrl,
274
+ ...(freshness ? { freshness } : {}),
275
+ ...(value(rest, 'environment') ?? extra.environment ? { environment: (value(rest, 'environment') ?? extra.environment) } : {}),
276
+ parameters: { ...(extra.parameters ?? {}), ...parameters }
277
+ });
278
+ const counts = { passed: 0, failed: 0, other: 0 };
279
+ for (const check of ingested.checks)
280
+ counts[check.status === 'passed' ? 'passed' : check.status === 'failed' ? 'failed' : 'other'] += 1;
281
+ const linkedCount = linked.checks.filter(check => check.specPath).length;
282
+ const hint = linkedCount < linked.checks.length && !value(rest, 'spec') && !specsDirectory ? ' (pass --specs <dir> or --spec <path> to link checks to their specs)' : '';
283
+ const json = wantsJson(rest);
284
+ const result = {
285
+ ok: true, store: target.label,
286
+ layer: { id: layer.id, producer, environment: layer.environment ?? null, checks: ingested.checks.length, passed: counts.passed, failed: counts.failed, notRun: counts.other, linkedToSpec: linkedCount, ...(freshness ? { freshness } : {}), attachmentFiles: attachedFiles, attachmentBytes: attachedBytes },
287
+ warnings: warnings.map(warning => `attachment skipped: ${warning}`)
288
+ };
289
+ if (!json)
290
+ io.out(`Published layer ${layer.id.slice(0, 12)}: ${ingested.checks.length} checks (${counts.passed} passed, ${counts.failed} failed, ${counts.other} not run) as "${producer}"${layer.environment ? ` on ${layer.environment}` : ''}${freshness === 'modified' ? ' [from a modified checkout]' : ''}; ${linkedCount} of ${linked.checks.length} linked to a spec${hint}${attachedFiles ? `; ${attachedFiles} attachment file${attachedFiles === 1 ? '' : 's'} (${(attachedBytes / 1024 / 1024).toFixed(1)} MiB)` : ''}\n`);
291
+ if (!json)
292
+ for (const warning of warnings)
293
+ io.out(`! attachment skipped: ${warning}\n`);
294
+ if (specsDirectory) {
295
+ const files = await collectSpecFiles(path.resolve(base, specsDirectory));
296
+ // A spec named with --spec that the directory does not hold (or whose kind is not a spec file) is published too.
297
+ for (const entry of linked.specs) {
298
+ if (files.some(file => file.path === entry.path))
299
+ continue;
300
+ const text = value(rest, 'spec') ? await readFile(path.resolve(base, value(rest, 'spec')), 'utf8').catch(() => null) : null;
301
+ if (text !== null)
302
+ files.push({ path: entry.path, text });
303
+ }
304
+ // The commit's own time for a clean checkout, so publishing the same commit twice is one set.
305
+ const committed = !freshness && sha ? Number((io.git ?? gitOutput)(['show', '-s', '--format=%ct', sha])) * 1000 : NaN;
306
+ const set = await publishSpecSet(target.store, { files, branch, sha, recordedAtMs: Number.isFinite(committed) && committed > 0 ? committed : clock(), ...(freshness ? { freshness } : {}), domain: path.basename(base) || 'product' });
307
+ result.specSet = { id: set.id, files: set.files.length, branch };
308
+ if (!json)
309
+ io.out(`Published spec set ${set.id.slice(0, 12)}: ${set.files.length} specs${branch ? ` from ${branch}` : ''}\n`);
310
+ }
311
+ if (rest.includes('--upload-run')) {
312
+ const run = await publishRunBundle(target.store, path.dirname(manifestFile));
313
+ result.run = { id: run.id, files: run.files, bytes: run.bytes, alreadyPublished: run.alreadyPublished };
314
+ if (!json)
315
+ io.out(run.alreadyPublished ? `Run ${run.id} was already in the store\n` : `Uploaded run ${run.id}: ${run.files} files, ${(run.bytes / 1024 / 1024).toFixed(1)} MiB\n`);
316
+ }
317
+ if (json)
318
+ io.out(`${JSON.stringify(result, null, 2)}\n`);
319
+ return 0;
320
+ }
321
+ if (command === 'stream') {
322
+ const target = writeStoreOf(value(rest, 'store'), base, env);
323
+ const producer = value(rest, 'producer');
324
+ if (!producer)
325
+ throw new Error('stream needs --producer <name>: a stable name for this suite, such as "web-e2e"');
326
+ const origin = detectOrigin(env, io.git);
327
+ const sha = value(rest, 'sha') ?? origin.sha;
328
+ const branch = value(rest, 'branch') ?? origin.branch;
329
+ const freshness = detectFreshness({ sha, ci: origin.ci && !value(rest, 'sha') }, io.git);
330
+ const environment = value(rest, 'environment');
331
+ const warnings = [];
332
+ const reporter = createReporter({
333
+ producer,
334
+ kind: value(rest, 'kind') ?? 'checks',
335
+ ...(value(rest, 'title') ? { title: value(rest, 'title') } : {}),
336
+ sha, branch,
337
+ runUrl: value(rest, 'run-url') ?? origin.runUrl,
338
+ ...(freshness ? { freshness } : {}),
339
+ ...(environment ? { environment } : {}),
340
+ parameters: Object.fromEntries(values(rest, 'param').map(pair => keyValue(pair, '--param')))
341
+ }, {
342
+ store: target.store,
343
+ flushMs: Number(value(rest, 'flush-ms') ?? 1000),
344
+ batchSize: Number(value(rest, 'batch') ?? 100),
345
+ now: clock,
346
+ onError: (error, willRetry) => { const line = `could not send${willRetry ? ' (will try again)' : ''}: ${error.message}`; if (wantsJson(rest))
347
+ warnings.push(line);
348
+ else
349
+ io.out(`! ${line}\n`); }
350
+ });
351
+ let lines = 0;
352
+ let buffered = '';
353
+ const decoder = new TextDecoder();
354
+ const take = (line) => {
355
+ if (!line.trim())
356
+ return;
357
+ lines += 1;
358
+ try {
359
+ reporter.check(JSON.parse(line));
360
+ }
361
+ catch (error) {
362
+ warnings.push(`line ${lines} skipped: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`);
363
+ }
364
+ };
365
+ for await (const chunk of io.stdin ?? process.stdin) {
366
+ buffered += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
367
+ let newline;
368
+ while ((newline = buffered.indexOf('\n')) >= 0) {
369
+ take(buffered.slice(0, newline));
370
+ buffered = buffered.slice(newline + 1);
371
+ }
372
+ }
373
+ take(buffered);
374
+ const result = await reporter.close();
375
+ if (wantsJson(rest)) {
376
+ io.out(`${JSON.stringify({ ok: result.lost === 0, producer, environment: environment ?? null, layers: result.layers, checks: result.checks, lost: result.lost, warnings }, null, 2)}\n`);
377
+ return result.lost ? 1 : 0;
378
+ }
379
+ for (const warning of warnings)
380
+ io.out(`! ${warning}\n`);
381
+ io.out(`Streamed ${result.checks} checks in ${result.layers} layer${result.layers === 1 ? '' : 's'} as "${producer}"${environment ? ` on ${environment}` : ''}${result.lost ? `; ${result.lost} could not be sent` : ''}\n`);
382
+ return result.lost ? 1 : 0;
383
+ }
384
+ if (command === 'status') {
385
+ const format = value(rest, 'format') ?? 'text';
386
+ if (!['text', 'json', 'markdown'].includes(format))
387
+ throw new Error(`--format takes text|json|markdown (got "${format}")`);
388
+ const backends = await backendsOf(rest, base, env);
389
+ const specsDirectory = value(rest, 'specs');
390
+ // One Atlas server, asked nothing it cannot answer itself: it already holds the layers, so it works out the gate
391
+ // and only the verdict travels, not every layer.
392
+ const server = backends.length === 1 && !specsDirectory && !value(rest, 'outline-branch') ? backends[0].server : undefined;
393
+ if (server) {
394
+ const query = new URLSearchParams();
395
+ for (const branch of values(rest, 'branch'))
396
+ query.append('branch', branch);
397
+ for (const branch of (env.ATLAS_ONLY_BRANCH ?? '').split(/[\s,]+/).filter(Boolean))
398
+ query.append('branch', branch);
399
+ if (rest.includes('--ignore-modified') || env.ATLAS_IGNORE_MODIFIED === '1' || env.ATLAS_IGNORE_MODIFIED === 'true')
400
+ query.set('ignoreModified', '1');
401
+ if (value(rest, 'require-environments'))
402
+ query.set('requireEnvironments', value(rest, 'require-environments'));
403
+ if (rest.includes('--require-all-specs'))
404
+ query.set('requireAllSpecs', '1');
405
+ const answer = ReportStatusSchema.safeParse(await server.status(query));
406
+ // A verdict this reader cannot read is unknown, never a pass.
407
+ if (!answer.success)
408
+ throw new Error(`${server.url} answered something that is not a report status. Upgrade @crvouga/atlas, or ask it with --format json to see it`);
409
+ const status = answer.data;
410
+ io.out(format === 'json' ? `${JSON.stringify(answer.data, null, 2)}\n` : format === 'markdown' ? formatStatusMarkdown(status) : formatStatusText(status));
411
+ return answer.data.ok ? 0 : 1;
412
+ }
413
+ let outline = specsDirectory ? await loadDirectoryOutline(path.resolve(base, specsDirectory), { repoRoot: base }) : undefined;
414
+ if (!outline) {
415
+ // No checkout to read the specs from: the newest set published to the store is the outline.
416
+ const found = await latestSpecSet(backends.map(backend => backend.store), value(rest, 'outline-branch')).catch(() => null);
417
+ if (found)
418
+ outline = { specs: found.set.files.map(({ path: file, domain, kind, title, fingerprint }) => ({ path: file, domain, kind, title, fingerprint })), sha: found.set.sha, changedFiles: [], readSpec: async () => null };
419
+ }
420
+ if (rest.includes('--require-all-specs') && !outline)
421
+ throw new Error('--require-all-specs needs to know which specs to expect: pass --specs <dir>, or publish with --specs so the store holds a spec set');
422
+ const include = layerFilter(rest, env);
423
+ const report = await combineBackends(backends, { ...(outline ? { outline } : {}), ...(include ? { include } : {}) });
424
+ const status = reportStatus(report, {
425
+ requireEnvironments: (value(rest, 'require-environments') ?? '').split(',').map(name => name.trim()).filter(Boolean),
426
+ requireAllSpecs: rest.includes('--require-all-specs')
427
+ });
428
+ io.out(format === 'json' ? `${JSON.stringify(status, null, 2)}\n` : format === 'markdown' ? formatStatusMarkdown(status) : formatStatusText(status));
429
+ return status.ok ? 0 : 1;
430
+ }
431
+ if (command === 'merge') {
432
+ const into = value(rest, 'into');
433
+ if (!into)
434
+ throw new Error('merge needs --into <dir or s3://bucket/prefix>');
435
+ const destination = writeStoreOf(into, base, env, 'into');
436
+ let copied = 0, skipped = 0, ignored = 0;
437
+ for (const backend of await backendsOf(rest, base, env)) {
438
+ const result = await copyStore(backend.store, destination.store);
439
+ copied += result.copied;
440
+ skipped += result.skipped;
441
+ ignored += result.ignored;
442
+ }
443
+ if (wantsJson(rest)) {
444
+ io.out(`${JSON.stringify({ ok: true, into, copied, skipped, ignored }, null, 2)}\n`);
445
+ return 0;
446
+ }
447
+ io.out(`Merged into ${into}: ${copied} objects copied, ${skipped} already there${ignored ? `; ${ignored} objects outside layers/ were not copied (only layers are merged)` : ''}\n`);
448
+ return 0;
449
+ }
450
+ if (command === 'doctor') {
451
+ const role = (value(rest, 'role') ?? 'both');
452
+ if (!['publisher', 'reader', 'both'].includes(role))
453
+ throw new Error(`--role takes publisher|reader|both (got "${role}")`);
454
+ let healthy = true;
455
+ const stores = [];
456
+ for (const backend of await backendsOf(rest, base, env)) {
457
+ const checks = await inspectStore(backend, { role });
458
+ stores.push({ id: backend.id, checks });
459
+ if (wantsJson(rest))
460
+ healthy = checks.every(check => check.ok) && healthy;
461
+ else
462
+ healthy = printChecks(io.out, backend.id, checks) && healthy;
463
+ }
464
+ if (wantsJson(rest))
465
+ io.out(`${JSON.stringify({ ok: healthy, stores }, null, 2)}\n`);
466
+ else
467
+ io.out(healthy ? 'All good.\n' : 'Something needs attention (see →).\n');
468
+ return healthy ? 0 : 1;
469
+ }
470
+ if (command === 'policy') {
471
+ const specs = storeSpecs(values(rest, 'store'), env);
472
+ const opened = specs.length === 1 ? openStore(specs[0], { base, env }) : null;
473
+ if (!opened || opened.location.kind !== 's3')
474
+ throw new Error('policy needs one --store s3://bucket/prefix (or ATLAS_STORE)');
475
+ const role = (value(rest, 'role') ?? 'both');
476
+ if (!['publisher', 'reader', 'both'].includes(role))
477
+ throw new Error(`--role takes publisher|reader|both (got "${role}")`);
478
+ io.out(`${JSON.stringify(iamPolicy(opened.location, role), null, 2)}\n`);
479
+ if (opened.location.endpoint)
480
+ io.out(`\n# ${opened.location.endpoint} is not AWS: this is the AWS form. Grant the same actions in its own policy language.\n`);
481
+ return 0;
482
+ }
483
+ io.out(REPORT_USAGE);
484
+ return command && command !== '--help' && command !== '-h' ? 2 : 0;
485
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @module Incremental report index
3
+ *
4
+ * `ReportIndex` is the report, maintained as layers arrive. Each immutable layer is read once and folded into the winning
5
+ * result of every slot it fills, then dropped unless it still holds a winner, so memory follows the number of checks and not
6
+ * the age of the project. The winner rule is a total order, so folding is exact in any order and equals combining every layer
7
+ * from scratch (a test proves it over random partial runs). It also keeps what each layer sends (rows, JSON, a digest) until
8
+ * that layer changes, so taking in a result costs one layer, answers viewers a version hash, a cached JSON text and a delta,
9
+ * and tells listeners what arrived.
10
+ *
11
+ * @see docs/report-contract.md
12
+ * @see docs/server.md#performance
13
+ */
14
+ import type { BackendInfo, LayerEvidence, LayerHeader } from './evidence-types.js';
15
+ import type { BackendInput } from './compose.js';
16
+ import type { ReportLayer } from './layer-types.js';
17
+ /** What a viewer needs to show the report, and nothing that is only history. */
18
+ export type EffectiveEvidence = {
19
+ /** A hash of the report itself: the same on every replica, and different exactly when the report is. Use it as an ETag. */
20
+ readonly version: string;
21
+ /** Only layers that fill at least one slot, each with only the rows that win. */
22
+ readonly layers: readonly LayerEvidence[];
23
+ readonly backends: readonly BackendInfo[];
24
+ readonly notices: readonly string[];
25
+ /** Every layer read, superseded or not. */
26
+ readonly totalLayers: number;
27
+ };
28
+ /** What changed, told to those who asked to know: the report's new version, and the layers that arrived. */
29
+ export type IndexChange = {
30
+ readonly version: string;
31
+ /** Layers read since the last change, newest first, whether or not they won anything. */
32
+ readonly added: readonly LayerHeader[];
33
+ readonly totalLayers: number;
34
+ };
35
+ /**
36
+ * The report, maintained as layers arrive. Each layer is read once and folded into the winning result
37
+ * of every slot it fills, then dropped unless it still holds a winner. Because the winner rule is a
38
+ * total order, folding is exact in any order, so this equals combining every layer from scratch, with
39
+ * memory proportional to the number of checks and not to how long the project has existed.
40
+ *
41
+ * It reads only immutable layers (`layers/index/…`), which is what makes "read once" correct.
42
+ */
43
+ export declare class ReportIndex {
44
+ #private;
45
+ constructor(backends: readonly BackendInput[], options?: {
46
+ readonly concurrency?: number;
47
+ readonly now?: () => number; /** Look at the whole catalog every this many refreshes; in between only its newest page. Default 30. */
48
+ readonly fullListEvery?: number; /** Leave layers out of the report, as a gate on `main` does. */
49
+ readonly include?: (layer: LayerHeader) => boolean;
50
+ });
51
+ /** Read what is new in every backend and fold it in. Resolves true when the report changed. Calls overlap safely. */
52
+ refresh(): Promise<boolean>;
53
+ /**
54
+ * A refresh that surely starts after this call: one already under way may have looked before whatever
55
+ * made the caller ask. Used when something was just written.
56
+ */
57
+ refreshNow(): Promise<boolean>;
58
+ /**
59
+ * Take in a layer an uploader has just written, from its catalog entry, without listing anything. Entries
60
+ * are read one after another; one already known is ignored. The regular refresh still reconciles the rest.
61
+ */
62
+ accept(backendId: string, indexKey: string, known?: ReportLayer): Promise<void>;
63
+ /** Be told when the report changes. Returns how to stop. */
64
+ onChange(listener: (change: IndexChange) => void): () => void;
65
+ /** Keep refreshing in the background. The first call returns once the first pass has finished. */
66
+ start(intervalMs: number): Promise<void>;
67
+ /** Stops the background refresh started by `start`. Safe to call twice. */
68
+ stop(): void;
69
+ /** True once a first pass over the backends has finished, so `snapshot` is a report and not an empty guess. */
70
+ get ready(): boolean;
71
+ /** The current report, ready to send. Built once per change, and only the layers that changed are rebuilt. */
72
+ snapshot(): EffectiveEvidence;
73
+ /**
74
+ * The report as JSON text: exactly what `JSON.stringify({ ...snapshot(), active: [] })` writes, put together from
75
+ * the text of each layer, which is written once. `active` is what a store without live runs always sends: none.
76
+ */
77
+ json(): {
78
+ readonly version: string;
79
+ readonly text: string;
80
+ };
81
+ /**
82
+ * What differs between the report a viewer holds (named by its version) and the current one: the layers that
83
+ * changed in full, and the ids of those that no longer hold a winner. Null when that version is not remembered,
84
+ * and the viewer should ask for the whole report. See `applyEffectiveDelta`.
85
+ */
86
+ delta(since: string): {
87
+ readonly text: string;
88
+ readonly version: string;
89
+ } | null;
90
+ /** For `/readyz` and logs. */
91
+ stats(): {
92
+ backends: number;
93
+ layers: number;
94
+ slots: number;
95
+ holding: number;
96
+ failed: number;
97
+ refreshedAt: number;
98
+ };
99
+ }