@crvouga/atlas 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +30 -11
  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 +6 -1
  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/report-cli.d.ts +1 -1
  17. package/dist/reports/src/report-cli.js +3 -2
  18. package/dist/reports/src/store-url.js +1 -1
  19. package/dist/runners/src/codecept.d.ts +14 -0
  20. package/dist/runners/src/codecept.js +16 -0
  21. package/dist/runners/src/jest.d.ts +50 -0
  22. package/dist/runners/src/jest.js +52 -0
  23. package/dist/runners/src/mocha.d.ts +40 -0
  24. package/dist/runners/src/mocha.js +39 -0
  25. package/dist/runners/src/node-test.d.ts +28 -0
  26. package/dist/runners/src/node-test.js +44 -0
  27. package/dist/runners/src/publish.d.ts +30 -0
  28. package/dist/runners/src/publish.js +68 -0
  29. package/dist/schema/src/api.d.ts +1 -0
  30. package/dist/schema/src/api.js +1 -1
  31. package/dist/schema/src/index.d.ts +2 -0
  32. package/dist/schema/src/index.js +2 -0
  33. package/dist/schema/src/live-run.d.ts +123 -0
  34. package/dist/schema/src/live-run.js +30 -0
  35. package/dist/schema/src/manifest.d.ts +2 -2
  36. package/dist/schema/src/project.d.ts +20 -0
  37. package/dist/schema/src/project.js +10 -0
  38. package/dist/schema/src/report-sources.d.ts +1 -0
  39. package/dist/schema/src/report-sources.js +2 -1
  40. package/dist/schema/src/runs-index.d.ts +1 -1
  41. package/dist/schema/src/server-config.d.ts +84 -0
  42. package/dist/schema/src/server-config.js +12 -0
  43. package/dist/server/src/app.d.ts +10 -0
  44. package/dist/server/src/app.js +148 -10
  45. package/dist/server/src/config.d.ts +1 -1
  46. package/dist/server/src/config.js +20 -3
  47. package/dist/server/src/dev.d.ts +3 -3
  48. package/dist/server/src/dev.js +22 -13
  49. package/dist/server/src/events.d.ts +3 -3
  50. package/dist/server/src/events.js +1 -1
  51. package/dist/server/src/index.d.ts +2 -0
  52. package/dist/server/src/index.js +2 -0
  53. package/dist/server/src/live-runs.d.ts +52 -0
  54. package/dist/server/src/live-runs.js +229 -0
  55. package/dist/server/src/main.d.ts +1 -1
  56. package/dist/server/src/main.js +7 -5
  57. package/dist/server/src/openapi.d.ts +2 -5
  58. package/dist/server/src/openapi.js +71 -7
  59. package/dist/server/src/projects.d.ts +3 -0
  60. package/dist/server/src/projects.js +154 -0
  61. package/dist/server/src/settings-page.d.ts +9 -0
  62. package/dist/server/src/settings-page.js +93 -0
  63. package/dist/server/src/settings.d.ts +23 -0
  64. package/dist/server/src/settings.js +72 -0
  65. package/dist/server/src/site.d.ts +8 -6
  66. package/dist/server/src/site.js +45 -10
  67. package/dist/server/src/store-api.js +43 -22
  68. package/docs/deploying.md +19 -19
  69. package/docs/dogfooding.md +2 -2
  70. package/docs/extending.md +3 -3
  71. package/docs/integrating.md +9 -9
  72. package/docs/reference/cli.md +9 -7
  73. package/docs/reference/core.md +6 -0
  74. package/docs/reference/http-api.md +498 -2
  75. package/docs/reference/reports.md +55 -1
  76. package/docs/reference/runners.md +169 -0
  77. package/docs/reference/schema.md +111 -0
  78. package/docs/reference/server.md +154 -14
  79. package/docs/report-backends.md +8 -8
  80. package/docs/report-contract.md +7 -7
  81. package/docs/reporting-issues.md +3 -3
  82. package/docs/runners.md +69 -0
  83. package/docs/server.md +61 -25
  84. package/docs/statecharts.md +22 -20
  85. package/llms-full.txt +713 -105
  86. package/llms.txt +3 -1
  87. package/package.json +31 -9
  88. package/packages/runners/README.md +6 -0
  89. package/packages/runners/compat/codecept.cjs +2 -0
  90. package/packages/runners/compat/jest.cjs +2 -0
  91. package/packages/runners/compat/mocha.cjs +2 -0
  92. package/packages/schema/schemas/atlas-change.schema.json +4 -0
  93. package/packages/schema/schemas/hello-event.schema.json +4 -0
  94. package/packages/schema/schemas/live-run-start.schema.json +42 -0
  95. package/packages/schema/schemas/live-run-update.schema.json +93 -0
  96. package/packages/schema/schemas/live-run.schema.json +225 -0
  97. package/packages/schema/schemas/project-slug.schema.json +8 -0
  98. package/packages/schema/schemas/project.schema.json +22 -0
  99. package/packages/schema/schemas/projects.schema.json +33 -0
  100. package/packages/schema/schemas/server-config.schema.json +14 -0
  101. package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
  102. package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
  103. package/packages/server/openapi.json +5101 -779
  104. package/packages/visualizer/README.md +13 -13
  105. package/packages/visualizer/bin/atlas-client.mjs +176 -0
  106. package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
  107. package/packages/visualizer/src/app/AppShell.tsx +26 -17
  108. package/packages/visualizer/src/app/report-sources.tsx +2 -1
  109. package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
  110. package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
  111. package/packages/visualizer/src/components/Header.module.css +101 -227
  112. package/packages/visualizer/src/components/Header.tsx +79 -41
  113. package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
  114. package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
  115. package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
  116. package/packages/visualizer/src/data/queries.ts +14 -7
  117. package/packages/visualizer/src/evidence/api.ts +2 -1
  118. package/packages/visualizer/src/evidence/check-detail.tsx +19 -4
  119. package/packages/visualizer/src/evidence/document-page.tsx +6 -2
  120. package/packages/visualizer/src/evidence/evidence-context.tsx +17 -12
  121. package/packages/visualizer/src/evidence/evidence.module.css +11 -1
  122. package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
  123. package/packages/visualizer/src/evidence/model-page.tsx +6 -2
  124. package/packages/visualizer/src/evidence/project-home.tsx +45 -0
  125. package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
  126. package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
  127. package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
  128. package/packages/visualizer/src/live/live-query.ts +21 -0
  129. package/packages/visualizer/src/live/live-runs.tsx +69 -0
  130. package/packages/visualizer/src/live/live-stream.ts +30 -9
  131. package/packages/visualizer/src/live/report-progress.ts +62 -0
  132. package/packages/visualizer/src/live/test-progress.tsx +34 -0
  133. package/packages/visualizer/src/runtime.ts +17 -1
  134. package/packages/visualizer/src/state/ui-store.ts +3 -2
  135. package/packages/visualizer/src/views/ChartPage.tsx +2 -0
  136. package/packages/visualizer/src/views/ChartTests.tsx +43 -0
  137. package/packages/visualizer/src/views/SidePanel.tsx +2 -0
  138. package/packages/visualizer/vite/atlas-files.test.ts +4 -4
  139. package/packages/visualizer/vite/atlas-files.ts +1 -5
  140. package/packages/visualizer/vite/atlas-server-proxy.ts +2 -1
package/llms-full.txt CHANGED
@@ -177,7 +177,7 @@ integrate. To run it for a whole team, with a bucket and one node everyone looks
177
177
  ```
178
178
  node 1: suite A, ios ──▶ layer ─┐
179
179
  node 2: suite A, android ──▶ layer ─┤ ┌──▶ atlas report status (gate: exit 1 on failure)
180
- node 3: suite B, shard 1 ──▶ layer ─┼──▶ backends ──▶ combine ──┼──▶ the viewer (browse it)
180
+ node 3: suite B, shard 1 ──▶ layer ─┼──▶ backends ──▶ combine ──┼──▶ the client (browse it)
181
181
  node 4: suite B, shard 2 ──▶ layer ─┤ └──▶ your own code (combineBackends)
182
182
  a laptop, one scenario ──▶ layer ─┘
183
183
  ```
@@ -543,7 +543,7 @@ unknown is not a pass.
543
543
 
544
544
  ## 6. Browse it
545
545
 
546
- The visualizer lays your stores over your specs directory. Add a module to your project:
546
+ The Atlas client lays your stores over your specs directory. Add a module to your project:
547
547
 
548
548
  ```ts
549
549
  // atlas.reports.ts
@@ -553,14 +553,14 @@ export const reportsDevFetch = reportsHost({ specs: './specs', stores: ['./atlas
553
553
  ```
554
554
 
555
555
  ```sh
556
- ATLAS_REPORTS_MODULE="$(pwd)/atlas.reports.ts" bunx atlas-visualizer dev --specs ./specs
556
+ ATLAS_REPORTS_MODULE="$(pwd)/atlas.reports.ts" bunx atlas client dev --specs ./specs
557
557
  ```
558
558
 
559
559
  `reportsHost(options)` returns `(request: Request, repoRoot: string) => Promise<Response>`, the
560
- function the viewer calls for every `/api/reports/…` request. `specs` and directory `stores` resolve
561
- against `repoRoot`, which the viewer sets to where you ran the command (`INIT_CWD`), not against the
560
+ function the client calls for every `/api/reports/…` request. `specs` and directory `stores` resolve
561
+ against `repoRoot`, which the client sets to where you ran the command (`INIT_CWD`), not against the
562
562
  module. A wrong path comes back as a JSON error that names it. A `stores` entry that is not a string is
563
- a backend of your own: `{ id, label?, store }`. You can call it yourself to look at what the viewer
563
+ a backend of your own: `{ id, label?, store }`. You can call it yourself to look at what the client
564
564
  sees:
565
565
 
566
566
  ```ts
@@ -573,11 +573,11 @@ const page = await (await reportsDevFetch(new Request('http://x/api/reports/evid
573
573
 
574
574
  | Route (after `/api/reports`) | Returns |
575
575
  | --- | --- |
576
- | `/evidence?cursor=` | A page of layer headers, the backends and any notices. The viewer combines them with the specs. |
576
+ | `/evidence?cursor=` | A page of layer headers, the backends and any notices. The client combines them with the specs. |
577
577
  | `/evidence/rows/:backend/:id` | One layer's checks. |
578
578
  | `/outline`, `/outline/spec?path=` | The specs directory as an outline, and one spec's text. |
579
579
 
580
- The combined result of each spec is computed in the viewer. To get it without a browser, use
580
+ The combined result of each spec is computed in the client. To get it without a browser, use
581
581
  `atlas report status --format json`: its `specs` array has each spec's counts per environment.
582
582
 
583
583
  Specs are the files of the specs directory (`.feature` as Gherkin, `.qnt` as Quint, `.dmn`, `.als`,
@@ -618,7 +618,7 @@ An integration is complete when each of these holds. Run them and keep the outpu
618
618
  | `X is unavailable` | A backend is down. The rest were combined; fix it or leave it out. |
619
619
  | `check id "…" appears twice` | Your suite emits two checks with one name. Make the names unique. Importers number repeats as `#2`. |
620
620
  | `Potentially outdated: spec changed` | The spec changed since the layer was recorded. Advisory: the recorded outcome still counts. Re-run to refresh. |
621
- | Viewer shows `No report store is configured` | `ATLAS_REPORTS_MODULE` is unset, or `stores` is empty. |
621
+ | Client shows `No report store is configured` | `ATLAS_REPORTS_MODULE` is unset, or `stores` is empty. |
622
622
 
623
623
  ## Rules
624
624
 
@@ -634,15 +634,87 @@ An integration is complete when each of these holds. Run them and keep the outpu
634
634
  [stable schema](../packages/schema/schemas/report-status.schema.json).
635
635
  </doc>
636
636
 
