@crvouga/atlas 0.5.0 → 0.6.1

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 (148) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +51 -16
  3. package/dist/core/src/cli.d.ts +1 -1
  4. package/dist/core/src/cli.js +8 -3
  5. package/dist/core/src/config.d.ts +7 -0
  6. package/dist/core/src/config.js +48 -14
  7. package/dist/core/src/run/execute.d.ts +6 -0
  8. package/dist/core/src/run/execute.js +4 -2
  9. package/dist/reports/src/doctor.d.ts +1 -1
  10. package/dist/reports/src/doctor.js +3 -1
  11. package/dist/reports/src/http-store.d.ts +4 -0
  12. package/dist/reports/src/http-store.js +6 -3
  13. package/dist/reports/src/init.js +24 -16
  14. package/dist/reports/src/live-reporter.d.ts +34 -0
  15. package/dist/reports/src/live-reporter.js +122 -0
  16. package/dist/reports/src/package-managers.d.ts +35 -0
  17. package/dist/reports/src/package-managers.js +23 -0
  18. package/dist/reports/src/report-cli.d.ts +1 -1
  19. package/dist/reports/src/report-cli.js +3 -2
  20. package/dist/reports/src/store-url.js +1 -1
  21. package/dist/runners/src/codecept.d.ts +14 -0
  22. package/dist/runners/src/codecept.js +16 -0
  23. package/dist/runners/src/jest.d.ts +50 -0
  24. package/dist/runners/src/jest.js +52 -0
  25. package/dist/runners/src/mocha.d.ts +40 -0
  26. package/dist/runners/src/mocha.js +39 -0
  27. package/dist/runners/src/node-test.d.ts +28 -0
  28. package/dist/runners/src/node-test.js +44 -0
  29. package/dist/runners/src/publish.d.ts +30 -0
  30. package/dist/runners/src/publish.js +68 -0
  31. package/dist/schema/src/api.d.ts +1 -0
  32. package/dist/schema/src/api.js +1 -1
  33. package/dist/schema/src/cli-results.d.ts +233 -2
  34. package/dist/schema/src/cli-results.js +14 -0
  35. package/dist/schema/src/index.d.ts +2 -0
  36. package/dist/schema/src/index.js +2 -0
  37. package/dist/schema/src/live-run.d.ts +123 -0
  38. package/dist/schema/src/live-run.js +30 -0
  39. package/dist/schema/src/manifest.d.ts +2 -2
  40. package/dist/schema/src/project.d.ts +20 -0
  41. package/dist/schema/src/project.js +10 -0
  42. package/dist/schema/src/report-sources.d.ts +1 -0
  43. package/dist/schema/src/report-sources.js +2 -1
  44. package/dist/schema/src/runs-index.d.ts +1 -1
  45. package/dist/schema/src/server-config.d.ts +84 -0
  46. package/dist/schema/src/server-config.js +12 -0
  47. package/dist/server/src/app.d.ts +10 -0
  48. package/dist/server/src/app.js +148 -10
  49. package/dist/server/src/config.d.ts +1 -1
  50. package/dist/server/src/config.js +20 -3
  51. package/dist/server/src/dev.d.ts +3 -3
  52. package/dist/server/src/dev.js +22 -13
  53. package/dist/server/src/events.d.ts +3 -3
  54. package/dist/server/src/events.js +1 -1
  55. package/dist/server/src/index.d.ts +2 -0
  56. package/dist/server/src/index.js +2 -0
  57. package/dist/server/src/live-runs.d.ts +52 -0
  58. package/dist/server/src/live-runs.js +229 -0
  59. package/dist/server/src/llms.js +17 -8
  60. package/dist/server/src/main.d.ts +1 -1
  61. package/dist/server/src/main.js +7 -5
  62. package/dist/server/src/openapi.d.ts +2 -5
  63. package/dist/server/src/openapi.js +71 -7
  64. package/dist/server/src/projects.d.ts +3 -0
  65. package/dist/server/src/projects.js +154 -0
  66. package/dist/server/src/settings-page.d.ts +9 -0
  67. package/dist/server/src/settings-page.js +93 -0
  68. package/dist/server/src/settings.d.ts +23 -0
  69. package/dist/server/src/settings.js +72 -0
  70. package/dist/server/src/site.d.ts +8 -6
  71. package/dist/server/src/site.js +45 -10
  72. package/dist/server/src/store-api.js +43 -22
  73. package/docs/agents.md +166 -15
  74. package/docs/deploying.md +24 -20
  75. package/docs/dogfooding.md +7 -3
  76. package/docs/extending.md +3 -3
  77. package/docs/integrating.md +19 -11
  78. package/docs/quickstart.md +81 -0
  79. package/docs/reference/cli.md +9 -7
  80. package/docs/reference/core.md +6 -0
  81. package/docs/reference/http-api.md +498 -2
  82. package/docs/reference/reports.md +84 -1
  83. package/docs/reference/runners.md +169 -0
  84. package/docs/reference/schema.md +120 -0
  85. package/docs/reference/server.md +154 -14
  86. package/docs/report-backends.md +8 -8
  87. package/docs/report-contract.md +7 -7
  88. package/docs/reporting-issues.md +3 -3
  89. package/docs/runners.md +69 -0
  90. package/docs/server.md +155 -25
  91. package/docs/statecharts.md +22 -20
  92. package/llms-full.txt +1079 -126
  93. package/llms.txt +7 -4
  94. package/package.json +33 -9
  95. package/packages/core/README.md +3 -1
  96. package/packages/runners/README.md +6 -0
  97. package/packages/runners/compat/codecept.cjs +2 -0
  98. package/packages/runners/compat/jest.cjs +2 -0
  99. package/packages/runners/compat/mocha.cjs +2 -0
  100. package/packages/schema/schemas/atlas-change.schema.json +4 -0
  101. package/packages/schema/schemas/hello-event.schema.json +4 -0
  102. package/packages/schema/schemas/live-run-start.schema.json +42 -0
  103. package/packages/schema/schemas/live-run-update.schema.json +93 -0
  104. package/packages/schema/schemas/live-run.schema.json +225 -0
  105. package/packages/schema/schemas/project-slug.schema.json +8 -0
  106. package/packages/schema/schemas/project.schema.json +22 -0
  107. package/packages/schema/schemas/projects.schema.json +33 -0
  108. package/packages/schema/schemas/server-config.schema.json +14 -0
  109. package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
  110. package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
  111. package/packages/server/openapi.json +5101 -779
  112. package/packages/visualizer/README.md +13 -13
  113. package/packages/visualizer/bin/atlas-client.mjs +176 -0
  114. package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
  115. package/packages/visualizer/src/app/AppShell.tsx +26 -17
  116. package/packages/visualizer/src/app/report-sources.tsx +2 -1
  117. package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
  118. package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
  119. package/packages/visualizer/src/components/Header.module.css +101 -227
  120. package/packages/visualizer/src/components/Header.tsx +79 -41
  121. package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
  122. package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
  123. package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
  124. package/packages/visualizer/src/data/queries.ts +14 -7
  125. package/packages/visualizer/src/evidence/api.ts +2 -1
  126. package/packages/visualizer/src/evidence/check-detail.tsx +19 -4
  127. package/packages/visualizer/src/evidence/document-page.tsx +6 -2
  128. package/packages/visualizer/src/evidence/evidence-context.tsx +17 -12
  129. package/packages/visualizer/src/evidence/evidence.module.css +11 -1
  130. package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
  131. package/packages/visualizer/src/evidence/model-page.tsx +6 -2
  132. package/packages/visualizer/src/evidence/project-home.tsx +45 -0
  133. package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
  134. package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
  135. package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
  136. package/packages/visualizer/src/live/live-query.ts +21 -0
  137. package/packages/visualizer/src/live/live-runs.tsx +69 -0
  138. package/packages/visualizer/src/live/live-stream.ts +30 -9
  139. package/packages/visualizer/src/live/report-progress.ts +62 -0
  140. package/packages/visualizer/src/live/test-progress.tsx +34 -0
  141. package/packages/visualizer/src/runtime.ts +17 -1
  142. package/packages/visualizer/src/state/ui-store.ts +3 -2
  143. package/packages/visualizer/src/views/ChartPage.tsx +2 -0
  144. package/packages/visualizer/src/views/ChartTests.tsx +43 -0
  145. package/packages/visualizer/src/views/SidePanel.tsx +2 -0
  146. package/packages/visualizer/vite/atlas-files.test.ts +4 -4
  147. package/packages/visualizer/vite/atlas-files.ts +1 -5
  148. package/packages/visualizer/vite/atlas-server-proxy.ts +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog: @crvouga/atlas
2
2
 
3
+ ## 0.6.1 (2026-10-15)
4
+
5
+ ### Fixes and improvements
6
+
7
+ - make consumer integration reproducible (#9) ([bfd4e0c](https://github.com/crvouga/atlas/commit/bfd4e0c1c804d1f11fce3299e609af86c50e4732))
8
+
9
+ ## 0.6.0 (2026-10-14)
10
+
11
+ ### Features
12
+
13
+ - add project-scoped live test reporting (#8) ([70936ab](https://github.com/crvouga/atlas/commit/70936ab5b2c6bec5e50b458fc1c3030a3f20bc55))
14
+
3
15
  ## 0.5.0 (2026-10-14)
4
16
 
5
17
  ### Features
package/README.md CHANGED
@@ -7,7 +7,7 @@ backends, and tells you in one command whether the product still does what it wa
7
7
 
8
8
  - **Any spec, any runner.** Anything that ends in "this check passed or failed" publishes a *layer*:
9
9
  from a JUnit or CTRF file, a ten-line script, or code. No runner and no notation is privileged.
10
- - **Layered, never partitioned.** A report is computed from layers, not stored as "the run". Partial
10
+ - **Layered within each project.** A report is computed from layers, not stored as "the run". Partial
11
11
  runs on separate nodes, branches and devices are how a report is built, and they compose without
12
12
  coordinating: the same layers give the same report in any order, split any way.
13
13
  - **Open evidence.** A result carries whatever your framework produces: screenshots, videos, traces, logs, DOM
@@ -16,9 +16,9 @@ backends, and tells you in one command whether the product still does what it wa
16
16
  Gherkin, Quint, DMN, Alloy, OpenSLO); evidence stays open.
17
17
  - **Open standards, stable schemas.** JUnit XML and CTRF in, JSON Schema 2020-12 contracts, a
18
18
  versioned layer format with a golden fixture, and an `ObjectStore` interface any bucket fits.
19
- - **One server, live.** A project runs one `atlas serve`: a Bun API that serves the viewer from the same host.
19
+ - **One node, many projects.** `atlas server` runs a Bun API and serves the Atlas client from the same host. Reports, specs, recordings and live progress are partitioned by project slug.
20
20
  Every laptop and CI job sends results to it as they finish (`atlas report publish`, or `atlas report stream` while
21
- a suite runs), it keeps them on its own disk (or in S3, R2, MinIO), and every viewer sees the combined report
21
+ a suite runs), it keeps them on its own disk (or in S3, R2, MinIO), and every client sees the combined report
22
22
  update live, receiving only what changed ([the server](docs/server.md), [deploying](docs/deploying.md)).