637
+ <doc path="docs/runners.md">
638
+ # Native test runner reporters
639
+
640
+ Projects upload results to a shared Atlas node under their own project slug. CodeceptJS, Mocha, Jest and Node's test runner have native reporters; other runners can publish their JUnit or CTRF output with `atlas report publish`. All reporters are entry points of the single `@crvouga/atlas` package.
641
+
642
+ ## Configure the project once
643
+
644
+ Set these in the test process, locally or in CI:
645
+
646
+ ```sh
647
+ export ATLAS_SERVER_URL=https://atlas.example.com
648
+ export ATLAS_PROJECT=checkout-service
649
+ export ATLAS_PUBLISH_TOKEN=your-publish-token
650
+ export ATLAS_ENVIRONMENT=linux-chromium
651
+ export ATLAS_BRANCH=main
652
+ export ATLAS_SHA=your-commit-sha
653
+ ```
654
+
655
+ The server owns its storage configuration and credentials. Consumers only need the server URL and a publish token. Upload failures fail the test invocation; reporters wait for publication before finishing. Each invocation streams a live run, then publishes final evidence, with stable check IDs relative to the project directory, durations, failures and skipped outcomes. Unknown outcomes, TODOs and skipped tests are `not-run`, never passed. Parallel processes upload independent layers; use distinct producer/environment values for independent suites.
656
+
657
+ A test file is part of a check's identity, not automatically a specification: link checks to product specifications through the existing report mapping workflow when importing JUnit/CTRF, or publish explicit `specPath` checks through the API.
658
+
659
+ ## CodeceptJS
660
+
661
+ Use its [Mocha reporter interface](https://codecept.io/reports/):
662
+
663
+ ```sh
664
+ bunx codeceptjs run --reporter @crvouga/atlas/codecept
665
+ ```
666
+
667
+ Streams scenario starts and outcomes, then records durations and scenario steps when supplied by CodeceptJS. Use the standard single-process runner; parallel workers should each publish their own layer. Runner-native screenshot/video attachments can also be imported from CTRF/JUnit; this reporter does not read arbitrary files from runner objects.
668
+
669
+ ## Mocha
670
+
671
+ ```sh
672
+ bunx mocha --reporter @crvouga/atlas/mocha
673
+ ```
674
+
675
+ Or configure `reporter` and `reporterOptions` in Mocha. Options include `url`, `token`, `producer`, `environment`, `branch`, `sha`, `rootDir` and `timeoutMs` (30 seconds by default); explicit options override the environment. Final failures and hook failures are failed checks. Passing retries include a flaky label. Mocha parallel mode is not supported by this reporter's event model; run each shard separately.
676
+
677
+ ## Jest
678
+
679
+ Keep Jest's console reporter alongside Atlas:
680
+
681
+ ```js
682
+ export default {
683
+ reporters: ['default', ['@crvouga/atlas/jest', { producer: 'unit' }]],
684
+ };
685
+ ```
686
+
687
+ The reporter streams test-case starts and results from workers and uploads full aggregated evidence after workers finish. File-level exceptions become failed checks even when no test ran. Watch mode resets publication errors on each invocation. It accepts the same options as Mocha.
688
+
689
+ ## Node's test runner
690
+
691
+ ```sh
692
+ node --test --test-reporter=spec --test-reporter-destination=stdout \
693
+ --test-reporter=@crvouga/atlas/node-test --test-reporter-destination=stdout
694
+ ```
695
+
696
+ Uses the native async event stream, with environment configuration. Reports leaf tests; successful suite containers are excluded, while suite hook failures remain failed checks. Test names include enclosing suites so identical names in different suites keep distinct IDs. Skips and TODOs remain not-run, including TODO failures.
697
+
698
+ ## Other runners and frameworks
699
+
700
+ Playwright, Vitest, Bun, WebDriver/Appium and Detox already have Atlas statechart adapters. Pytest, JUnit/TestNG, NUnit, xUnit, Go, Rust, Cypress, WebdriverIO, Cucumber and Robot Framework can publish JUnit XML or CTRF JSON through the existing importer. See [integration commands](integrating.md) and the [report contract](report-contract.md). These report importers complement native reporters; they do not require a storage backend in the consumer project.
701
+
702
+ ## Operate the singleton
703
+
704
+ Run `atlas server` once for the projects the node hosts; point local and CI publishers at that URL and set each project's `ATLAS_PROJECT`. Configure backends using environment variables or open **Settings** in the client (`/admin`). Authentication and listening addresses remain deployment configuration. Settings changes are validated and saved atomically for the next server restart. Environment variables and flags override saved values. A backend change does not migrate historical data: retain the old backend as a read source when switching the upload destination.
705
+
706
+ Native reporters send a heartbeat every 5 seconds. The server interrupts invocations after 30 seconds of silence, or after the configurable progress-stall timeout. It preserves finished results and never marks missing tests as passed. See [live test runs](server.md#live-test-runs-and-heartbeats).
707
+ </doc>
708
+
637
709
  <doc path="docs/server.md">
638
710
  # The Atlas server
639
711
 
640
- One process per project that **takes in results from every environment, keeps them, and shows everyone the
641
- combined report, live**. It is a Bun API that serves a Vite-built single-page viewer: one host, one port, in
712
+ One node for multiple projects that **takes in results from every environment, keeps them, and shows everyone the
713
+ combined report, live**. It is a Bun API that serves a Vite-built single-page client: one host, one port, in
642
714
  development and in production.
643
715
 
644
716
  ```
645
- laptop ──────┐ ┌── browsers (the viewer: reads /api, listens on /api/events)
717
+ laptop ──────┐ ┌── browsers (the client: reads /api, listens on /api/events)
646
718
  CI job ──────┼── PUT /api/store/… POST /api/checks ─▶│
647
719
  device lab ──┘ atlas report publish | stream │ atlas report status (the gate: asks /api/status)
648
720
  │
@@ -655,8 +727,8 @@ development and in production.
655
727
  - **Its default backend is its own filesystem** (`./atlas-data`). That folder is the whole state: back it up,
656
728
  mount it on a volume, or put the state in a bucket instead ([backends](#backends)). Several backends are one
657
729
  report, as ever.
658
- - **The report is live.** Every viewer holds one server-sent-events connection; when results arrive the server
659
- says so and each viewer reads only what changed ([streaming](#streaming)).
730
+ - **The report is live.** Every client holds one server-sent-events connection; when results arrive the server
731
+ says so and each client reads only what changed ([streaming](#streaming)).
660
732
  - **Layered, never partitioned**, as everywhere in Atlas: the server only stores immutable, content-addressed
661
733
  layers and works out the report from them. Replace it, restore it, or run a second one on a copy of the data:
662
734
  the same layers give the same report ([the contract](report-contract.md)).
@@ -673,13 +745,13 @@ export ATLAS_TOKEN=$(openssl rand -hex 24) # admin token; see "Tokens and role
673
745
  bunx atlas serve # http://127.0.0.1:8080, data in ./atlas-data
674
746
  ```
675
747
 
676
- It runs on Bun and on Node 22.18 or later. The first start builds the viewer once into `~/.cache/atlas/site`
748
+ It runs on Bun and on Node 22.18 or later. The first start builds the client once into `~/.cache/atlas/site`
677
749
  (or serves `./atlas-site`, or `--site <dir>`); the build is the same for every project, because the server writes
678
750
  its own address book into the page (`window.__ATLAS_CONFIG__`).
679
751
 
680
- **Working on Atlas or on the viewer:** `atlas dev` runs the server on a port of its own and the viewer's Vite dev
752
+ **Working on Atlas or on the client:** `atlas dev` runs the server on a port of its own and the client's Vite dev
681
753
  server in front of it. Vite proxies `/api` (server-sent events included) to the server, so the browser, the
682
- uploaders and the viewer all use **one host**, exactly as in production, and a change to the viewer shows at once.
754
+ uploaders and the client all use **one host**, exactly as in production, and a change to the client shows at once.
683
755
 
684
756
  ```sh
685
757
  bunx atlas dev --data .atlas-data # http://127.0.0.1:5180
@@ -727,7 +799,7 @@ purposes.
727
799
 
728
800
  A backend is anything that implements `ObjectStore`: a folder, an S3-compatible bucket (`s3://`, `r2://`,
729
801
  `gs://`), or **another Atlas server** (`https://…`, with `ATLAS_TOKEN` for it). The server reads every backend
730
- and shows one report; one that is down is named in the viewer and skipped, and the server recovers by itself.
802
+ and shows one report; one that is down is named in the client and skipped, and the server recovers by itself.
731
803
 
732
804
  - **Uploads go to one backend**: the `data` folder, or the one named by `write` / `ATLAS_WRITE`. Name a bucket
733
805
  to make it the system of record, so the server holds nothing a replacement would lose:
@@ -744,7 +816,7 @@ and shows one report; one that is down is named in the viewer and skipped, and t
744
816
 
745
817
  | Role | May |
746
818
  | --- | --- |
747
- | `read` | look: the viewer, the report, `/api/status` |
819
+ | `read` | look: the client, the report, `/api/status` |
748
820
  | `publish` | also upload layers, specs and runs, and remove the scratch objects `atlas report doctor` writes |
749
821
  | `admin` | all server operations |
750
822
 
@@ -828,24 +900,26 @@ there (`409`), so nothing half-uploaded is ever shown.
828
900
 
829
901
  | Event | Data | Meaning |
830
902
  | --- | --- | --- |
831
- | `hello` | `{ version, ready, time }` | Sent first. The report's current version. |
903
+ | `hello` | `{ version, ready, time, heartbeatMs? }` | Sent first. The report's current version. |
832
904
  | `report` | `{ version, totalLayers, added: [{ id, producer, environment, branch, sha, recordedAtMs, counts }], addedTotal }` | Layers arrived. Enough to say who sent what, without asking. |
833
905
  | `atlas:change` | `{ scope: "specs" \| "runs", files, runIds }` | New specs or runs. |
834
- | `ping` | `{}` | Every 15 s, so proxies keep the connection and the viewer can tell a quiet server from a dead connection. |
906
+ | `run` | A bounded `LiveRun` snapshot | Start, progress logs, heartbeat, completion, and interruption; no producer credentials. |
907
+ | `projects` | `{}` | Another project was discovered; refresh the project picker. |
908
+ | `ping` | `{}` | Every 15 s, so proxies keep the connection and the client can tell a quiet server from a dead connection. |
835
909
 
836
- Events say *that* something changed; the state is always fetched. A viewer that missed some (a dropped
837
- connection, a laptop that slept) loses nothing: it reads again when it reconnects, and the viewer also reads on a
910
+ Run events update progress directly in the client cache. Report and specification events trigger coalesced reads of the affected data. Events arriving during a read cause a trailing refresh rather than cancelling that read. A client that missed some (a dropped
911
+ connection, a laptop that slept) loses nothing: it reads again when it reconnects, and the client also reads on a
838
912
  slow timer behind the stream. Past `limits.maxStreams` viewers the server answers `503` with `Retry-After` and the
839
- viewer polls instead; a viewer that stops reading is let go rather than held in memory.
913
+ client polls instead; a client that stops reading is let go rather than held in memory.
840
914
 
841
915
  ### Reading only what changed
842
916
 
843
917
  `GET /api/reports/evidence/effective` is the whole report: only the layers that win a check, each with only the
844
- rows that win, with an ETag. A viewer that holds a version asks `?since=<version>` and is sent **only the layers
918
+ rows that win, with an ETag. A client that holds a version asks `?since=<version>` and is sent **only the layers
845
919
  that changed since, and the ids that no longer win anything** (`"delta": true`). Applying that gives exactly the
846
920
  report the server would have sent whole (`applyEffectiveDelta`, tested against every earlier version of random
847
- runs); a version the server does not remember, or an older server, answers with the whole report, which a viewer
848
- takes as it always did. A result streamed into a 100 000-check report costs a viewer a few kilobytes.
921
+ runs); a version the server does not remember, or an older server, answers with the whole report, which a client
922
+ takes as it always did. A result streamed into a 100 000-check report costs a client a few kilobytes.
849
923
 
850
924
  ## API reference
851
925
 
@@ -887,7 +961,7 @@ laptop with 2 000 layers of 100 checks (200 000 results, 10 000 slots) and with
887
961
  | --- | --- | --- |
888
962
  | Start: read and fold every layer | 0.4 s | 1.7 s |
889
963
  | A refresh with nothing new | 4 ms | 4 ms |
890
- | Upload one layer → every viewer told | 4 ms | 5 ms |
964
+ | Upload one layer → every client told | 4 ms | 5 ms |
891
965
  | `GET effective`, cached / `304` | 1 ms / 0.2 ms | 13 ms / 0.5 ms |
892
966
  | `GET status`, cached for the version | 12 ms first | 70 ms first |
893
967
 
@@ -901,25 +975,59 @@ What makes it so, none of it at the cost of the contract:
901
975
  - **An upload is read once.** A layer's manifest is parsed and verified when it arrives and handed to the report,
902
976
  not read again; blob existence is asked 16 at a time.
903
977
  - **The newest end of the catalog is all a refresh looks at**, with a full look now and then.
904
- - **Compressed once.** The viewer's files are brotli-compressed once (gzip for clients that cannot read it), a
905
- report version is gzipped once however many viewers fetch it, and events are encoded once for every viewer.
906
- - **A small viewer.** The graph, the Gherkin parser, the recording player and the finder load when used: the
978
+ - **Compressed once.** The client's files are brotli-compressed once (gzip for clients that cannot read it), a
979
+ report version is gzipped once however many viewers fetch it, and events are encoded once for every client.
980
+ - **A small client.** The graph, the Gherkin parser, the recording player and the finder load when used: the
907
981
  first page is about 250 KB over the wire.
908
982
 
909
983
  ## Operating it
910
984
 
911
- - **One server per project.** It is a singleton by design: local environments and CI all talk to it, and it tells
912
- every viewer at once. Two servers on one backend both work and show the same report, but a viewer hears about
985
+ - **One node for multiple projects.** Local environments and CI select their project slug and talk to the same node, which tells
986
+ every client at once. Two servers on one backend both work and show the same report, but a client hears about
913
987
  an upload only from the server that took it and about the other's at the next refresh, so give a team one.
914
988
  - **State is the write backend.** Back up `./atlas-data` (or the bucket). Restoring it, or starting a new server
915
989
  on it, shows the same report.
916
990
  - **Stopping.** `SIGTERM` stops accepting, lets requests in flight finish, ends the event streams and exits 0.
917
991
  - **Logs.** One JSON line per request (method, path, status, ms) to stdout, none for the probes. The first lines
918
992
  say what it reads and which backend takes uploads, and warn when there are no tokens.
919
- - **A backend goes down.** The server keeps serving what it has, names the backend in the viewer and in
993
+ - **A backend goes down.** The server keeps serving what it has, names the backend in the client and in
920
994
  `/readyz`, and recovers by itself. The gate fails closed: unknown is not a pass.
921
995
  - **Upgrades.** The layer format is a stable, versioned contract; upgrade the server before the clients that
922
996
  publish when a release says the format moved.
997
+
998
+ ## Configure from the client
999
+
1000
+ Open **Settings** (`/admin`) and enter an admin token. The token stays in page memory. Read and publish tokens cannot edit settings; the browser sign-in box only grants reading. The API uses `Authorization: Bearer` and refuses cross-origin browser writes.
1001
+
1002
+ Use the Storage, Live updates, and Report rules forms to edit saved settings. Advanced includes request limits, logging, a JSON editor, and a read-only view of the active configuration. Save changes and restart Atlas to apply them. Atlas validates the configuration and atomically replaces `atlas.server.json`, preserving authentication and deployment settings. Changes apply at the next restart; environment variables and flags retain precedence. Concurrent edits require reloading the current revision. Data is never moved or removed by changing settings. Keep an old backend as a read source when moving future uploads to a new one. Embedded servers can provide their own settings persistence or manage configuration outside this API.
1003
+
1004
+ Native test reporters for CodeceptJS, Mocha, Jest and Node upload directly to this server; see [runner setup](runners.md). Every publisher uses the same server address and only a publish token. Storage credentials belong in the server environment.
1005
+
1006
+ ## Live test runs and heartbeats
1007
+
1008
+ The default native reporters and statechart runner stream tests and steps through `/api/live`, before the invocation finishes. Running tests appear inside their report with an animated status mark and the latest step. Open a test to read its progress log; its previous recorded result stays available separately. The **Running** filter narrows both the spec list and the test rows, and counts update as tests finish or time out. Statechart tests appear inside the chart and screen details. Selecting an archived run leaves live progress out of that historical view. Finished checks immediately enter normal immutable report layers. Operational progress is separate from historical winners, so a previous passed result never implies that the current invocation passed.
1009
+
1010
+ Start with `POST /api/live/<uuid>`, send ordered check deltas with `PATCH /api/live/<uuid>`, and renew the producer lease with `POST /api/live/<uuid>/heartbeat` every 5 seconds. Finish with a terminal update. Only the publisher token that registered the invocation can update it. Replayed sequences must have identical content; late writes cannot revive an expired invocation. `createLiveReporter` handles batching, retries, heartbeat timers and the final flush.
1011
+
1012
+ The server uses its own receipt clock. By default it interrupts a run after 30 seconds without a heartbeat (`ATLAS_RUN_TIMEOUT_SECONDS`). Heartbeats do not reset progress: after 15 minutes with no changed tests/steps, a separate stall timeout interrupts it (`ATLAS_RUN_STALL_SECONDS`, 0 disables). Both settings are editable in the admin UI. Healthy long tests continue renewing heartbeats; increase or disable the progress timeout for tests that legitimately make no observable progress for longer.
1013
+
1014
+ Interrupted runs retain completed results and show why the remaining work stopped. Missing outcomes never become passes. The server persists bounded summaries under `server/live/` in its write backend (an HTTP relay keeps operational leases in its own data folder; the remote server only accepts immutable report objects) and restores outstanding leases as interrupted after a server restart. S3 server credentials need GetObject/PutObject for that prefix; generate the updated `atlas report policy --role both` policy. Producers still receive no backend credentials. Live `run` events carry bounded snapshots directly into test rows, including step logs and interruption reasons. The legacy `atlas:change` announcement remains available for older clients; its optional `liveRunId` identifies lease updates. Reconnecting viewers re-fetch progress, reports, and specifications. With SSE connected, live progress uses a 60-second safety refresh; without SSE it polls every two seconds. Returning to the tab or coming back online also revalidates reports and progress.
1015
+
1016
+ In this repository, use `bun atlas` and `bun example:run` in two terminals. Every example Gherkin scenario executes, including sharing and renaming; `@unrecorded` no longer excludes tests.
1017
+
1018
+ ## Multiple projects on one node
1019
+
1020
+ Start the node with `atlas server` (or `bun atlas` in this repository). The server builds or finds the Atlas client and serves it with the API from one process. It never imports a consumer or an example. In this repository the node uses port 5180 and `./atlas-data`; `bun example:run` publishes the Todo app to project `todo-app`.
1021
+
1022
+ Set `ATLAS_PROJECT=todo-app` alongside `ATLAS_SERVER_URL` for native reporters and statechart runs. Report CLI commands accept `--project todo-app` (or `ATLAS_PROJECT`) with an HTTP store. Programmatic HTTP stores, live reporters and native reporter options accept `project: 'todo-app'`. Keep the same slug for the invocation's live progress, final results, spec sets and recordings. Slugs use 1–64 lowercase letters, digits or hyphens, starting and ending with a letter or digit.
1023
+
1024
+ Every project API is available as `/api/projects/<slug>/…`, for example `/api/projects/todo-app/checks`, `/api/projects/todo-app/events` and `/api/projects/todo-app/reports/evidence/effective`. The unprefixed API accepts optional `?project=todo-app` or `X-Atlas-Project: todo-app`. Conflicting selectors are refused. `GET /api/projects` discovers projects. A successful authenticated upload creates a project; reads of an unknown project return 404.
1025
+
1026
+ Projects isolate check identities, specs, run IDs, recordings, lease summaries, event streams and report caches. Identical layer bytes are preserved as uploaded. The node stores each project's objects under `projects/<slug>/` and discovery markers under `server/projects/`. Unqualified requests continue reading the legacy default namespace, advertised as **Default** when it has reports. Existing data is never implicitly moved. For S3, use the combined server policy (`atlas report policy --role both`) with access to both project namespaces and the catalog. An HTTP relay selects the same project on its upstream node and keeps discovery and leases locally.
1027
+
1028
+ The project picker sits at the top right; the header also provides spec dropdown breadcrumbs. Project selection lives in the page URL, so links and refreshes preserve it. Switching projects reloads the client, clearing report queries, media and event connections. Custom report-source preferences are keyed by the project's data URL. Project namespaces share the node's authentication roles: they are organizational partitions, not separate tenant authorization policies. Settings and server identity remain node-wide.
1029
+
1030
+ Project indexes are opened lazily; at most 64 are active in memory. An index with no active requests or event subscribers is closed after five idle minutes. Event subscribers share the node-wide stream limit. Project discovery lists only small catalog markers, never every report layer.
923
1031
  </doc>
924
1032
 
925
1033
  <doc path="docs/deploying.md">
@@ -993,7 +1101,7 @@ so run `atlas report doctor` against yours before relying on it.
993
1101
  ```sh
994
1102
  atlas report policy --store s3://my-bucket/atlas/my-project --role publisher # CI and developers
995
1103
  atlas report policy --store s3://my-bucket/atlas/my-project --role reader # a server that only reads it, and the gate
996
- # the server that uploads into it needs --role both
1104
+ # the server that uploads into it needs --role both (also grants server/live/* for live leases)
997
1105
  ```
998
1106
 
999
1107
  A publisher may write and read `layers/*` and list the folder. A reader may read `layers/*` and list.
@@ -1102,7 +1210,7 @@ Everything is immutable and addressed by its content, so each publish is safe to
1102
1210
  in parallel. A set of specs is named by the files and the commit, so publishing the same commit from ten
1103
1211
  nodes writes it once.
1104
1212
 
1105
- For statecharts, publish each run as a layer and upload it for the viewer:
1213
+ For statecharts, publish each run as a layer and upload it for the client:
1106
1214
 
1107
1215
  ```sh
1108
1216
  bunx atlas run --config atlas.config.ts --shard 1/2
@@ -1110,7 +1218,7 @@ bunx atlas report publish --producer statecharts --environment desktop-web \
1110
1218
  --manifest atlas-runs/<run>/manifest.json --specs ./specs --upload-run
1111
1219
  ```
1112
1220
 
1113
- A run is uploaded only when its privacy scan passed: the same rule the static viewer applies.
1221
+ A run is uploaded only when its privacy scan passed: the same rule the static client applies.
1114
1222
 
1115
1223
  ## 4. The server
1116
1224
 
@@ -1118,7 +1226,7 @@ A run is uploaded only when its privacy scan passed: the same rule the static vi
1118
1226
  atlas serve # ATLAS_STORE says where; listens on :8080
1119
1227
  ```
1120
1228
 
1121
- It serves the viewer, the report and the project's specs and runs from all configured backends. It also
1229
+ It serves the client, the report and the project's specs and runs from all configured backends. It also
1122
1230
  accepts uploads and announces new results on its event stream. Uploads go to `--data` by default;
1123
1231
  use a named `--store project=s3://… --write project` to put uploads in a shared bucket, with publisher permissions.
1124
1232
 
@@ -1129,7 +1237,7 @@ use a named `--store project=s3://… --write project` to put uploads in a share
1129
1237
  | `--write <id>` | `ATLAS_WRITE` | Backend that accepts uploads. Default `data`. |
1130
1238
  | `--port <n>` | `ATLAS_PORT` or `PORT` | Default 8080. `0` picks a free one. |
1131
1239
  | `--host <h>` | `ATLAS_HOST` or `HOST` | Default `127.0.0.1`. Listening beyond this machine requires tokens or `--insecure`. |
1132
- | `--site <dir>` | `ATLAS_SITE` | The built viewer. Default: `./atlas-site`, else built once into `~/.cache/atlas/site`. |
1240
+ | `--site <dir>` | `ATLAS_SITE` | The built client. Default: `./atlas-site`, else built once into `~/.cache/atlas/site`. |
1133
1241
  | `--outline-branch <b>` | `ATLAS_OUTLINE_BRANCH` | Which branch's specs the report is laid over. Default `main`, then `master`, then the newest. |
1134
1242
  | `--refresh <seconds>` | `ATLAS_REFRESH_SECONDS` | How often the bucket is looked at for new layers. Default 10. |
1135
1243
  | `--branch <name>` (repeatable) | `ATLAS_ONLY_BRANCH` | Only layers recorded on these branches take part. |
@@ -1139,7 +1247,7 @@ use a named `--store project=s3://… --write project` to put uploads in a share
1139
1247
  ### Run it
1140
1248
 