23
23
  - **Fast and fail-closed.** Publishing is two small writes. A thousand layers of a hundred checks
24
24
  combine in about a second. An unreadable layer, a downed backend or a check still running is never
@@ -28,7 +28,7 @@ backends, and tells you in one command whether the product still does what it wa
28
28
 
29
29
  ```
30
30
  any suite ──▶ layer ─┐ atlas report status exit 1 unless it passes
31
- any node ──▶ layer ─┼─▶ backends ─▶ combine ─▶ the visualizer browse it beside the specs
31
+ any node ──▶ layer ─┼─▶ backends ─▶ combine ─▶ the Atlas client browse it beside the specs
32
32
  any branch ──▶ layer ─┘ (directories, buckets, …) combineBackends() your own code
33
33
  ```
34
34
 
@@ -40,6 +40,34 @@ backends, and tells you in one command whether the product still does what it wa
40
40
  > [using Atlas from another project](docs/dogfooding.md) and [reporting issues](docs/reporting-issues.md).
41
41
  > Releases are automatic: see [releasing](https://github.com/crvouga/atlas/blob/main/docs/releasing.md).
42
42
 
43
+ ## Given this repo link to integrate Atlas?
44
+
45
+ Start with **[the agent integration playbook](docs/agents.md#given-only-this-repository-link)** in your own
46
+ project. It covers choosing an integration, installing with your package manager, finding docs for your installed
47
+ version, running without a server, connecting real suites, and proving completion. **[Verify Atlas locally](docs/quickstart.md)**
48
+ is a copyable walkthrough with expected JSON and exit codes; it needs no credentials or existing test suite.
49
+ The machine-readable index is [llms.txt](llms.txt), and [llms-full.txt](llms-full.txt) contains the guides in one file.
50
+
51
+ ## Run this repository's example
52
+
53
+ For contributors working in an Atlas checkout with dependencies installed (`bun install --frozen-lockfile`).
54
+ Consumer projects install the npm package instead; they do not need to clone or build this repository.
55
+
56
+ Use two terminals:
57
+
58
+ ```sh
59
+ bun atlas # starts the project-agnostic Atlas node at http://localhost:5180
60
+ bun example:run # runs tests and streams results, current steps and progress as they execute
61
+ ```
62
+
63
+ The server starts independently with data in `./atlas-data`; it never loads example specs or reports. `bun example:run` publishes the Todo example under `todo-app`. Select a project using the top-right picker, then use the spec dropdown breadcrumbs to navigate. Other projects set `ATLAS_SERVER_URL`, `ATLAS_PROJECT` (for example `checkout-service`) and their publish token. Every uploader talks to the node; storage credentials stay on the server.
64
+
65
+ Every Gherkin scenario runs, including sharing and renaming. Test reports show running spinners,
66
+ live step logs and a Running status filter. Producers send heartbeats every
67
+ 5 seconds. A run becomes interrupted after 30 seconds without a heartbeat, or 15 minutes without progress;
68
+ configure `ATLAS_RUN_TIMEOUT_SECONDS` and `ATLAS_RUN_STALL_SECONDS` (0 disables the progress timeout),
69
+ or edit these settings in the client. An interruption never turns missing outcomes into passes.
70
+
43
71
  ## For AI agents
44
72
 
45
73
  Atlas is built to be integrated and operated by agents, with nothing to click and nothing to guess.
@@ -47,20 +75,21 @@ Atlas is built to be integrated and operated by agents, with nothing to click an
47
75
  - **`atlas init`** detects a project's test runners, specs and CI and prints a plan (`--format json`) of the exact commands to
48
76
  run each suite and send its results; `--write` creates the CI workflow and an `AGENTS.md` section. Running it twice
49
77
  changes nothing.
50
- - **Every command prints JSON on request** (`--format json`), every document has a published JSON Schema, exit codes mean the
51
- same everywhere (`0` worked, `1` the answer is no, `2` the command was wrong), and every operation is safe to repeat.
78
+ - **Init and report commands print JSON on request** (`--format json`) with published JSON Schemas and consistent
79
+ exit codes (`0` worked, `1` the answer is no, `2` the command was wrong). Reapplying init or publishing identical evidence is safe.
52
80
  - **The server describes itself:** `GET /api/openapi.json` (OpenAPI 3.1) and `GET /llms.txt` (a guide for an agent given only
53
81
  the address) need no token; errors are RFC 9457 problem documents that say what to do.
54
82
  - **Documentation is generated from the code beside it:** `llms.txt` / `llms-full.txt` (also in the package), the TypeScript
55
83
  reference in [`docs/reference/`](docs/reference/server.md), the CLI and HTTP references, and the settings tables; tests fail when
56
84
  any of it drifts.
57
85
 
58
- Start with **[Atlas for AI agents](docs/agents.md)**.
86
+ Start with **[Atlas for AI agents](docs/agents.md)**. A [copyable integration brief](docs/agents.md#an-integration-brief-you-can-give-an-agent)
87
+ lets a user hand an agent this repository URL and a concrete definition of done.
59
88
 
60
89
  ## Integrate in five minutes
61
90
 
62
91
  ```sh
63
- bun add -d @crvouga/atlas # or npm install -D / pnpm add -D; runs on Bun and Node 22.18+
92
+ bun add -d @crvouga/atlas # or npm install -D / pnpm add -D
64
93
 
65
94
  # 1. Each suite publishes a layer of what it ran: a JUnit or CTRF file, an Atlas run, or a checks file.
66
95
  bunx atlas report publish --store atlas-reports --producer web-e2e --junit out.xml --specs ./specs --environment desktop-web
@@ -88,7 +117,11 @@ bunx atlas report status --store all --specs ./specs --require-environments desk
88
117
  ## One package
89
118
 
90
119
  Atlas is a single npm package. Each part is its own import path, so a bundler keeps only what you
91
- import. It runs on Bun and on Node 22.18 or later, with any package manager:
120
+ import. Use your project's package manager:
121
+
122
+ <!-- generated:runtime-requirements -->
123
+ Supported runtimes (from `package.json`): **node `>=22.18.0`** or **bun `>=1.2.0`**. Node consumers do not need Bun unless they use the Bun WebView driver.
124
+ <!-- /generated:runtime-requirements -->
92
125
 
93
126
  ```sh
94
127
  bun add -d @crvouga/atlas
@@ -105,10 +138,10 @@ bun add -d @crvouga/atlas
105
138
  | [`@crvouga/atlas/webdriver`](packages/webdriver) | W3C WebDriver driver with no dependencies: Safari, Chrome, Firefox and Edge, and native iOS and Android apps through Appium. |
106
139
  | [`@crvouga/atlas/detox`](packages/detox) | Detox driver for iOS and Android apps, inside a Detox test run. |
107
140
  | [`@crvouga/atlas/vitest`](packages/vitest) | Run a config as a test suite in Vitest, `bun test` or any Jest-style runner, one test per planned path. |
108
- | [`atlas-visualizer`](packages/visualizer) (command) | Browse specs, charts and layered reports: every spec with its result per environment, every state with its screenshots, every transition with its clip. |
141
+ | [`atlas client`](packages/visualizer) (command) | Browse specs, charts and layered reports: every spec with its result per environment, every state with its screenshots, every transition with its clip. |
109
142
 
110
143
  Two commands install with it: `atlas` (`report`, `lint`, `plan`, `run`, `merge`, `export`) and
111
- `atlas-visualizer`.
144
+ `atlas client`.
112
145
 
113
146
  ## Reports for any spec
114
147
 
@@ -147,10 +180,10 @@ has results. `--format json` is a [stable schema](packages/schema/schemas/report
147
180
  means the report does not pass; 2 means the command itself was wrong (a missing store directory, a bad
148
181
  flag).
149
182
 
150
- **The server** is `atlas serve`: one process that serves the viewer, takes in results and streams the report to
151
- every viewer. Point `ATLAS_STORE` at it and every developer's laptop and every CI job sends results with the same
183
+ **The server** is `atlas serve`: one process that serves the client, takes in results and streams the report to
184
+ every client. Point `ATLAS_STORE` at it and every developer's laptop and every CI job sends results with the same
152
185
  commands that write to a folder; everyone then looks at one report, live. Results are kept in its data folder
153
- unless you choose a bucket. `atlas dev` runs the same server with the viewer's dev server in front, so
186
+ unless you choose a bucket. `atlas dev` runs the same server with the client's dev server in front, so
154
187
  development is one host too. See [the server](docs/server.md) and
155
188
  [running Atlas for a project](docs/deploying.md).
156
189
 
@@ -163,7 +196,7 @@ my-suite --json | atlas report stream --producer e2e --environment ios # or as
163
196
  atlas report status --branch main --ignore-modified # the gate, answered by the server
164
197
  ```
165
198
 
166
- **The viewer** shows each spec (Gherkin, Quint, DMN, Alloy, OpenSLO, and the rest by file type) with
199
+ **The client** shows each spec (Gherkin, Quint, DMN, Alloy, OpenSLO, and the rest by file type) with
167
200
  its combined result per environment and the layer behind it. See
168
201
  [integrating Atlas](docs/integrating.md#6-browse-it).
169
202
 
@@ -190,7 +223,7 @@ understand, or a JSON Schema it publishes.
190
223
  | [JUnit XML](https://github.com/testmoapp/junitxml) | Read: any test runner's results become a layer. Written: `junit.xml`, one test case per statechart path. |
191
224
  | [CTRF](https://ctrf.io) | Read: any CTRF report becomes a layer. Written: `ctrf.json` for each statechart run. |
192
225
  | [JSON Schema 2020-12](https://json-schema.org/draft/2020-12) | The contract: layers, check files, report status, run manifests, journeys, state nodes, timelines and the runs index (`@crvouga/atlas/schemas/*.schema.json`) |
193
- | [Gherkin](https://cucumber.io/docs/gherkin/), [Quint](https://quint-lang.org), [DMN](https://www.omg.org/spec/DMN), [Alloy](https://alloytools.org), [OpenSLO](https://openslo.com) | Spec kinds the viewer lays results over, each in its own tab. Any other file is a spec of its own type. |
226
+ | [Gherkin](https://cucumber.io/docs/gherkin/), [Quint](https://quint-lang.org), [DMN](https://www.omg.org/spec/DMN), [Alloy](https://alloytools.org), [OpenSLO](https://openslo.com) | Spec kinds the client lays results over, each in its own tab. Any other file is a spec of its own type. |
194
227
  | [W3C SCXML](https://www.w3.org/TR/scxml/) | Statecharts (`*.scxml`), with business metadata in an `atlas` XML namespace |
195
228
  | [XState v5](https://stately.ai/docs/machines) machine config | Statecharts (`*.machine.json`, `*.machine.yaml`): the JSON shape Stately Studio imports and exports |
196
229
  | [WebVTT](https://www.w3.org/TR/webvtt1/) | Captions for every clip: each tap, keystroke, system call and screen change |
@@ -228,3 +261,5 @@ in **[Executable statecharts](docs/statecharts.md)**, which starts with a runnab
228
261
  ## License
229
262
 
230
263
  [MIT](LICENSE)
264
+
265
+ Native [CodeceptJS, Mocha, Jest and Node test reporters](docs/runners.md) upload directly to the project server. Open **Settings** in the client to configure storage and report settings for the next restart.
@@ -9,6 +9,6 @@
9
9
  * @see docs/reference/cli.md
10
10
  */
11
11
  /** The text `atlas --help` prints: every command, and the flags of the statechart commands. */
12
- export declare const USAGE = "atlas \u2014 executable statecharts\n\n atlas lint [--config atlas.config.ts | --specs <dir>] [--require-implementations]\n atlas plan [--config atlas.config.ts] [--state <name>]\n atlas run [--config atlas.config.ts] [--mode fast|showcase] [--workers n] [--state <name>]\n [--seed <name>]... [--paths id,id] [--shard i/n] [--rerun-failed | --reuse <run-dir>]\n [--no-retry] [--output <dir>]\n atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...\n atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]\n atlas serve [--data <dir>] [--port 8080] [--store <url>]... the Atlas server: results in, the live report out\n atlas dev [--port 5180] the server and the viewer's dev server, on one host\n atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)\n atlas report publish|stream|status|merge|doctor|policy \u2026 layered evidence for any kind of spec (atlas report --help)\n\n --workers path attempts that run at once (default: one per CPU, capped by the driver)\n --seed only paths that start from this seed (\"start\" for paths setup starts); repeatable\n --shard 2/4 run the second quarter of the plan; merge the shards' runs afterwards\n --rerun-failed keep the newest run's passed paths and run only the rest\n";
12
+ export declare const USAGE = "atlas \u2014 executable statecharts\n\n atlas lint [--config atlas.config.ts | --specs <dir>] [--require-implementations]\n atlas plan [--config atlas.config.ts] [--state <name>]\n atlas run [--config atlas.config.ts] [--mode fast|showcase] [--workers n] [--state <name>]\n [--seed <name>]... [--paths id,id] [--shard i/n] [--rerun-failed | --reuse <run-dir>]\n [--no-retry] [--output <dir>]\n atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...\n atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]\n atlas server [--data <dir>] [--port 8080] [--store <url>]... one Atlas node: API, projects and client\n atlas client build [--no-data] [--out <dir>] build the project-agnostic Atlas client\n atlas dev [--port 5180] develop the Atlas server and client on one host\n atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)\n atlas report publish|stream|status|merge|doctor|policy \u2026 layered evidence for any kind of spec (atlas report --help)\n\n --workers path attempts that run at once (default: one per CPU, capped by the driver)\n --seed only paths that start from this seed (\"start\" for paths setup starts); repeatable\n --shard 2/4 run the second quarter of the plan; merge the shards' runs afterwards\n --rerun-failed keep the newest run's passed paths and run only the rest\n";
13
13
  /** Runs `atlas` with `argv` (default: the process arguments) and returns the exit code. Throws for a command that is wrong, which the `atlas` binary reports as a message and exit code 2. */
14
14
  export declare function main(argv?: string[]): Promise<number>;
@@ -28,8 +28,9 @@ export const USAGE = `atlas — executable statecharts
28
28
  [--no-retry] [--output <dir>]
29
29
  atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...
30
30
  atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]
31
- atlas serve [--data <dir>] [--port 8080] [--store <url>]... the Atlas server: results in, the live report out
32
- atlas dev [--port 5180] the server and the viewer's dev server, on one host
31
+ atlas server [--data <dir>] [--port 8080] [--store <url>]... one Atlas node: API, projects and client
32
+ atlas client build [--no-data] [--out <dir>] build the project-agnostic Atlas client
33
+ atlas dev [--port 5180] develop the Atlas server and client on one host
33
34
  atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
34
35
  atlas report publish|stream|status|merge|doctor|policy … layered evidence for any kind of spec (atlas report --help)
35
36
 
@@ -88,10 +89,14 @@ async function loadConfig(file) {
88
89
  export async function main(argv = process.argv.slice(2)) {
89
90
  const [command] = argv;
90
91
  // Loaded on demand: lint, plan and run do not pay for the reports.
91
- if (command === 'serve') {
92
+ if (command === 'serve' || command === 'server') {
92
93
  const { serveMain } = await import('../../server/src/main.js');
93
94
  return serveMain(argv.slice(1), { out: text => process.stdout.write(text) });
94
95
  }
96
+ if (command === 'client') {
97
+ const { clientMain } = await import('../../server/src/site.js');
98
+ return clientMain(argv.slice(1));
99
+ }
95
100
  if (command === 'dev') {
96
101
  const { devMain } = await import('../../server/src/dev.js');
97
102
  return devMain(argv.slice(1), { out: text => process.stdout.write(text) });
@@ -1,3 +1,4 @@
1
+ import { type LiveReporterOptions } from '../../reports/src/live-reporter.js';
1
2
  import type { StateValue } from './graph.js';
2
3
  import type { Shard } from './run/execute.js';
3
4
  import type { Driver, EventMatcher, EventSource, Implementation, PrivacyRules, RunMode } from './run/types.js';
@@ -8,6 +9,8 @@ import type { Driver, EventMatcher, EventSource, Implementation, PrivacyRules, R
8
9
  export type AtlasConfig<C = unknown> = {
9
10
  /** The directory holding the charts (`*.scxml`, `*.machine.{json,yaml}`) and `journeys.yaml`. */
10
11
  specs: string;
12
+ /** Live reporting endpoint. Defaults to ATLAS_SERVER_URL when set; false disables streaming. */
13
+ reporting?: LiveReporterOptions | false;
11
14
  /** The chart to run; the root chart when omitted. */
12
15
  chart?: string;
13
16
  /** Where generated paths start (an XState state value); the initial state when omitted. */
@@ -125,6 +128,8 @@ export declare function prepare<C>(config: AtlasConfig<C>, baseDirectory?: strin
125
128
  export type RunCommandOptions = PrepareOptions & {
126
129
  /** `fast` when omitted. */
127
130
  mode?: RunMode;
131
+ /** Override live reporting. Defaults to config.reporting or ATLAS_SERVER_URL. */
132
+ reporting?: LiveReporterOptions | false;
128
133
  /** Only these paths, by id. */
129
134
  paths?: string[];
130
135
  /** Only paths that start from these seeds ("start" for paths that `setup` starts). */
@@ -260,6 +265,7 @@ export declare function runConfig<C>(config: AtlasConfig<C>, options?: RunComman
260
265
  key: string;
261
266
  name: string;
262
267
  kind: import('./plan.js').PlannedPath["kind"];
268
+ progress?: "queued" | "running" | "complete";
263
269
  status: import('./index.js').Status;
264
270
  attempts: {
265
271
  status: "passed" | "failed";
@@ -458,6 +464,7 @@ export declare function mergeConfigRuns<C>(config: AtlasConfig<C>, runDirs: stri
458
464
  key: string;
459
465
  name: string;
460
466
  kind: import('./plan.js').PlannedPath["kind"];
467
+ progress?: "queued" | "running" | "complete";
461
468
  status: import('./index.js').Status;
462
469
  attempts: {
463
470
  status: "passed" | "failed";
@@ -11,6 +11,11 @@
11
11
  */
12
12
  import { availableParallelism } from 'node:os';
13
13
  import path from 'node:path';
14
+ import { createLiveReporter } from '../../reports/src/live-reporter.js';
15
+ import { httpStore } from '../../reports/src/http-store.js';
16
+ import { publishChecks } from '../../reports/src/publish-checks.js';
17
+ import { publishRunBundle } from '../../reports/src/run-bundle.js';
18
+ import { checksFromManifest } from '../../reports/src/ingest.js';
14
19
  import { lintBundle } from './lint.js';
15
20
  import { chartScope, planChart } from './plan.js';
16
21
  import { assertAllowedTarget, mergePrivacy } from './privacy.js';
@@ -129,20 +134,49 @@ export async function runConfig(config, options = {}, baseDirectory = process.cw
129
134
  const reused = reuseDir ? readOutcomes(reuseDir) : null;
130
135
  if (options.reuse && !reused)
131
136
  throw new Error(`There is no earlier run to reuse${reuseDir ? ` at ${reuseDir}` : ''}`);
132
- return runPaths({
133
- ...contextOf(config, prepared),
134
- paths,
135
- driver,
136
- mode: options.mode ?? 'fast',
137
- outputRoot,
138
- ...(options.retry === undefined ? {} : { retry: options.retry }),
139
- ...(config.stepTimeoutMs === undefined ? {} : { stepTimeoutMs: config.stepTimeoutMs }),
140
- eventSources: config.eventSources ? await config.eventSources() : [],
141
- workers: options.workers ?? config.workers ?? availableParallelism(),
142
- ...(options.shard ? { shard: options.shard } : {}),
143
- ...(reused && reuseDir ? { reuse: { runDir: reuseDir, outcomes: reused.outcomes } } : {}),
144
- ...(options.log ? { log: options.log } : {})
145
- });
137
+ const requestedReporting = options.reporting ?? config.reporting ?? (process.env.ATLAS_SERVER_URL ? { url: process.env.ATLAS_SERVER_URL, token: process.env.ATLAS_PUBLISH_TOKEN ?? process.env.ATLAS_TOKEN } : undefined);
138
+ const reporting = requestedReporting ? { ...requestedReporting, project: requestedReporting.project ?? process.env.ATLAS_PROJECT, token: requestedReporting.token ?? process.env.ATLAS_PUBLISH_TOKEN ?? process.env.ATLAS_TOKEN } : undefined;
139
+ let live;
140
+ const onProgress = reporting ? (manifest) => {
141
+ if (!manifest.privacy.passed)
142
+ return;
143
+ live ??= createLiveReporter({ producer: process.env.ATLAS_PRODUCER ?? 'atlas-statecharts', title: `${prepared.scope.chart} · ${options.mode ?? 'fast'}`,
144
+ plannedChecks: paths.length, environment: process.env.ATLAS_ENVIRONMENT ?? 'desktop-web', branch: manifest.run.branch ?? undefined, sha: manifest.run.commit ?? undefined }, reporting);
145
+ const ingested = checksFromManifest(manifest);
146
+ ingested.checks.forEach((check, index) => {
147
+ if (manifest.paths[index]?.progress === 'queued' && !manifest.run.finishedAt)
148
+ return;
149
+ live.check({ ...check, steps: check.steps?.slice(-20).map(step => ({ name: step.name, status: step.status })) });
150
+ });
151
+ } : undefined;
152
+ try {
153
+ const outcome = await runPaths({
154
+ ...contextOf(config, prepared),
155
+ paths,
156
+ driver,
157
+ mode: options.mode ?? 'fast',
158
+ outputRoot,
159
+ ...(options.retry === undefined ? {} : { retry: options.retry }),
160
+ ...(config.stepTimeoutMs === undefined ? {} : { stepTimeoutMs: config.stepTimeoutMs }),
161
+ eventSources: config.eventSources ? await config.eventSources() : [],
162
+ workers: options.workers ?? config.workers ?? availableParallelism(),
163
+ ...(options.shard ? { shard: options.shard } : {}),
164
+ ...(reused && reuseDir ? { reuse: { runDir: reuseDir, outcomes: reused.outcomes } } : {}),
165
+ ...(options.log ? { log: options.log } : {}), ...(onProgress ? { onProgress } : {})
166
+ });
167
+ await live?.finish(outcome.manifest.privacy.passed ? 'completed' : 'interrupted', outcome.manifest.privacy.passed ? undefined : 'Run failed its privacy scan; unsafe progress was withheld.');
168
+ if (reporting && outcome.manifest.privacy.passed) {
169
+ const store = httpStore({ url: reporting.url, project: reporting.project, token: reporting.token });
170
+ await publishChecks(store, { producer: process.env.ATLAS_PRODUCER ?? 'atlas-statecharts', environment: process.env.ATLAS_ENVIRONMENT ?? 'desktop-web',
171
+ branch: outcome.manifest.run.branch, sha: outcome.manifest.run.commit, recordedAtMs: Math.max(Date.now(), (live?.finishedAtMs ?? 0) + 1), checks: checksFromManifest(outcome.manifest).checks.map(check => ({ ...check, measurements: [...check.measurements] })) });
172
+ await publishRunBundle(store, outcome.runDir);
173
+ }
174
+ return outcome;
175
+ }
176
+ catch (error) {
177
+ await live?.finish('interrupted', 'The producer exited before completing the run.').catch(() => { });
178
+ throw error;
179
+ }
146
180
  }
147
181
  /**
148
182
  * Composes runs of a config (its shards, or a rerun and the run before it) into one new run
@@ -58,6 +58,8 @@ export type RunOptions<C> = RunContext<C> & {
58
58
  };
59
59
  /** Receives progress lines; writes to stdout when omitted. */
60
60
  log?: (line: string) => void;
61
+ /** Receives privacy-checked manifest snapshots as paths and steps change. */
62
+ onProgress?: (manifest: Manifest) => void;
61
63
  };
62
64
  type StepResult = {
63
65
  transition: string;
@@ -268,6 +270,7 @@ export declare function buildManifest<C>(context: RunContext<C>, paths: PlannedP
268
270
  key: string;
269
271
  name: string;
270
272
  kind: PlannedPath["kind"];
273
+ progress?: "queued" | "running" | "complete";
271
274
  status: Status;
272
275
  attempts: Attempt[];
273
276
  steps: StepResult[];
@@ -447,6 +450,7 @@ export declare function writeRun<C>(runDir: string, context: RunContext<C>, path
447
450
  key: string;
448
451
  name: string;
449
452
  kind: PlannedPath["kind"];
453
+ progress?: "queued" | "running" | "complete";
450
454
  status: Status;
451
455
  attempts: Attempt[];
452
456
  steps: StepResult[];
@@ -638,6 +642,7 @@ export declare function mergeRuns<C>(context: RunContext<C>, paths: PlannedPath[
638
642
  key: string;
639
643
  name: string;
640
644
  kind: PlannedPath["kind"];
645
+ progress?: "queued" | "running" | "complete";
641
646
  status: Status;
642
647
  attempts: Attempt[];
643
648
  steps: StepResult[];
@@ -832,6 +837,7 @@ export declare function runPaths<C>(options: RunOptions<C>): Promise<{
832
837
  key: string;
833
838
  name: string;
834
839
  kind: PlannedPath["kind"];
840
+ progress?: "queued" | "running" | "complete";
835
841
  status: Status;
836
842
  attempts: Attempt[];
837
843
  steps: StepResult[];
@@ -891,7 +891,8 @@ export async function runPaths(options) {
891
891
  }
892
892
  });
893
893
  function publish() {
894
- writeRun(runDir, options, options.paths, new Map([...outcomes, ...activeOutcomes]), runInfo(false));
894
+ const snapshot = writeRun(runDir, options, options.paths, new Map([...outcomes, ...activeOutcomes]), runInfo(false));
895
+ options.onProgress?.(snapshot);
895
896
  }
896
897
  const history = durationHistory(options.outputRoot);
897
898
  const cost = (p) => history[p.key] ?? estimateMs(p);
@@ -899,7 +900,7 @@ export async function runPaths(options) {
899
900
  if (reused.size)
900
901
  log(`Reusing ${reused.size} passed path(s) from ${path.basename(options.reuse.runDir)}`);
901
902
  log(`Running ${queue.length} path(s) on ${workers} worker(s)`);
902
- writeRun(runDir, options, options.paths, outcomes, runInfo(false));
903
+ options.onProgress?.(writeRun(runDir, options, options.paths, outcomes, runInfo(false)));
903
904
  // A driver can throw outside any awaited call (a message for a context that just closed); it
904
905
  // belongs to one attempt, so it is logged and the run goes on instead of the process dying.
905
906
  const stray = (error) => log(` (ignored an error thrown outside any step: ${(error instanceof Error ? error.message : String(error)).split('\n')[0]})`);
@@ -924,5 +925,6 @@ export async function runPaths(options) {
924
925
  await driver.dispose?.();
925
926
  await Promise.all(sources.map((s) => s.close?.()));
926
927
  const manifest = writeRun(runDir, options, options.paths, outcomes, runInfo(true));
928
+ options.onProgress?.(manifest);
927
929
  return { runDir, manifest, ok: manifest.privacy.passed && manifest.paths.every((r) => r.status !== 'failed') };
928
930
  }
@@ -16,7 +16,7 @@ export type Check = {
16
16
  readonly detail: string;
17
17
  readonly fix?: string;
18
18
  };
19
- /** The least an account needs, as an AWS IAM policy. R2, MinIO and the rest have their own policy languages: grant the same actions. */
19
+ /** The least an account needs, as an AWS IAM policy. The combined server role also stores operational leases under server/live/. R2, MinIO and the rest have their own policy languages: grant the same actions. */
20
20
  export declare function iamPolicy(location: {
21
21
  bucket: string;
22
22
  prefix: string;
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { LAYER_INDEX } from './layer-store.js';
12
12
  import { verifyObjectStore } from './object-store-conformance.js';
13
- /** The least an account needs, as an AWS IAM policy. R2, MinIO and the rest have their own policy languages: grant the same actions. */
13
+ /** The least an account needs, as an AWS IAM policy. The combined server role also stores operational leases under server/live/. R2, MinIO and the rest have their own policy languages: grant the same actions. */
14
14
  export function iamPolicy(location, role) {
15
15
  const base = location.prefix ? `${location.prefix}/` : '';
16
16
  const bucketArn = `arn:aws:s3:::${location.bucket}`;
@@ -21,6 +21,8 @@ export function iamPolicy(location, role) {
21
21
  if (role !== 'publisher') {
22
22
  statements.push({ Sid: 'AtlasRead', Effect: 'Allow', Action: 's3:GetObject', Resource: `${bucketArn}/${base}layers/*` });
23
23
  }
24
+ if (role === 'both')
25
+ statements.push({ Sid: 'AtlasLiveRuns', Effect: 'Allow', Action: ['s3:GetObject', 's3:PutObject'], Resource: `${bucketArn}/${base}server/live/*` }, { Sid: 'AtlasProjects', Effect: 'Allow', Action: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject'], Resource: [`${bucketArn}/${base}projects/*`, `${bucketArn}/${base}server/projects/*`] });
24
26
  // Publishers need this too: without it S3 answers 403, not 404, when asked whether a layer exists yet.
25
27
  statements.push({ Sid: 'AtlasList', Effect: 'Allow', Action: 's3:ListBucket', Resource: bucketArn, // IfExists: a listing always carries a prefix and is held to it; a HEAD or GET of one object carries none, and
26
28
  // must still be answered "not found" rather than "forbidden".
@@ -11,6 +11,8 @@
11
11
  import type { ObjectStore, PagedListing } from './object-store.js';
12
12
  /** How to reach a server: its `url`, the `token` to send (never in the URL), and, for tests and special runtimes, `fetch`, the retry count, the per-attempt timeout and `sleep`. */
13
13
  export type HttpStoreOptions = {
14
+ /** Project namespace on a shared Atlas node. Omit for the legacy default project. */
15
+ readonly project?: string;
14
16
  /** The Atlas server, such as `https://atlas.example.com`. A path on it (`/atlas`) is kept. */
15
17
  readonly url: string;
16
18
  /** A publish or admin token to write with, a read token to read with. Never put it in the URL. */
@@ -30,6 +32,8 @@ export declare class AtlasHttpError extends Error {
30
32
  }
31
33
  /** An `ObjectStore` (with paged listing) over an Atlas server, plus what only a server can answer: `ping` (is it there, and what may this token do) and `status` (the gate, worked out by the server). */
32
34
  export type HttpStore = ObjectStore & PagedListing & {
35
+ /** The same authenticated node connection, scoped to another project. */
36
+ forProject(project: string): HttpStore;
33
37
  /** `https://atlas.example.com`, with no secrets. */
34
38
  readonly label: string;
35
39
  readonly url: string;
@@ -9,6 +9,7 @@
9
9
  * @see docs/server.md#the-store-protocol
10
10
  */
11
11
  import { ObjectStoreWithPrefix } from './store-prefix.js';
12
+ import { ProjectSlugSchema } from '../../schema/src/project.js';
12
13
  /** What an Atlas server said no to, with what to do about it. */
13
14
  export class AtlasHttpError extends Error {
14
15
  status;
@@ -40,7 +41,8 @@ function explain(status, base, message) {
40
41
  */
41
42
  export function httpStore(options) {
42
43
  const base = options.url.replace(/\/+$/, '');
43
- const root = `${base}/api/store`;
44
+ const api = options.project ? `${base}/api/projects/${ProjectSlugSchema.parse(options.project)}` : `${base}/api`;
45
+ const root = `${api}/store`;
44
46
  const doFetch = options.fetch ?? fetch;
45
47
  const retries = options.retries ?? 4;
46
48
  const timeoutMs = options.timeoutMs ?? 30_000;
@@ -66,7 +68,7 @@ export function httpStore(options) {
66
68
  await response.body?.cancel();
67
69
  return null;
68
70
  }
69
- const said = await response.json().then((body) => body.error ?? null, () => null);
71
+ const said = await response.json().then((body) => body && typeof body === 'object' && 'error' in body && typeof body.error === 'string' ? body.error : null, () => null);
70
72
  failure = new AtlasHttpError(explain(response.status, base, said), response.status, response.status >= 500 || response.status === 429 || response.status === 408);
71
73
  }
72
74
  catch (error) {
@@ -78,6 +80,7 @@ export function httpStore(options) {
78
80
  }
79
81
  }
80
82
  const store = {
83
+ forProject: project => httpStore({ ...options, project }),
81
84
  label: base,
82
85
  url: base,
83
86
  async get(key) {
@@ -127,7 +130,7 @@ export function httpStore(options) {
127
130
  },
128
131
  async status(query = new URLSearchParams()) {
129
132
  const suffix = query.size ? `?${query}` : '';
130
- return (await request('GET', `${base}/api/status${suffix}`)).json();
133
+ return (await request('GET', `${api}/status${suffix}`)).json();
131
134
  },
132
135
  async ping() {
133
136
  const started = performance.now();