1141
1249
  **Docker.** [`examples/deploy/Dockerfile`](https://github.com/crvouga/atlas/blob/main/examples/deploy/Dockerfile)
1142
- installs Atlas, builds the viewer once (it is the same for every project: the server tells the page where its
1250
+ installs Atlas, builds the client once (it is the same for every project: the server tells the page where its
1143
1251
  data is) and starts the server, keeping results on the volume at `/data`:
1144
1252
 
1145
1253
  ```sh
@@ -1148,7 +1256,7 @@ docker run -p 8080:8080 -v atlas-data:/data -e ATLAS_TOKEN=$(openssl rand -hex 2
1148
1256
  ```
1149
1257
 
1150
1258
  For a bucket as the system of record add `-e ATLAS_STORE=project=s3://my-bucket/atlas/my-project -e ATLAS_WRITE=project -e AWS_REGION=us-east-1`.
1151
- On ECS, EKS or EC2, attach a role with the **publisher** and **reader** policies to the task, service account or
1259
+ On ECS, EKS or EC2, attach the combined **both** policy (including `server/live/*` for producer leases) to the task, service account or
1152
1260
  instance and set no keys.
1153
1261
 
1154
1262
  **Try it on one machine.** [`examples/deploy/compose.yaml`](https://github.com/crvouga/atlas/blob/main/examples/deploy/compose.yaml)
@@ -1159,7 +1267,7 @@ Publish to `http://localhost:8080` from your project and open it.
1159
1267
  one copy (the server is a singleton for the project), results in the bucket, readiness on `/readyz`, an IRSA
1160
1268
  service account.
1161
1269
 
1162
- **Without a container.** `bun add -d @crvouga/atlas`, then `bunx atlas-visualizer build --no-data --out
1270
+ **Without a container.** `bun add -d @crvouga/atlas`, then `bunx atlas client build --no-data --out
1163
1271
  atlas-site` and `bunx atlas serve`. It runs on Bun and on Node 22.18 or later. Everything it can be told is in
1164
1272
  [the Atlas server](server.md#configuration).
1165
1273
 
@@ -1174,12 +1282,12 @@ layer too. Pass `--insecure` only when that proxy handles sign-in.
1174
1282
 
1175
1283
  | Path | |
1176
1284
  | --- | --- |
1177
- | `/` | The viewer. Its page carries the server's address book, so one build serves every project. |
1285
+ | `/` | The client. Its page carries the server's address book, so one build serves every project. |
1178
1286
  | `/api/reports/evidence/effective` | The whole report in one request: only the layers that win a check, each with only the rows that win. ETag, gzip. |
1179
1287
  | `/api/reports/evidence`, `/evidence/rows/<backend>/<id>`, `/outline`, `/outline/spec?path=` | History, one layer's checks, the specs. |
1180
1288
  | `/api/reports/evidence/detail/<backend>/<id>?check=<id>&kind=<kind>` | Everything one result carries: its timeline, annotations, retries, attachments and extensions, and the run's own. Without `check`, only the run's. |
1181
1289
  | `/api/reports/layers/<id>/blobs/<key>` | One file a layer carries (screenshot, video, trace…), served as untrusted bytes with range support, cached for good. |
1182
- | `/api/data/specs/…`, `/api/data/runs/…`, `/api/data/sources.json` | The specs and runs, laid out the way the viewer reads a static host. |
1290
+ | `/api/data/specs/…`, `/api/data/runs/…`, `/api/data/sources.json` | The specs and runs, laid out the way the client reads a static host. |
1183
1291
  | `/api/checks` | Upload a layer of checks with `POST`. |
1184
1292
  | `/api/events` | Live report updates over server-sent events. |
1185
1293
  | `/healthz` | The process is up. |
@@ -1202,7 +1310,7 @@ Narrow it by choice, per consumer, never by moving data:
1202
1310
  | A pull request's gate that inherits what it did not re-run from `main` | `--branch "$PR_BRANCH" --branch main --ignore-modified` |
1203
1311
  | A business dashboard of what is committed | `atlas serve --ignore-modified` |
1204
1312
  | Everything, as the team lives it | nothing (the default) |
1205
- | One person's view of what leaving a source out changes | **Sources** in the viewer: leave out a branch, environment, version or layer |
1313
+ | One person's view of what leaving a source out changes | **Sources** in the client: leave out a branch, environment, version or layer |
1206
1314
 
1207
1315
  ## 6. Operating it
1208
1316
 
@@ -1212,12 +1320,12 @@ Narrow it by choice, per consumer, never by moving data:
1212
1320
  they never need to move together.
1213
1321
  - **Logs.** One JSON line per request to stdout. `/healthz` and `/readyz` are not logged. At start it
1214
1322
  prints what it reads and any store it cannot reach.
1215
- - **A store goes down.** The server keeps serving what it has, names the store in the viewer's notices and
1323
+ - **A store goes down.** The server keeps serving what it has, names the store in the client's notices and
1216
1324
  in `/readyz`, and recovers by itself. The gate fails closed: unknown is not a pass.
1217
1325
  - **A layer cannot be read** (corrupt, or written by a newer Atlas): a notice names it. It is tried again
1218
1326
  every five minutes.
1219
- - **Scaling.** One server per project is the design, and it is enough: it takes in an upload in milliseconds and a
1220
- report of 100,000 checks costs a viewer kilobytes per change ([performance](server.md#performance)). Memory
1327
+ - **Scaling.** One Atlas node supports multiple project namespaces with independently loaded report indexes. It takes in an upload in milliseconds and a
1328
+ report of 100,000 checks costs a client kilobytes per change ([performance](server.md#performance)). Memory
1221
1329
  grows with the number of distinct checks, plus about a hundred bytes for each layer ever seen (to know it has
1222
1330
  read it). More than one server on a backend works and shows the same report, but each hears of uploads to
1223
1331
  another only at its next refresh.
@@ -1243,13 +1351,13 @@ latency per request, which the server hides by reading 64 at a time):
1243
1351
  | Publish a layer of plain checks | One `HEAD` and two small `PUT`s. Two thousand layers published in under two seconds from one process, in parallel. |
1244
1352
  | Server start | About 0.2 ms of CPU per layer, plus S3 latency ÷ 64 per layer: 2,000 layers in well under a second on a fast store, a few seconds on S3. Each layer is read once, ever. |
1245
1353
  | Refresh with nothing new | One `LIST` request for the newest page of the catalog, whatever the history. |
1246
- | The report, to a viewer | One request. 12,000 checks from 2,000 layers is 108 KB gzipped, built once per change. |
1247
- | The viewer asking again | `304 Not Modified` in under a millisecond. While the live stream is open it asks only when told, and then for only what changed. |
1248
- | The viewer in a browser | One request for the report, plus the spec index, outline and run index and the specs it opens. None per layer, whatever the history (measured in Chromium against a server reading S3). |
1354
+ | The report, to a client | One request. 12,000 checks from 2,000 layers is 108 KB gzipped, built once per change. |
1355
+ | The client asking again | `304 Not Modified` in under a millisecond. While the live stream is open it asks only when told, and then for only what changed. |
1356
+ | The client in a browser | One request for the report, plus the spec index, outline and run index and the specs it opens. None per layer, whatever the history (measured in Chromium against a server reading S3). |
1249
1357
 
1250
1358
  How: each layer is folded into the winning result of every check once and dropped unless it still holds a
1251
1359
  winner, so work and memory follow the number of checks. Because the winner rule is a total order, folding in
1252
- any order is exact; a test proves the incremental report equals recomputing from every layer. The viewer
1360
+ any order is exact; a test proves the incremental report equals recomputing from every layer. The client
1253
1361
  reads that compact report in one request and only falls back to reading every layer when you ask it to
1254
1362
  leave a source out (which needs the history).
1255
1363
 
@@ -1266,7 +1374,7 @@ leave a source out (which needs the history).
1266
1374
  | The server says `listening … no backend can be read yet` | The bucket is unreachable from the server. It retries; `/readyz` stays 503 until it can read. |
1267
1375
  | A new layer does not show | Uploaded through the server it is there at once: check the token (`401`/`403` say so). Written straight to a bucket, wait one refresh interval (10 s). Check `atlas report status --store <url>` sees it. |
1268
1376
  | Everything is red because of one laptop | By design the newest result wins. Gate on `--ignore-modified` and a branch ([above](#5-choosing-what-takes-part)). |
1269
- | Specs are empty in the viewer | Nothing published with `--specs` yet, or only from a branch other than `main`: set `--outline-branch`. |
1377
+ | Specs are empty in the client | Nothing published with `--specs` yet, or only from a branch other than `main`: set `--outline-branch`. |
1270
1378
  | `Run … is not shared: its privacy scan did not pass` | `--upload-run` refuses runs whose scan found real data. Fix what it found and run again. |
1271
1379
  </doc>
1272
1380
 
@@ -1279,7 +1387,7 @@ unit tests, linters, anything that ends in "this check passed or failed". If thi
1279
1387
  disagree, that is a bug: [file it](reporting-issues.md).
1280
1388
 
1281
1389
  The contract is small on purpose. It is one document (the **layer**), one rule (how layers compose),
1282
- and one interface (the **backend** that stores them). Everything else, the viewer, the gate, the CLI,
1390
+ and one interface (the **backend** that stores them). Everything else, the client, the gate, the CLI,
1283
1391
  is built on those three and can be replaced.
1284
1392
 
1285
1393
  - [Concepts](#concepts)
@@ -1562,7 +1670,7 @@ import { verifyObjectStore } from '@crvouga/atlas/reports/object-store-conforman
1562
1670
  const { ok, failures } = await verifyObjectStore(myS3Store); // writes under a scratch prefix, cleans up
1563
1671
  ```
1564
1672
 
1565
- Several backends are one report: pass them all to `combineBackends`, to the viewer
1673
+ Several backends are one report: pass them all to `combineBackends`, to the client
1566
1674
  (`reportsHost`), or to the CLI (`--store` for directories, `--backends <module>` for the rest). A
1567
1675
  backend that cannot be reached is named in the notices and leaves the others usable; asking again
1568
1676
  retries it.
@@ -1575,7 +1683,7 @@ entry whose content has not arrived:
1575
1683
  A layer's checks are the contract. Next to them the store holds the two other things a whole project
1576
1684
  needs, written the same way (content first, catalog entry last, nothing overwritten):
1577
1685
 
1578
- - **Spec sets** (`layers/specs/…`): the specs of one commit, so a viewer needs no checkout. A spec set is
1686
+ - **Spec sets** (`layers/specs/…`): the specs of one commit, so a client needs no checkout. A spec set is
1579
1687
  `{ schemaVersion, id, branch, sha, recordedAtMs, freshness?, files: [{ path, digest, fingerprint, kind,
1580
1688
  title, domain, bytes }] }` ([`spec-set.schema.json`](../packages/schema/schemas/spec-set.schema.json)). Its
1581
1689
  id is the digest of the branch, commit and file digests, so one commit's specs are one set however many
@@ -1583,7 +1691,7 @@ needs, written the same way (content first, catalog entry last, nothing overwrit
1583
1691
  The outline a report is laid over is the **newest set from `main`** (then `master`, then the newest);
1584
1692
  a set from a modified checkout is used only if no committed set exists.
1585
1693
  - **Run bundles** (`layers/runs/<run>/…` and `layers/runs/index/…`): a finished statechart run with its
1586
- screenshots and clips, for the viewer. Uploaded only when the run's privacy scan passed; the manifest is
1694
+ screenshots and clips, for the client. Uploaded only when the run's privacy scan passed; the manifest is
1587
1695
  written last, then the catalog entry.
1588
1696
 
1589
1697
  The keys of a layer, bottom up:
@@ -1649,13 +1757,13 @@ and composed with layers; Atlas never rewrites them. `copyStore` and `atlas repo
1649
1757
  the number of checks (plus a few bytes for each layer ever read), not the age of the project, and a refresh
1650
1758
  with nothing new is one request for the newest page of the catalog. Folding is exact in any order because the winner
1651
1759
  rule is a total order; a test proves the incremental report equals combining every layer from scratch.
1652
- The node sends the viewer only that: one request, only winning rows, with an ETag.
1760
+ The node sends the client only that: one request, only winning rows, with an ETag.
1653
1761
  - Measured on a laptop with a directory backend: 300 layers of 50 checks in about 0.2 s; 1,000 layers
1654
1762
  of 100 checks, each recorded against 1,000 specs (100,000 results), in about 0.9 s. Composing two
1655
1763
  thousand layers of 100 checks (200,000 results) already in memory takes about 0.3 s.
1656
1764
  `performance.test.ts` holds budgets ten times looser, to catch an accidental quadratic step on a
1657
1765
  slow machine.
1658
- - A directory backend lists only the directories a prefix can reach. The viewer polls a small index,
1766
+ - A directory backend lists only the directories a prefix can reach. The client polls a small index,
1659
1767
  reads an immutable layer once and caches it forever, and loads recordings only when opened.
1660
1768
 
1661
1769
  ## Other documents of the contract
@@ -1667,7 +1775,7 @@ and composed with layers; Atlas never rewrites them. `copyStore` and `atlas repo
1667
1775
  | Report status | `report-status.schema.json` | The output of `atlas report status --format json`. |
1668
1776
  | Spec set | `spec-set.schema.json` | The specs of one commit, as published beside the layers. |
1669
1777
  | Run manifest | `manifest.schema.json` | What `atlas run` writes for a statechart run. Publish it as a layer with `--manifest`. |
1670
- | Runs index, sources, change notices | `runs-index`, `report-sources`, `atlas-change` | How the viewer finds runs. See [report backends](report-backends.md). |
1778
+ | Runs index, sources, change notices | `runs-index`, `report-sources`, `atlas-change` | How the client finds runs. See [report backends](report-backends.md). |
1671
1779
  | CloudEvents | `cloud-event.schema.json` | Business events a run observed. |
1672
1780
 
1673
1781
  All of them are JSON Schema 2020-12 and ship in the package at `@crvouga/atlas/schemas/<name>.schema.json`.
@@ -1707,7 +1815,7 @@ carry any of these. Every one is optional.
1707
1815
  | `attempts[]` | Every try, when the check was retried. |
1708
1816
  | `tags[]` | Free labels to filter by: `smoke`, `payments`. |
1709
1817
  | `labels{}` | Facts about the result that are not its identity: `owner`, `severity`, `browser`, `build`. |
1710
- | `links[]` | Where to read more: the CI run, the issue, the trace viewer. |
1818
+ | `links[]` | Where to read more: the CI run, the issue, the trace client. |
1711
1819
  | `extensions{}` | Structured data of your own or a tool's, under a name you own. |
1712
1820
  | `measurements[]` | Numbers, with units and an allowance (already part of the contract). |
1713
1821
 
@@ -1900,7 +2008,7 @@ Annotation `type`: `note`, `warning`, `deprecation`, `issue`, `accessibility`, `
1900
2008
  | Something that should be shown well | Ask for a renderer (below). |
1901
2009
  | A field every tool should have | Open an issue. It is added as an optional field, and the rules above guarantee nothing breaks. |
1902
2010
 
1903
- **A renderer** is one entry in the viewer's list (`artifactRenderers` in
2011
+ **A renderer** is one entry in the client's list (`artifactRenderers` in
1904
2012
  `packages/visualizer/src/evidence/artifacts.tsx`): how to show an attachment of a media type. The first one that
1905
2013
  matches wins:
1906
2014
 
@@ -1912,7 +2020,7 @@ registerArtifactRenderer({
1912
2020
  });
1913
2021
  ```
1914
2022
 
1915
- The viewer is part of Atlas, so a renderer for a new media type is a contribution to Atlas: file an issue
2023
+ The client is part of Atlas, so a renderer for a new media type is a contribution to Atlas: file an issue
1916
2024
  ([reporting issues](reporting-issues.md)) with a sample file. Until it lands, the attachment is still stored,
1917
2025
  listed with its name, role, type and size, and offered for download, and everything else about the result is
1918
2026
  unaffected.
@@ -1924,7 +2032,7 @@ unaffected.
1924
2032
  > [!NOTE]
1925
2033
  > Atlas has two kinds of backend, and this page is about the first.
1926
2034
  >
1927
- > - **Run sources** (this page): where the viewer finds *statechart runs* (`manifest.json` per run)
2035
+ > - **Run sources** (this page): where the client finds *statechart runs* (`manifest.json` per run)
1928
2036
  > and live snapshots of a run in progress: a directory, HTTP files, or an HTTP API.
1929
2037
  > - **Layer stores**: where *layers of evidence for any kind of spec* are kept, an `ObjectStore`
1930
2038
  > such as a directory or a bucket. Any number of them are one report. See
@@ -1935,7 +2043,7 @@ unaffected.
1935
2043
 
1936
2044
  Atlas maps the product's areas, screens, events and journeys from a spec, then overlays evidence
1937
2045
  of what the product did. Specs and reports are independent. Local development, CI, device labs
1938
- and hosted runners can supply reports to the same product atlas simultaneously. The viewer works
2046
+ and hosted runners can supply reports to the same product atlas simultaneously. The client works
1939
2047
  before the first report exists.
1940
2048
 
1941
2049
  ## Connect sources
@@ -1954,9 +2062,9 @@ The default source is the directory passed to `--runs`. Add sources with a JSON
1954
2062
  ```
1955
2063
 
1956
2064
  ```sh
1957
- atlas-visualizer dev --specs ./specs --runs ./atlas-runs --sources ./atlas.sources.json
1958
- atlas-visualizer build --specs ./specs --runs ./atlas-runs --sources ./atlas.sources.json --out ./atlas-site
1959
- atlas-visualizer publish-data --specs ./specs --runs ./atlas-runs --sources ./atlas.sources.json --out ./atlas-data
2065
+ atlas client dev --specs ./specs --runs ./atlas-runs --sources ./atlas.sources.json
2066
+ atlas client build --specs ./specs --runs ./atlas-runs --sources ./atlas.sources.json --out ./atlas-site
2067
+ atlas client publish-data --specs ./specs --runs ./atlas-runs --sources ./atlas.sources.json --out ./atlas-data
1960
2068
  ```
1961
2069
 
1962
2070
  Directory paths resolve against the configuration file. Source ids are unique, stable names
@@ -1966,7 +2074,7 @@ Credentials must not appear in this file or URLs: descriptors are sent to the br
1966
2074
  included in static output.
1967
2075
 
1968
2076
  Remote services must allow requests
1969
- from the viewer's origin. For private services, use an authenticated same-origin gateway. Atlas
2077
+ from the client's origin. For private services, use an authenticated same-origin gateway. Atlas
1970
2078
  does not download private GitHub artifacts or sign object-storage requests itself: expose their
1971
2079
  published data over HTTP or implement a gateway using the contract below.
1972
2080
 
@@ -2034,7 +2142,7 @@ updates to keep a snapshot steady; manual Refresh still works. Resuming refreshe
2034
2142
 
2035
2143
  ## Identity, failures and publishing
2036
2144
 
2037
- Viewer identities combine source ids and native run ids. Cache keys include the backend location
2145
+ Client identities combine source ids and native run ids. Cache keys include the backend location
2038
2146
  and transport. Matching run names cannot mix results or media. Shared links retain the qualified
2039
2147
  identity; old links containing only a run id still resolve to workspace reports.
2040
2148
 
@@ -2057,7 +2165,7 @@ URLs and never expose the directory paths.
2057
2165
 
2058
2166
  `src/data/adapters/report-backend.ts` defines `ReportBackend`: stable identity and cache key,
2059
2167
  report listing, snapshot loading, media resolution and optional change subscription. The query
2060
- layer consumes this separately from the spec adapter. An embedded viewer can supply a backend
2168
+ layer consumes this separately from the spec adapter. An embedded client can supply a backend
2061
2169
  through `ReportSourcesContext`; a new built-in transport can add a factory alongside
2062
2170
  `createReportBackend`. Screens, maps and journeys require no transport-specific code.
2063
2171
 
@@ -2073,7 +2181,7 @@ paths that cover every transition, run those paths against the real product thro
2073
2181
  browser, a phone, a plain function), and write a manifest of what happened, with screenshots, video
2074
2182
  clips, captions, business events and standard test reports. Each run publishes as a layer
2075
2183
  (`atlas report publish --manifest`), so it combines with every other suite's evidence. This page covers
2076
- running them, from a runnable example to the visualizer.
2184
+ running them, from a runnable example to the Atlas client.
2077
2185
 
2078
2186
  ```
2079
2187
  specs/ atlas lint the charts are pure, reachable, free of silent dead ends,
@@ -2103,7 +2211,7 @@ served by a small Node server) specified as two charts:
2103
2211
  - `specs/getting-started.feature` and `specs/working-the-list.feature`, the same stories in Gherkin.
2104
2212
  - `specs/todo.qnt`, the list, its sync, and its actions as a Quint model.
2105
2213
 
2106
- The visualizer lists the features under Gherkin and the model under Quint.
2214
+ The Atlas client lists the features under Gherkin and the model under Quint.
2107
2215
 
2108
2216
  It has every kind of event: user events (taps and typing), a **system** event ("Sync fails", which
2109
2217
  the test triggers through a real HTTP endpoint on the example server), a **time** event ("Retry
@@ -2133,24 +2241,26 @@ visualizer updates when a run finishes.
2133
2241
 
2134
2242
  ```sh
2135
2243
  bun run atlas:run # example:clients, then example:run: every chart of the example, in showcase mode
2136
- bun run atlas:viz # the example's specs, chart runs, and layered reports
2244
+ bun atlas # the example's specs, chart runs, and layered reports
2137
2245
  ```
2138
2246
 
2139
- Each config writes its own run and the visualizer shows one run at a time: the latest is the
2247
+ Each config writes its own run and the Atlas client shows one run at a time: the latest is the
2140
2248
  single-browser run, and the run across clients is the one before it in the run picker.
2141
2249
 
2142
- `bun run viz:dev` opens the full coffee example with a layered report, without waiting
2143
- for a run. Gherkin, Quint, DMN, Alloy and OpenSLO documents sit beside the charts. The same scenarios
2144
- are filled by separate layers across desktop web, mobile web, iOS and Android, and across versions,
2145
- branches and backends (this machine, shared reports, and a device lab). A layer that repeated one
2146
- scenario leaves the others as the last layer that ran them. One feature is left with no results.
2147
- **Sources** in the header switches any of those off. The chart runs stay on the statechart map.
2148
-
2149
- `bun run atlas:viz` opens the todo example the same way. The Gherkin, Quint, DMN, Alloy and OpenSLO
2150
- tabs already show five layers, so each environment is visible before a run. `example:run` adds one
2151
- desktop web layer for the scenarios and traces it actually ran. That layer replaces only those
2152
- desktop web results. The other environments stay. `ATLAS_REPORTS_MODULE` replaces this example API
2153
- with a host's reports.
2250
+ Gherkin, Quint, DMN, Alloy and OpenSLO documents sit beside the charts. Results compose across
2251
+ environments, branches and backends within each project. **Sources** switches report sources on or off.
2252
+
2253
+ `bun atlas` runs the singleton Atlas server and the client together at `http://localhost:5180`.
2254
+ It starts a project-agnostic node with no example data. Select a project using the top-right picker.
2255
+ In another terminal run `bun run example:run`: statecharts, Gherkin scenarios and Quint traces stream running checks, completed results and current steps
2256
+ to project `todo-app` on that server while testing. Producers renew a heartbeat every 5 seconds; 30 seconds of silence
2257
+ marks an invocation interrupted. Final specs and recordings upload when their privacy scan completes.
2258
+ Tests show inline running indicators and logs without waiting for the whole suite to finish. Report history
2259
+ is retained in `atlas-data/projects/todo-app`; local run artifacts remain in `atlas-runs` and
2260
+ `atlas-reports`. To upload a completed run again without rerunning tests,
2261
+ use `bun run example:publish`. Set `ATLAS_SERVER_URL` for a different upload destination (and
2262
+ `ATLAS_PUBLISH_TOKEN` when it requires one). Restart `bun atlas` when switching from the older
2263
+ file-based example client; a busy port is an error so publishers cannot silently target another server.
2154
2264
 
2155
2265
  A showcase run ends with:
2156
2266
 
@@ -2160,7 +2270,7 @@ Transitions 14/14 passed, 0 failed, 0 not reached
2160
2270
  Manifest: examples/todo/atlas-runs/<run-id>/manifest.json
2161
2271
  ```
2162
2272
 
2163
- `example:run` then runs the Gherkin features in a browser and the Quint model, and publishes that desktop web layer beside the ones already in the report.
2273
+ `example:run` then runs the Gherkin features in a browser and the Quint model, and streams every scenario and trace as it runs, including sharing and renaming. The final recordings are uploaded alongside the completed statechart run.
2164
2274
 
2165
2275
  `bun test examples` runs [`model.test.ts`](https://github.com/crvouga/atlas/blob/main/examples/todo/model.test.ts): the same charts and journeys
2166
2276
  against `TodoModel`, a plain class, through `functionDriver` and `describeAtlas`. Only the driver
@@ -2585,7 +2695,7 @@ atlas report publish --producer statecharts --environment desktop-web \
2585
2695
  --manifest atlas-runs/<run>/manifest.json --specs ./specs --upload-run
2586
2696
  ```
2587
2697
 
2588
- `--upload-run` also sends the run's screenshots and clips to the store (or the server) so the viewer can show them
2698
+ `--upload-run` also sends the run's screenshots and clips to the store (or the server) so the client can show them
2589
2699
  (only when the run's privacy scan passed).
2590
2700
 
2591
2701
  ## Config reference
@@ -2695,7 +2805,7 @@ Runs produce media and manifests that get shared, so Atlas refuses to leak real
2695
2805
 
2696
2806
  ## Visualizer
2697
2807
 
2698
- The [`atlas-visualizer`](../packages/visualizer) command renders the charts from the specs directory and
2808
+ The [`atlas client`](../packages/visualizer) command renders the charts from the specs directory and
2699
2809
  overlays runs from the runs directory: screenshots on states, clips on transitions, statuses and
2700
2810
  history. See its README for how to point it at a project.
2701
2811
 
@@ -2706,7 +2816,7 @@ and completed steps while paths are still running.
2706
2816
 
2707
2817
  Layered reports for any spec appear beside the charts: each Gherkin, Quint, DMN, Alloy and OpenSLO
2708
2818
  document shows the combined result of its checks per environment, with the layer behind each. Point
2709
- the viewer at your stores with `reportsHost` ([integrating Atlas](integrating.md#6-browse-it)).
2819
+ the client at your stores with `reportsHost` ([integrating Atlas](integrating.md#6-browse-it)).
2710
2820
  </doc>
2711
2821
 
2712
2822
  <doc path="docs/dogfooding.md">
@@ -2763,7 +2873,7 @@ and nothing runs just because the package is installed.
2763
2873
  | `@crvouga/atlas/reports/<module>` | Publishing, storing, combining, gating and serving test evidence for specs of any kind. |
2764
2874
 
2765
2875
  It also installs two commands: `atlas` (report, lint, plan, run, merge, export) and
2766
- `atlas-visualizer` (browse specs, runs and reports).
2876
+ `atlas client` (browse specs, runs and reports).
2767
2877
 
2768
2878
  `playwright` and `detox` are optional peers: install the one your driver uses, as above. The
2769
2879
  documentation ships in the package, matching the version you installed: the README of each part at
@@ -2863,7 +2973,7 @@ Do this once per project. Check first that the package is on npm:
2863
2973
  | `@crvouga/atlas-schema` | `@crvouga/atlas/schema` |
2864
2974
  | `@crvouga/atlas-schema/schemas/<file>` | `@crvouga/atlas/schemas/<file>` |
2865
2975
  | `@crvouga/atlas-reports/<module>` | `@crvouga/atlas/reports/<module>` |
2866
- | `@crvouga/atlas-visualizer` (the `atlas-visualizer` command) | installed with `@crvouga/atlas` |
2976
+ | `@crvouga/atlas client` (the `atlas client` command) | installed with `@crvouga/atlas` |
2867
2977
 
2868
2978
  `@crvouga/atlas`, `@crvouga/atlas/spec` and `@crvouga/atlas/signals` are unchanged. One pass
2869
2979
  over a project's sources does the rest:
@@ -2906,7 +3016,7 @@ This guide is written for coding agents. Read it top to bottom once, then use
2906
3016
 
2907
3017
  `<part>` is the part of `@crvouga/atlas` the issue is about: `core` (the main import and the
2908
3018
  `atlas` command), or the import path after the package name: `playwright`, `bun`, `webdriver`,
2909
- `detox`, `vitest`, `schema`, `reports`; `visualizer` for the `atlas-visualizer` command. Use `core`
3019
+ `detox`, `vitest`, `schema`, `reports`; `visualizer` for the `atlas client` command. Use `core`
2910
3020
  when you cannot tell.
2911
3021
 
2912
3022
  1. [Search for an existing issue](#1-search-first). Comment on it instead of opening a duplicate.
@@ -2933,10 +3043,10 @@ File when:
2933
3043
 
2934
3044
  - **bug**: Atlas throws or hangs; lint, plan or a run gives a result the spec does not support;
2935
3045
  a driver taps, types, waits or records differently from what its README says; the manifest or a
2936
- report does not match its JSON Schema; the visualizer shows something other than what the run
3046
+ report does not match its JSON Schema; the Atlas client shows something other than what the run
2937
3047
  recorded; the package fails to install, import, build or type-check.
2938
3048
  - **feature**: you need something Atlas does not have: a spec construct, a config option, a driver
2939
- capability, a CLI flag, a field in the manifest, a view in the visualizer, an export format. Check
3049
+ capability, a CLI flag, a field in the manifest, a view in the Atlas client, an export format. Check
2940
3050
  that part's README and `docs/` first.
2941
3051
 
2942
3052
  Do not file when:
@@ -3069,8 +3179,9 @@ atlas — executable statecharts
3069
3179
  [--no-retry] [--output <dir>]
3070
3180
  atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...
3071
3181
  atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]
3072
- atlas serve [--data <dir>] [--port 8080] [--store <url>]... the Atlas server: results in, the live report out
3073
- atlas dev [--port 5180] the server and the viewer's dev server, on one host
3182
+ atlas server [--data <dir>] [--port 8080] [--store <url>]... one Atlas node: API, projects and client
3183
+ atlas client build [--no-data] [--out <dir>] build the project-agnostic Atlas client
3184
+ atlas dev [--port 5180] develop the Atlas server and client on one host
3074
3185
  atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
3075
3186
  atlas report publish|stream|status|merge|doctor|policy … layered evidence for any kind of spec (atlas report --help)
3076
3187
 
@@ -3110,7 +3221,7 @@ atlas serve: the Atlas server
3110
3221
  [--outline-branch <b>] [--branch <name>]... [--ignore-modified] [--refresh <seconds>]
3111
3222
  [--anonymous-read] [--insecure] [--no-log]
3112
3223
 
3113
- One process serves the viewer and the API. Test results are uploaded to it from anywhere (laptops, CI) and it
3224
+ One process serves the client and the API. Test results are uploaded to it from anywhere (laptops, CI) and it
3114
3225
  shows everyone the combined report, live. It keeps its data in --data (default ./atlas-data) and can read more
3115
3226
  backends (--store): S3, other servers, folders. Uploads go to the data folder unless --write names another.
3116
3227
 
@@ -3121,12 +3232,12 @@ atlas serve: the Atlas server
3121
3232
  ## atlas dev
3122
3233
 
3123
3234
  ```text
3124
- atlas dev: the Atlas server and the viewer, for working on either
3235
+ atlas dev: the Atlas server and the client, for working on either
3125
3236
 
3126
3237
  atlas dev [--port 5180] [--host 127.0.0.1] [--data <dir>] [--store <url|id=url>]... [--config atlas.server.json]
3127
3238
 
3128
- Starts the server (API only, on a port of its own) and the viewer's dev server, which proxies /api to it, so the
3129
- browser talks to one host and a change to the viewer shows at once. In production `atlas serve` does both from
3239
+ Starts the server (API only, on a port of its own) and the client's dev server, which proxies /api to it, so the
3240
+ browser talks to one host and a change to the client shows at once. In production `atlas serve` does both from
3130
3241
  one process.
3131
3242
  ```
3132
3243
 
@@ -3154,7 +3265,8 @@ atlas report — layered evidence for any kind of spec
3154
3265
  atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]
3155
3266
 
3156
3267
  A store is a directory or s3://bucket/prefix (add ?region=…&endpoint=… for R2, MinIO and the
3157
- rest). When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set
3268
+ rest). Select a project on an Atlas node with --project <slug> or ATLAS_PROJECT. This scopes specs, reports and recordings together.
3269
+ When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set
3158
3270
  AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with `aws sso login`, or use a role.
3159
3271
 
3160
3272
  publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.
@@ -3204,6 +3316,134 @@ atlas report — layered evidence for any kind of spec
3204
3316
 
3205
3317
  > Generated from the operation table in `packages/server/src/openapi.ts` by `bun run docs`. Do not edit. The same description, for tools, is `openapi.json` (OpenAPI 3.1) and `GET /api/openapi.json` on every server.
3206
3318
 
3319
+ ### List projects on this Atlas node
3320
+
3321
+ `GET /api/projects` · role: read
3322
+
3323
+ Projects are created on their first authenticated upload. All report, spec, recording and live-run data is isolated by project. Unqualified API calls address the legacy default project. Tokens retain their node-wide role; project partitioning is not a tenant authorization boundary.
3324
+
3325
+ Responses: `200` Available project identities.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The project catalog is unavailable.
3326
+
3327
+ ### Live test progress and recent invocations
3328
+
3329
+ `GET /api/live` · role: read
3330
+
3331
+ Returns up to 50 recent producer leases, including running, completed and interrupted runs. Counters and recent check/step updates are bounded; authentication credentials are never returned. Consume run events for immediate bounded snapshots; re-fetch after reconnect or atlas:change with scope runs on older servers.
3332
+
3333
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3334
+
3335
+ Responses: `200` Recent live run summaries.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
3336
+
3337
+ ### Live test progress and recent invocations
3338
+
3339
+ `GET /api/projects/{project}/live` · role: read
3340
+
3341
+ Returns up to 50 recent producer leases, including running, completed and interrupted runs. Counters and recent check/step updates are bounded; authentication credentials are never returned. Consume run events for immediate bounded snapshots; re-fetch after reconnect or atlas:change with scope runs on older servers.
3342
+
3343
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3344
+
3345
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3346
+
3347
+ Responses: `200` Recent live run summaries.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
3348
+
3349
+ ### Start a live producer lease
3350
+
3351
+ `POST /api/live/{id}` · role: publish
3352
+
3353
+ Register a UUID run id and fixed producer identity before tests begin. Retrying the same identity and token is idempotent. The server timestamps receipt. Send heartbeats every 5 seconds and progress as tests execute. The default heartbeat timeout is 30 seconds; a 15-minute progress stall is configurable separately.
3354
+
3355
+ - `id` (path, required): Producer-generated UUID, also given in the body.
3356
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3357
+
3358
+ Responses: `201` The registered lease.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` The id already belongs to another invocation.; `413` Request too large.; `422` Invalid identity.; `429` Live run capacity reached.
3359
+
3360
+ ### Start a live producer lease
3361
+
3362
+ `POST /api/projects/{project}/live/{id}` · role: publish
3363
+
3364
+ Register a UUID run id and fixed producer identity before tests begin. Retrying the same identity and token is idempotent. The server timestamps receipt. Send heartbeats every 5 seconds and progress as tests execute. The default heartbeat timeout is 30 seconds; a 15-minute progress stall is configurable separately.
3365
+
3366
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3367
+
3368
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3369
+ - `id` (path, required): Producer-generated UUID, also given in the body.
3370
+
3371
+ Responses: `201` The registered lease.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` The id already belongs to another invocation.; `413` Request too large.; `422` Invalid identity.; `429` Live run capacity reached.
3372
+
3373
+ ### Publish progress or finish an invocation
3374
+
3375
+ `PATCH /api/live/{id}` · role: publish
3376
+
3377
+ Apply ordered check deltas, starting at sequence 1. Retry a sequence with identical content only. Completed results become immutable report layers immediately; running checks remain operational progress and do not replace historical winners. Finish as completed or interrupted. Stale updates cannot revive an expired lease; pending checks never become passes.
3378
+
3379
+ - `id` (path, required): The UUID of a run started with this publisher token.
3380
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3381
+
3382
+ Responses: `200` Updated progress.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `404` Unknown run.; `409` Run ended or sequence is stale.; `413` Request too large.; `422` Invalid progress.; `429` Run check capacity reached.
3383
+
3384
+ ### Publish progress or finish an invocation
3385
+
3386
+ `PATCH /api/projects/{project}/live/{id}` · role: publish
3387
+
3388
+ Apply ordered check deltas, starting at sequence 1. Retry a sequence with identical content only. Completed results become immutable report layers immediately; running checks remain operational progress and do not replace historical winners. Finish as completed or interrupted. Stale updates cannot revive an expired lease; pending checks never become passes.
3389
+
3390
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3391
+
3392
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3393
+ - `id` (path, required): The UUID of a run started with this publisher token.
3394
+
3395
+ Responses: `200` Updated progress.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `404` Unknown run.; `409` Run ended or sequence is stale.; `413` Request too large.; `422` Invalid progress.; `429` Run check capacity reached.
3396
+
3397
+ ### Renew a producer heartbeat
3398
+
3399
+ `POST /api/live/{id}/heartbeat` · role: publish
3400
+
3401
+ Renew a running lease using server receipt time, independently of test progress. No result layers are created. Heartbeats cannot revive terminal runs or bypass the configured progress-stall timeout. The producer token must be the one that started this invocation.
3402
+
3403
+ - `id` (path, required): The UUID of this producer invocation.
3404
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3405
+
3406
+ Responses: `200` Heartbeat received.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `404` Unknown run.; `409` Run ended or its lease expired.; `422` Invalid run id.
3407
+
3408
+ ### Renew a producer heartbeat
3409
+
3410
+ `POST /api/projects/{project}/live/{id}/heartbeat` · role: publish
3411
+
3412
+ Renew a running lease using server receipt time, independently of test progress. No result layers are created. Heartbeats cannot revive terminal runs or bypass the configured progress-stall timeout. The producer token must be the one that started this invocation.
3413
+
3414
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3415
+
3416
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3417
+ - `id` (path, required): The UUID of this producer invocation.
3418
+
3419
+ Responses: `200` Heartbeat received.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `404` Unknown run.; `409` Run ended or its lease expired.; `422` Invalid run id.
3420
+
3421
+ ### Server settings page
3422
+
3423
+ `GET /admin` · role: read
3424
+
3425
+ A standalone settings editor. Supply an admin Bearer token in the page to read or save configuration. The token is held only in memory.
3426
+
3427
+ Responses: `200` Settings editor.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
3428
+
3429
+ ### Read server settings
3430
+
3431
+ `GET /api/settings` · role: admin
3432
+
3433
+ Returns editable persisted storage/report settings, active settings, a revision, and configuration precedence. Authentication settings are excluded. Requires an admin Bearer token. Embedded applications may manage settings outside this API.
3434
+
3435
+ Responses: `200` Settings and revision.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `503` Configuration is managed by the embedding application.
3436
+
3437
+ ### Save settings for the next restart
3438
+
3439
+ `PUT /api/settings` · role: admin
3440
+
3441
+ Replaces editable storage/report settings, preserving authentication and other file fields. Atomically persists a validated configuration; no report data is moved. Environment variables and flags override these values after restart. Requires an admin Bearer token, same-origin requests in browsers, and a current revision.
3442
+
3443
+ - `If-Match` (header, required): The quoted revision returned by GET /api/settings.
3444
+
3445
+ Responses: `200` Saved settings and new revision. Restart required.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` Settings changed; reload before saving.; `413` Settings exceed the body limit.; `422` Invalid configuration.; `428` A quoted settings revision is required.; `503` Configuration is managed by the embedding application.
3446
+
3207
3447
  ### Is the process up
3208
3448
 
3209
3449
  `GET /healthz` · role: none
@@ -3248,17 +3488,44 @@ Responses: `200` The server.; `401` No token, or an unknown one. Send `Authoriza
3248
3488
 
3249
3489
  `GET /api/events` · role: read
3250
3490
 
3251
- A `text/event-stream` that stays open. Events say *that* something changed; the state is always fetched, so a client that missed events (a dropped connection) loses nothing: read again on reconnect.
3491
+ A `text/event-stream` that stays open. Report events announce changes, and run events carry bounded live progress snapshots; authoritative state can always be fetched, so a client that missed events (a dropped connection) loses nothing: read again on reconnect.
3492
+
3493
+ | event | data |
3494
+ | --- | --- |
3495
+ | `hello` | `{ version, ready, time, heartbeatMs? }`, sent first |
3496
+ | `report` | `{ version, totalLayers, added: [{ id, producer, environment, branch, sha, recordedAtMs, counts }], addedTotal }`: layers arrived |
3497
+ | `atlas:change` | `{ scope: "specs" \| "runs", files, runIds }`: new specs or runs |
3498
+ | `run` | A LiveRun snapshot after start, progress, heartbeat, completion, or interruption; contains no producer credentials |
3499
+ | `projects` | `{}` when the node discovers another project; refresh `/api/projects` |
3500
+ | `ping` | `{}` every `heartbeatSeconds`, so a quiet server is told apart from a dead connection |
3501
+
3502
+ Past `limits.maxStreams` viewers the answer is `503` with `Retry-After`: poll `/api/reports/evidence/effective` instead. A browser needs no code beyond `new EventSource("/api/events")`; other clients send the token as `Authorization: Bearer`.
3503
+
3504
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3505
+
3506
+ Responses: `200` The stream.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` Too many viewers are connected; poll instead.
3507
+
3508
+ ### Server-sent events: results arrived, specs or runs changed
3509
+
3510
+ `GET /api/projects/{project}/events` · role: read
3511
+
3512
+ A `text/event-stream` that stays open. Report events announce changes, and run events carry bounded live progress snapshots; authoritative state can always be fetched, so a client that missed events (a dropped connection) loses nothing: read again on reconnect.
3252
3513
 
3253
3514
  | event | data |
3254
3515
  | --- | --- |
3255
- | `hello` | `{ version, ready, time }`, sent first |
3516
+ | `hello` | `{ version, ready, time, heartbeatMs? }`, sent first |
3256
3517
  | `report` | `{ version, totalLayers, added: [{ id, producer, environment, branch, sha, recordedAtMs, counts }], addedTotal }`: layers arrived |
3257
3518
  | `atlas:change` | `{ scope: "specs" \| "runs", files, runIds }`: new specs or runs |
3519
+ | `run` | A LiveRun snapshot after start, progress, heartbeat, completion, or interruption; contains no producer credentials |
3520
+ | `projects` | `{}` when the node discovers another project; refresh `/api/projects` |
3258
3521
  | `ping` | `{}` every `heartbeatSeconds`, so a quiet server is told apart from a dead connection |
3259
3522
 
3260
3523
  Past `limits.maxStreams` viewers the answer is `503` with `Retry-After`: poll `/api/reports/evidence/effective` instead. A browser needs no code beyond `new EventSource("/api/events")`; other clients send the token as `Authorization: Bearer`.
3261
3524
 
3525
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3526
+
3527
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3528
+
3262
3529
  Responses: `200` The stream.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` Too many viewers are connected; poll instead.
3263
3530
 
3264
3531
  ### The whole report, or only what changed since a version
@@ -3271,6 +3538,25 @@ The report as it stands: only the layers that win at least one check, each with
3271
3538
 
3272
3539
  To combine layers yourself, a check's *slot* is `(producer, kind, specPath, check id, environment, parameters)`; the newest completed result wins it. See `docs/report-contract.md`.
3273
3540
 
3541
+ - `since` (query): The `version` of the report you hold.
3542
+ - `if-none-match` (header): The ETag (the quoted version) of the report you hold.
3543
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3544
+
3545
+ Responses: `200` The whole report, or a delta when `since` was given and remembered.; `304` You already hold this version.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
3546
+
3547
+ ### The whole report, or only what changed since a version
3548
+
3549
+ `GET /api/projects/{project}/reports/evidence/effective` · role: read
3550
+
3551
+ The report as it stands: only the layers that win at least one check, each with only the rows that win. Whatever the history behind it, this is one request.
3552
+
3553
+ `version` changes exactly when the report does, so it works as an ETag: send `If-None-Match` and get `304`. Better, send `?since=<version>` of the report you hold and get **only what changed** (`delta: true`): replace the layers of the same id, drop `removed`, keep the rest. If the server does not remember that version it answers with the whole report (no `delta` key), so a client always handles both.
3554
+
3555
+ To combine layers yourself, a check's *slot* is `(producer, kind, specPath, check id, environment, parameters)`; the newest completed result wins it. See `docs/report-contract.md`.
3556
+
3557
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3558
+
3559
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3274
3560
  - `since` (query): The `version` of the report you hold.
3275
3561
  - `if-none-match` (header): The ETag (the quoted version) of the report you hold.
3276
3562
 
@@ -3284,6 +3570,25 @@ The `atlas report status --format json` document, worked out by the server from
3284
3570
 
3285
3571
  The header `X-Atlas-Ok: true|false` carries the verdict, so a shell can test it without parsing. Narrow it by choice, per request: `branch` (repeatable) and `ignoreModified` read only matching layers.
3286
3572
 
3573
+ - `branch` (query): Only layers recorded on this branch. Repeatable.
3574
+ - `ignoreModified` (query): `1`: leave out layers from modified checkouts and unknown origins.
3575
+ - `requireEnvironments` (query): Comma-separated environments that must have results.
3576
+ - `requireAllSpecs` (query): `1`: fail while any spec of the outline has no results.
3577
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3578
+
3579
+ Responses: `200` The verdict, whether or not it passes.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
3580
+
3581
+ ### The gate: does the report pass
3582
+
3583
+ `GET /api/projects/{project}/status` · role: read
3584
+
3585
+ The `atlas report status --format json` document, worked out by the server from the layers it holds and remembered for each version of the report. It passes (`ok: true`) only if some check completed, none failed, none is still running, every layer and backend could be read, and every required environment has results. **Unknown is never a pass.**
3586
+
3587
+ The header `X-Atlas-Ok: true|false` carries the verdict, so a shell can test it without parsing. Narrow it by choice, per request: `branch` (repeatable) and `ignoreModified` read only matching layers.
3588
+
3589
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3590
+
3591
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3287
3592
  - `branch` (query): Only layers recorded on this branch. Repeatable.
3288
3593
  - `ignoreModified` (query): `1`: leave out layers from modified checkouts and unknown origins.
3289
3594
  - `requireEnvironments` (query): Comma-separated environments that must have results.
@@ -3299,6 +3604,22 @@ The simplest way in: anything that can make an HTTP request can report. The body
3299
3604
 
3300
3605
  Send the token as `Authorization: Bearer <token>` (a `publish` or `admin` token). A browser's sign-in box is refused for uploads on purpose. A check id appearing twice in one body is `422`.
3301
3606
 
3607
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3608
+
3609
+ Responses: `201` The layer was written.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `413` The body is over `limits.maxJsonMiB`.; `422` The body is not valid: the detail says which field.
3610
+
3611
+ ### Upload one layer of checks as JSON
3612
+
3613
+ `POST /api/projects/{project}/checks` · role: publish
3614
+
3615
+ The simplest way in: anything that can make an HTTP request can report. The body is the checks file (`packages/schema/schemas/publish-checks.schema.json`); `recordedAtMs` defaults to now. Binary evidence goes in as `base64`. The server writes the layer and it is in every viewer's report before this answers.
3616
+
3617
+ Send the token as `Authorization: Bearer <token>` (a `publish` or `admin` token). A browser's sign-in box is refused for uploads on purpose. A check id appearing twice in one body is `422`.
3618
+
3619
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3620
+
3621
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3622
+
3302
3623
  Responses: `201` The layer was written.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `413` The body is over `limits.maxJsonMiB`.; `422` The body is not valid: the detail says which field.
3303
3624
 
3304
3625
  ### List keys under layers/
@@ -3307,6 +3628,22 @@ Responses: `201` The layer was written.; `401` No token, or an unknown one. Send
3307
3628
 
3308
3629
  One page of the keys of `layers/…` (never anything else), in code-unit order. Page with `after` = the `next` of the previous page. This is the `listPage` of the `ObjectStore` interface; `atlas report merge` and `httpStore` use it.
3309
3630
 
3631
+ - `prefix` (query): Only keys starting with this. Default `layers/`.
3632
+ - `after` (query): Only keys after this one (exclusive).
3633
+ - `limit` (query): Page size, 1 to 1000. Default 1000.
3634
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3635
+
3636
+ Responses: `200` A page.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `422` The prefix is not one Atlas holds.
3637
+
3638
+ ### List keys under layers/
3639
+
3640
+ `GET /api/projects/{project}/store` · role: read
3641
+
3642
+ One page of the keys of `layers/…` (never anything else), in code-unit order. Page with `after` = the `next` of the previous page. This is the `listPage` of the `ObjectStore` interface; `atlas report merge` and `httpStore` use it.
3643
+
3644
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3645
+
3646
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3310
3647
  - `prefix` (query): Only keys starting with this. Default `layers/`.
3311
3648
  - `after` (query): Only keys after this one (exclusive).
3312
3649
  - `limit` (query): Page size, 1 to 1000. Default 1000.
@@ -3319,6 +3656,20 @@ Responses: `200` A page.; `401` No token, or an unknown one. Send `Authorization
3319
3656
 
3320
3657
  The bytes of one object, with the content type it was stored with. Everything under `layers/` is immutable, so the answer is cacheable for good. The bytes came from a test run and are untrusted: they are served with `nosniff` and a sandboxing Content-Security-Policy, whatever type their uploader gave them.
3321
3658
 
3659
+ - `key` (path, required): An object key under `layers/`, such as `layers/manifests/<id>.json`. It contains slashes, which are not percent-encoded; every other character is.
3660
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3661
+
3662
+ Responses: `200` The object.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such object.; `422` Not a key Atlas holds.
3663
+
3664
+ ### Read an object
3665
+
3666
+ `GET /api/projects/{project}/store/{key}` · role: read
3667
+
3668
+ The bytes of one object, with the content type it was stored with. Everything under `layers/` is immutable, so the answer is cacheable for good. The bytes came from a test run and are untrusted: they are served with `nosniff` and a sandboxing Content-Security-Policy, whatever type their uploader gave them.
3669
+
3670
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3671
+
3672
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3322
3673
  - `key` (path, required): An object key under `layers/`, such as `layers/manifests/<id>.json`. It contains slashes, which are not percent-encoded; every other character is.
3323
3674
 
3324
3675
  Responses: `200` The object.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such object.; `422` Not a key Atlas holds.
@@ -3329,6 +3680,20 @@ Responses: `200` The object.; `401` No token, or an unknown one. Send `Authoriza
3329
3680
 
3330
3681
  `200` when the object exists, `404` when not. No body.
3331
3682
 
3683
+ - `key` (path, required): An object key under `layers/`, such as `layers/manifests/<id>.json`. It contains slashes, which are not percent-encoded; every other character is.
3684
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3685
+
3686
+ Responses: `200` It exists.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such object.
3687
+
3688
+ ### Does an object exist
3689
+
3690
+ `HEAD /api/projects/{project}/store/{key}` · role: read
3691
+
3692
+ `200` when the object exists, `404` when not. No body.
3693
+
3694
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3695
+
3696
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3332
3697
  - `key` (path, required): An object key under `layers/`, such as `layers/manifests/<id>.json`. It contains slashes, which are not percent-encoded; every other character is.
3333
3698
 
3334
3699
  Responses: `200` It exists.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such object.
@@ -3347,6 +3712,29 @@ The write half of the `ObjectStore` interface. The server checks what it can fro
3347
3712
 
3348
3713
  Upload in dependency order: blobs, then the manifest, then the catalog entry. `atlas report publish`, `atlas report stream` and `httpStore` do. Needs a `Bearer` token of role `publish` or `admin`; a browser's sign-in box is refused.
3349
3714
 
3715
+ - `key` (path, required): An object key under `layers/`, such as `layers/manifests/<id>.json`. It contains slashes, which are not percent-encoded; every other character is.
3716
+ - `content-type` (header): Stored with the object and returned when it is read.
3717
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3718
+
3719
+ Responses: `200` Already there with the same bytes.; `201` Stored.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` A dependency has not been uploaded yet, or other bytes are already stored under this key.; `413` Over `limits.maxObjectMiB`.; `422` Not a key Atlas writes, or the content is not what its name says.
3720
+
3721
+ ### Upload an object
3722
+
3723
+ `PUT /api/projects/{project}/store/{key}` · role: publish
3724
+
3725
+ The write half of the `ObjectStore` interface. The server checks what it can from the bytes in hand and says no to what would corrupt the report for everyone:
3726
+
3727
+ - a **layer manifest** (`layers/manifests/<id>.json`) must be a valid layer whose content hashes to `<id>`, and its blobs must already be there (`409` otherwise);
3728
+ - a **blob** (`layers/blobs/<sha256>.gz`) must be gzip of content whose SHA-256 is its name; a **spec file** likewise;
3729
+ - a **catalog entry** (`…/index/…`) must name something already uploaded (`409` otherwise): it is what makes a layer visible, so it is written last;
3730
+ - a **run manifest** must have passed its privacy scan;
3731
+ - keys are immutable: the same bytes again are `200` (a retry), other bytes under the same key are `409`.
3732
+
3733
+ Upload in dependency order: blobs, then the manifest, then the catalog entry. `atlas report publish`, `atlas report stream` and `httpStore` do. Needs a `Bearer` token of role `publish` or `admin`; a browser's sign-in box is refused.
3734
+
3735
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3736
+
3737
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3350
3738
  - `key` (path, required): An object key under `layers/`, such as `layers/manifests/<id>.json`. It contains slashes, which are not percent-encoded; every other character is.
3351
3739
  - `content-type` (header): Stored with the object and returned when it is read.
3352
3740
 
@@ -3358,6 +3746,20 @@ Responses: `200` Already there with the same bytes.; `201` Stored.; `401` No tok
3358
3746
 
3359
3747
  Only the scratch objects `atlas report doctor` writes (`layers/doctor-…`) can be removed. Layers, specs and runs are never removed through the API.
3360
3748
 
3749
+ - `key` (path, required): An object key under `layers/`, such as `layers/manifests/<id>.json`. It contains slashes, which are not percent-encoded; every other character is.
3750
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3751
+
3752
+ Responses: `204` Removed (or it was not there).; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` Not a scratch object, or the token may not.
3753
+
3754
+ ### Remove a scratch object
3755
+
3756
+ `DELETE /api/projects/{project}/store/{key}` · role: publish
3757
+
3758
+ Only the scratch objects `atlas report doctor` writes (`layers/doctor-…`) can be removed. Layers, specs and runs are never removed through the API.
3759
+
3760
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3761
+
3762
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3361
3763
  - `key` (path, required): An object key under `layers/`, such as `layers/manifests/<id>.json`. It contains slashes, which are not percent-encoded; every other character is.
3362
3764
 
3363
3765
  Responses: `204` Removed (or it was not there).; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` Not a scratch object, or the token may not.
@@ -3368,6 +3770,20 @@ Responses: `204` Removed (or it was not there).; `401` No token, or an unknown o
3368
3770
 
3369
3771
  The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. A spec here with no result shows as having none.
3370
3772
 
3773
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3774
+
3775
+ Responses: `200` The outline.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
3776
+
3777
+ ### The specs a report is laid over
3778
+
3779
+ `GET /api/projects/{project}/reports/outline` · role: read
3780
+
3781
+ The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. A spec here with no result shows as having none.
3782
+
3783
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3784
+
3785
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3786
+
3371
3787
  Responses: `200` The outline.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
3372
3788
 
3373
3789
  ### The text of one spec
@@ -3376,6 +3792,20 @@ Responses: `200` The outline.; `401` No token, or an unknown one. Send `Authoriz
3376
3792
 
3377
3793
  The text of a spec file of the outline, by its path.
3378
3794
 
3795
+ - `path` (query, required): The spec's path, `specs/<path>`.
3796
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3797
+
3798
+ Responses: `200` The spec text.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such spec in the outline.
3799
+
3800
+ ### The text of one spec
3801
+
3802
+ `GET /api/projects/{project}/reports/outline/spec` · role: read
3803
+
3804
+ The text of a spec file of the outline, by its path.
3805
+
3806
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3807
+
3808
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3379
3809
  - `path` (query, required): The spec's path, `specs/<path>`.
3380
3810
 
3381
3811
  Responses: `200` The spec text.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such spec in the outline.
@@ -3386,6 +3816,20 @@ Responses: `200` The spec text.; `401` No token, or an unknown one. Send `Author
3386
3816
 
3387
3817
  The history: layer headers newest first, merged from every backend. Prefer `/api/reports/evidence/effective`, which is one request and holds only what wins. Use this to audit what was recorded.
3388
3818
 
3819
+ - `cursor` (query): The `cursor` of the previous page.
3820
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3821
+
3822
+ Responses: `200` A page of headers.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
3823
+
3824
+ ### Page through every layer header
3825
+
3826
+ `GET /api/projects/{project}/reports/evidence` · role: read
3827
+
3828
+ The history: layer headers newest first, merged from every backend. Prefer `/api/reports/evidence/effective`, which is one request and holds only what wins. Use this to audit what was recorded.
3829
+
3830
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3831
+
3832
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3389
3833
  - `cursor` (query): The `cursor` of the previous page.
3390
3834
 
3391
3835
  Responses: `200` A page of headers.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
@@ -3396,6 +3840,21 @@ Responses: `200` A page of headers.; `401` No token, or an unknown one. Send `Au
3396
3840
 
3397
3841
  Every check of one layer (not only those that win), with the spec fingerprints it recorded.
3398
3842
 
3843
+ - `backend` (path, required): A backend id from `/api/server`.
3844
+ - `id` (path, required): A layer id: 64 lowercase hex digits.
3845
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3846
+
3847
+ Responses: `200` The rows.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such layer in that backend.
3848
+
3849
+ ### The checks of one layer
3850
+
3851
+ `GET /api/projects/{project}/reports/evidence/rows/{backend}/{id}` · role: read
3852
+
3853
+ Every check of one layer (not only those that win), with the spec fingerprints it recorded.
3854
+
3855
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3856
+
3857
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3399
3858
  - `backend` (path, required): A backend id from `/api/server`.
3400
3859
  - `id` (path, required): A layer id: 64 lowercase hex digits.
3401
3860
 
@@ -3407,6 +3866,23 @@ Responses: `200` The rows.; `401` No token, or an unknown one. Send `Authorizati
3407
3866
 
3408
3867
  The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
3409
3868
 
3869
+ - `backend` (path, required): A backend id from `/api/server`.
3870
+ - `id` (path, required): A layer id: 64 lowercase hex digits.
3871
+ - `check` (query): The check id.
3872
+ - `kind` (query): The kind of the part, when the same id is in several.
3873
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3874
+
3875
+ Responses: `200` The detail.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such layer or check.
3876
+
3877
+ ### Everything one result carries
3878
+
3879
+ `GET /api/projects/{project}/reports/evidence/detail/{backend}/{id}` · role: read
3880
+
3881
+ The timeline, annotations, retries, attachments and extensions of one check, and what its run carries as a whole. Without `check`, only the run's. Attachment bytes are fetched from `/api/reports/layers/{id}/blobs/{bundleKey}`.
3882
+
3883
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3884
+
3885
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3410
3886
  - `backend` (path, required): A backend id from `/api/server`.
3411
3887
  - `id` (path, required): A layer id: 64 lowercase hex digits.
3412
3888
  - `check` (query): The check id.
@@ -3420,6 +3896,22 @@ Responses: `200` The detail.; `401` No token, or an unknown one. Send `Authoriza
3420
3896
 
3421
3897
  A screenshot, video, trace, log or any attachment of a layer, by the `bundleKey` its attachment names (`attachments/<sha256>`). Served as untrusted bytes: never sniffed, never run, sandboxed; immutable, cacheable for good; videos can be sought with `Range`.
3422
3898
 
3899
+ - `id` (path, required): A layer id: 64 lowercase hex digits.
3900
+ - `bundleKey` (path, required): The attachment's `bundleKey`, which contains a slash.
3901
+ - `range` (header): A byte range, `bytes=0-1023`.
3902
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3903
+
3904
+ Responses: `200` The bytes, with the type the producer gave (or `application/octet-stream`).; `206` The requested range.; `304` Not modified.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such layer or file.
3905
+
3906
+ ### A file a layer carries
3907
+
3908
+ `GET /api/projects/{project}/reports/layers/{id}/blobs/{bundleKey}` · role: read
3909
+
3910
+ A screenshot, video, trace, log or any attachment of a layer, by the `bundleKey` its attachment names (`attachments/<sha256>`). Served as untrusted bytes: never sniffed, never run, sandboxed; immutable, cacheable for good; videos can be sought with `Range`.
3911
+
3912
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3913
+
3914
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3423
3915
  - `id` (path, required): A layer id: 64 lowercase hex digits.
3424
3916
  - `bundleKey` (path, required): The attachment's `bundleKey`, which contains a slash.
3425
3917
  - `range` (header): A byte range, `bytes=0-1023`.
@@ -3432,6 +3924,21 @@ Responses: `200` The bytes, with the type the producer gave (or `application/oct
3432
3924
 
3433
3925
  The `report.json`, captured files and frame stylesheet of a recorded statechart test of a layer (`tests/<testId>/report.json`, `tests/<testId>/files/…`, `frame.css`). Untrusted bytes, served like attachments.
3434
3926
 
3927
+ - `id` (path, required): A layer id: 64 lowercase hex digits.
3928
+ - `path` (path, required): The path under the layer.
3929
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3930
+
3931
+ Responses: `200` The file.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such file.
3932
+
3933
+ ### A recorded test of a statechart run
3934
+
3935
+ `GET /api/projects/{project}/reports/layers/{id}/{path}` · role: read
3936
+
3937
+ The `report.json`, captured files and frame stylesheet of a recorded statechart test of a layer (`tests/<testId>/report.json`, `tests/<testId>/files/…`, `frame.css`). Untrusted bytes, served like attachments.
3938
+
3939
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3940
+
3941
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3435
3942
  - `id` (path, required): A layer id: 64 lowercase hex digits.
3436
3943
  - `path` (path, required): The path under the layer.
3437
3944
 
@@ -3443,6 +3950,20 @@ Responses: `200` The file.; `401` No token, or an unknown one. Send `Authorizati
3443
3950
 
3444
3951
  For spec runs writing on the same machine as a development host (`atlas-visualizer dev`). A server has none: it answers `404`.
3445
3952
 
3953
+ - `source` (path, required): The live source id.
3954
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3955
+
3956
+ Responses: `200` The stylesheet.; `404` Not a live source.
3957
+
3958
+ ### Frame stylesheet of a live spec run (development only)
3959
+
3960
+ `GET /api/projects/{project}/reports/live/{source}/frame.css` · role: read
3961
+
3962
+ For spec runs writing on the same machine as a development host (`atlas-visualizer dev`). A server has none: it answers `404`.
3963
+
3964
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3965
+
3966
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3446
3967
  - `source` (path, required): The live source id.
3447
3968
 
3448
3969
  Responses: `200` The stylesheet.; `404` Not a live source.
@@ -3453,6 +3974,22 @@ Responses: `200` The stylesheet.; `404` Not a live source.
3453
3974
 
3454
3975
  For spec runs writing on the same machine as a development host. A server has none: it answers `404`.
3455
3976
 
3977
+ - `source` (path, required): The live source id.
3978
+ - `testId` (path, required): The test id.
3979
+ - `path` (path, required): The path under the test.
3980
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3981
+
3982
+ Responses: `200` The file.; `404` Not a live source.
3983
+
3984
+ ### A file of a test in a live spec run (development only)
3985
+
3986
+ `GET /api/projects/{project}/reports/live/{source}/tests/{testId}/{path}` · role: read
3987
+
3988
+ For spec runs writing on the same machine as a development host. A server has none: it answers `404`.
3989
+
3990
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
3991
+
3992
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3456
3993
  - `source` (path, required): The live source id.
3457
3994
  - `testId` (path, required): The test id.
3458
3995
  - `path` (path, required): The path under the test.
@@ -3465,6 +4002,20 @@ Responses: `200` The file.; `404` Not a live source.
3465
4002
 
3466
4003
  The viewer's address book of data sources, in the shape a static host serves. A server has one source, itself.
3467
4004
 
4005
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4006
+
4007
+ Responses: `200` The sources file.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
4008
+
4009
+ ### The sources the viewer reads
4010
+
4011
+ `GET /api/projects/{project}/data/sources.json` · role: read
4012
+
4013
+ The viewer's address book of data sources, in the shape a static host serves. A server has one source, itself.
4014
+
4015
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
4016
+
4017
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4018
+
3468
4019
  Responses: `200` The sources file.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
3469
4020
 
3470
4021
  ### The spec files
@@ -3473,6 +4024,20 @@ Responses: `200` The sources file.; `401` No token, or an unknown one. Send `Aut
3473
4024
 
3474
4025
  The files of the newest committed spec set, relative to `specs/`.
3475
4026
 
4027
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4028
+
4029
+ Responses: `200` The index.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
4030
+
4031
+ ### The spec files
4032
+
4033
+ `GET /api/projects/{project}/data/specs/index.json` · role: read
4034
+
4035
+ The files of the newest committed spec set, relative to `specs/`.
4036
+
4037
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
4038
+
4039
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4040
+
3476
4041
  Responses: `200` The index.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
3477
4042
 
3478
4043
  ### One spec file
@@ -3481,6 +4046,20 @@ Responses: `200` The index.; `401` No token, or an unknown one. Send `Authorizat
3481
4046
 
3482
4047
  The text of a spec file, with the digest of its content as `ETag`.
3483
4048
 
4049
+ - `file` (path, required): The file's path relative to `specs/`.
4050
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4051
+
4052
+ Responses: `200` The text.; `304` Not modified.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such spec.
4053
+
4054
+ ### One spec file
4055
+
4056
+ `GET /api/projects/{project}/data/specs/{file}` · role: read
4057
+
4058
+ The text of a spec file, with the digest of its content as `ETag`.
4059
+
4060
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
4061
+
4062
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3484
4063
  - `file` (path, required): The file's path relative to `specs/`.
3485
4064
 
3486
4065
  Responses: `200` The text.; `304` Not modified.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such spec.
@@ -3491,6 +4070,20 @@ Responses: `200` The text.; `304` Not modified.; `401` No token, or an unknown o
3491
4070
 
3492
4071
  The newest uploaded statechart runs (`atlas report publish --manifest … --upload-run`), newest first.
3493
4072
 
4073
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4074
+
4075
+ Responses: `200` The index.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
4076
+
4077
+ ### The statechart runs
4078
+
4079
+ `GET /api/projects/{project}/data/runs/index.json` · role: read
4080
+
4081
+ The newest uploaded statechart runs (`atlas report publish --manifest … --upload-run`), newest first.
4082
+
4083
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
4084
+
4085
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4086
+
3494
4087
  Responses: `200` The index.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
3495
4088
 
3496
4089
  ### A file of an uploaded run
@@ -3499,6 +4092,21 @@ Responses: `200` The index.; `401` No token, or an unknown one. Send `Authorizat
3499
4092
 
3500
4093
  A file of an uploaded run: its manifest, timelines, screenshots, clips. Untrusted bytes, served sandboxed; immutable.
3501
4094
 
4095
+ - `id` (path, required): The run id.
4096
+ - `path` (path, required): The path inside the run.
4097
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4098
+
4099
+ Responses: `200` The file.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such run or file.
4100
+
4101
+ ### A file of an uploaded run
4102
+
4103
+ `GET /api/projects/{project}/data/runs/{id}/{path}` · role: read
4104
+
4105
+ A file of an uploaded run: its manifest, timelines, screenshots, clips. Untrusted bytes, served sandboxed; immutable.
4106
+
4107
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
4108
+
4109
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
3502
4110
  - `id` (path, required): The run id.
3503
4111
  - `path` (path, required): The path inside the run.
3504
4112