@crvouga/atlas 1.19.0 → 1.21.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 (48) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/core/src/cli.d.ts +1 -1
  3. package/dist/core/src/cli.js +3 -0
  4. package/dist/reports/src/compose.d.ts +23 -1
  5. package/dist/reports/src/compose.js +29 -19
  6. package/dist/reports/src/report-cli.d.ts +13 -0
  7. package/dist/reports/src/report-cli.js +6 -4
  8. package/dist/reports/src/ui-catalog-cli.d.ts +10 -0
  9. package/dist/reports/src/ui-catalog-cli.js +391 -0
  10. package/dist/reports/src/ui-catalog-diff.d.ts +104 -0
  11. package/dist/reports/src/ui-catalog-diff.js +130 -0
  12. package/dist/reports/src/ui-catalog-export.d.ts +82 -0
  13. package/dist/reports/src/ui-catalog-export.js +221 -0
  14. package/dist/schema/src/api.d.ts +55 -0
  15. package/dist/schema/src/api.js +33 -0
  16. package/dist/schema/src/cli-results.d.ts +318 -1
  17. package/dist/schema/src/cli-results.js +89 -1
  18. package/dist/server/src/app.js +3 -1
  19. package/dist/server/src/openapi.d.ts +1 -1
  20. package/dist/server/src/openapi.js +24 -2
  21. package/dist/server/src/recording-verification.d.ts +34 -0
  22. package/dist/server/src/recording-verification.js +118 -0
  23. package/docs/agents.md +5 -0
  24. package/docs/reference/cli.md +56 -1
  25. package/docs/reference/http-api.md +52 -0
  26. package/docs/reference/reports.md +392 -0
  27. package/docs/reference/schema.md +82 -1
  28. package/docs/reference/server.md +39 -0
  29. package/docs/rich-evidence.md +12 -0
  30. package/docs/server.md +12 -0
  31. package/docs/ui-catalog.md +106 -12
  32. package/docs/ui.md +6 -1
  33. package/llms-full.txt +246 -11
  34. package/package.json +1 -1
  35. package/packages/schema/schemas/ui-catalog-diff-result.schema.json +201 -0
  36. package/packages/schema/schemas/ui-catalog-export-result.schema.json +83 -0
  37. package/packages/schema/schemas/ui-catalog-publish-result.schema.json +116 -0
  38. package/packages/schema/schemas/ui-catalog-status.schema.json +197 -0
  39. package/packages/server/openapi.json +527 -1
  40. package/packages/visualizer/src/catalog/Picture.tsx +32 -0
  41. package/packages/visualizer/src/catalog/UiCatalogCompare.tsx +289 -0
  42. package/packages/visualizer/src/catalog/UiCatalogPage.tsx +18 -28
  43. package/packages/visualizer/src/catalog/history.ts +61 -0
  44. package/packages/visualizer/src/catalog/search.ts +14 -3
  45. package/packages/visualizer/src/evidence/evidence-context.tsx +2 -1
  46. package/packages/visualizer/src/explorer/RunsView.tsx +32 -6
  47. package/packages/visualizer/src/explorer/catalog.tsx +24 -1
  48. package/packages/visualizer/src/explorer/recording-verification.ts +58 -0
@@ -7,16 +7,19 @@ thousands of captures can be watched while it happens and split across as many m
7
7
 
8
8
  Nothing about it is new to the report contract. A state is an ordinary check with a screenshot, the expected set is an ordinary
9
9
  layer, and the stream is the one every viewer already holds. This page is the convention that makes those usable as a catalog, the
10
- decisions behind it, and what is not built yet.
10
+ decisions behind it, the command line, and what stays in your project.
11
11
 
12
12
  - [How a state is written](#how-a-state-is-written)
13
13
  - [What is expected](#what-is-expected)
14
14
  - [Publishing from any collector](#publishing-from-any-collector)
15
+ - [The command line](#the-command-line)
15
16
  - [Sharding](#sharding)
16
17
  - [Streaming and loading](#streaming-and-loading)
17
18
  - [Exploring it](#exploring-it)
19
+ - [Comparing two commits or two profiles](#comparing-two-commits-or-two-profiles)
20
+ - [A single file that needs no server](#a-single-file-that-needs-no-server)
18
21
  - [Decisions](#decisions)
19
- - [What stays in your project, and what is not built yet](#what-stays-in-your-project-and-what-is-not-built-yet)
22
+ - [What stays in your project, and what Atlas does not do](#what-stays-in-your-project-and-what-atlas-does-not-do)
20
23
 
21
24
  ## How a state is written
22
25
 
@@ -101,6 +104,35 @@ for a readiness check (bounded), photographs the page and returns the capture fo
101
104
  a `failed` capture that says why (with a picture of what it showed), never an exception that ends the run. Nothing that reads,
102
105
  composes or shows the catalog imports a collection framework, and a test keeps it that way.
103
106
 
107
+ ## The command line
108
+
109
+ `atlas ui-catalog` does the same without a script. Every subcommand prints a JSON document with `--format json`
110
+ ([`atlas ui-catalog --help`](reference/cli.md) lists every flag), and exits `0` it worked, `1` the answer is no, `2` the command was wrong.
111
+
112
+ ```sh
113
+ # The expected set under one profile. The file is {"states":[{"id":"checkout/payment","covers":["payment-form"]},{"id":"settings","inventory":true}]}.
114
+ atlas ui-catalog plan --states plan.json --producer catalog-plan --environment mobile-web --param viewport=390x844 --param scheme=dark
115
+
116
+ # The states one collector or shard captured. "screenshot" names a file beside captures.json.
117
+ atlas ui-catalog publish --captures captures.json --producer web-capture --environment mobile-web --param viewport=390x844 --param scheme=dark --shard 2/8
118
+
119
+ atlas ui-catalog status # exit 1 while anything failed, is missing, is still being captured or could not be read
120
+ atlas ui-catalog diff --from 3f9a2c1 --to 8b41d70 # two commits (the whole id, or at least 7 digits)
121
+ atlas ui-catalog export --out public/catalog.html # one file, no server
122
+ ```
123
+
124
+ `captures.json` is `{"states":[…]}` (or just the list); a state is `{"id","title"?,"status"?,"detail"?,"route"?,"fixture"?,"cohort"?,"area"?,
125
+ "flags"?,"covers"?,"labels"?,"tags"?,"durationMs"?,"screenshot"?}` with `screenshot` a file name or `{"path"|"uri","width"?,"height"?,"contentType"?}`.
126
+ A misspelled field is refused with its path, not ignored. A picture file is read from beside the file and from each `--attach-root`,
127
+ never from a hidden file or a link out. **A `passed` state whose picture cannot be read is published as `failed`, with the reason**: a
128
+ state without its picture is not shown as captured. `--recorded-at` defaults to the file's modification time, so publishing the same
129
+ file twice is the same layer.
130
+
131
+ `status` is the gate: it exits `1` unless the catalog holds a state and nothing needs attention (a failed capture, a state a plan expects
132
+ with nothing captured, a capture still in progress, a backend or layer that could not be read). Without a plan nothing is expected, so
133
+ nothing can be missing: it says so. A skipped cell is counted, not a problem. Schemas: `ui-catalog-publish-result`,
134
+ `ui-catalog-status`, `ui-catalog-diff-result`, `ui-catalog-export-result`.
135
+
104
136
  ## Sharding
105
137
 
106
138
  A shard is **any publisher that does not know the others**: by case, by profile, by area or by machine; Atlas does not care which.
@@ -144,6 +176,47 @@ curl "$ATLAS_SERVER_URL/api/reports/ui-catalog?coverage=failed,missing&profile=
144
176
  `@crvouga/atlas/reports/ui-catalog`), filtered by `q`, `coverage`, `profile`, `area`, `route`, `cohort`, `flag`, `surface` and
145
177
  `producer`, paged with `limit` and `offset`, with an ETag. Its picture `path` is relative to `/api/reports`.
146
178
 
179
+ ## Comparing two commits or two profiles
180
+
181
+ The viewer's **Compare** button on `/catalog` (`?compare=commit&from=<commit>&to=<commit>`, or `?compare=profile&from=<profile>&to=<profile>`,
182
+ so a link reproduces it) and `atlas ui-catalog diff` set one side against the other. Every pair is one state in one profile.
183
+
184
+ | A pair is | When |
185
+ | --- | --- |
186
+ | **Unchanged** | the same coverage and, when captured, the same stored picture |
187
+ | **Picture changed** | captured on both sides, and the picture's bytes (SHA-256) differ. This is not a measure of how many pixels differ |
188
+ | **Coverage changed** | it stands differently (captured against failed, failed against not captured yet…). A capture that did not pass is never compared by picture |
189
+ | **Cannot tell** | captured on both sides, but a picture has no content hash (it is given only by address, or was left out) |
190
+ | **Only after** / **Only before** | one side has no such cell. It may be new, renamed, or the other side's shard may not have run: it is not called a regression |
191
+
192
+ - **Two commits** compares the layers recorded at each commit. A layer replaces the older one of its slot, so the report keeps only the
193
+ newest capture of every state; the comparison therefore reads the layers themselves (the viewer asks for the layer list and the rows of
194
+ the two commits only; the command line reads the stores). Layers recorded from a checkout with uncommitted changes are not a commit's
195
+ own result and are left out. A commit is the whole id or at least 7 digits that name exactly one commit; anything else is an error
196
+ that lists the commits found, never a guess.
197
+ - **Two profiles** sets each state's cell in one profile beside its cell in another (desktop against mobile, light against dark) in
198
+ the report the viewer holds.
199
+ - A layer or backend that could not be read is named, and what it held is unknown, never "unchanged": `diff` exits `1` for it.
200
+ `--fail-on-change` also exits `1` for any difference, for a gate that must stay the same.
201
+ - The filters (words, area, route, cohort, flag, surface, producer) narrow a comparison; coverage does not, because coverage is what is
202
+ compared.
203
+
204
+ ## A single file that needs no server
205
+
206
+ `atlas ui-catalog export --out catalog.html` writes the catalog as **one HTML file**: the pictures embedded as `data:` addresses, the
207
+ manifest embedded as JSON (`<script type="application/json" id="ui-catalog">`), a search box and a coverage filter. Open it from a disk,
208
+ attach it to a ticket or serve it as a file. It is the catalog at the moment it was made, and says so; it does not stream (for that,
209
+ serve the store with `atlas serve` or build the site with `atlas client build`, which shows the same catalog).
210
+
211
+ - **Size.** Hosting platforms cap the size of one file. `--max-mb` (default 32) limits the embedded pictures; pictures past it are listed
212
+ in the page as left out, with the reason, in the order the page shows them, and the manifest stays complete. `--images none` embeds none.
213
+ `--manifest catalog.json` also writes the manifest as its own file.
214
+ - **Safe to open.** The page is generated from data written by test runs, and treats it so: text is escaped, only PNG, JPEG, GIF and WebP
215
+ bytes (by their first bytes, never SVG, never a layer's claimed type) are embedded, a picture given by address is linked only if it is
216
+ `http` or `https`, and a Content-Security-Policy forbids every network request. Without scripts every state is simply shown.
217
+ - It is a generated document, not the viewer: it carries its own small inline style and script because it must work with no build
218
+ and no server, which [the viewer's UI conventions](ui.md) forbid inside the viewer.
219
+
147
220
  ## Decisions
148
221
 
149
222
  The questions the original proposal left open, and how they are answered here. Each is the simplest answer that stays inside the
@@ -161,13 +234,34 @@ The questions the original proposal left open, and how they are answered here. E
161
234
  | What moves | Reading, composing, the viewer, the document and the API, the publish builders, a Playwright capture adapter. Staying with a product: its state declarations, its inventory, its fixtures and its runner. |
162
235
  | Where it lives | `@crvouga/atlas/reports/ui-catalog` (model), `…/ui-catalog-publish` (builders), `…/ui-state` (convention); the viewer's `/catalog`; the server's `/api/reports/ui-catalog`; `@crvouga/atlas/playwright`'s `captureUiState`. |
163
236
  | Exploration capabilities | Search, filters, profile comparison, coverage and surface coverage, failure diagnostics, deep links, manifest export. |
164
- | Static export | The catalog is computed from the layers a viewer holds, so a static export (`atlas client build`) shows it; it just does not stream. |
165
-
166
- ## What stays in your project, and what is not built yet
167
-
168
- **Stays in a project** (it is specific to the product): the list of states and profiles to capture, the inventory of surfaces,
169
- fixtures and test data, the runner that drives the product to each state, and the readiness conditions of each screen.
170
-
171
- **Not built yet:** a command line to list and publish a catalog without a script (use `atlas report publish --checks` today), a
172
- dedicated static single-file export, diff views that compare two commits of a state, and a hosting-platform publish step beyond
173
- `atlas serve`. The first slice makes the catalog explorable, streaming, shardable and independent of how it was collected.
237
+ | Static export | Two ways, neither needs a server. The catalog is computed from the layers a viewer holds, so the static site (`atlas client build`) shows it, without streaming. `atlas ui-catalog export` writes it as one self-contained HTML file with a size budget. |
238
+ | Comparing | Two commits (from the layers recorded at each, because the report keeps only the newest) or two profiles; pictures by content hash, never by a pixel measure; one-sided states are not regressions. |
239
+ | Command line | `atlas ui-catalog plan`, `publish`, `status`, `diff` and `export`, each with `--format json`. Running a collector is not one: that is the product's. |
240
+
241
+ ## What stays in your project, and what Atlas does not do
242
+
243
+ A project that had its own catalog moves each role it had to Atlas, and keeps only what is specific to its product. The eight roles, and
244
+ where each now is:
245
+
246
+ | # | Role | In Atlas | Stays in your project |
247
+ | --- | --- | --- | --- |
248
+ | 1 | Declaration of the states to capture, and the plan of what is expected | The plan format (`ui-plan` layers), `uiPlanLayer`, `atlas ui-catalog plan` | **The list itself**: which states and profiles your product has |
249
+ | 2 | A capture helper, with readiness checks and diagnostics | An optional adapter, `captureUiState` in `@crvouga/atlas/playwright`; any other tool writes the same layer | **The readiness condition of each screen**, and the helper for a framework Atlas has no adapter for |
250
+ | 3 | An evidence store: one atomic record per capture, images by content | Layers and the stores that hold them (`publishChecks`, a directory, S3, a server) | Nothing |
251
+ | 4 | An inventory of the product's surfaces, and reconciliation against it | Reconciliation: `inventory` plan entries and `covers`, the surface coverage table | **The inventory**: the list of surfaces your product has |
252
+ | 5 | A manifest and report builder | `uiCatalog`, the JSON manifest, Markdown, the single HTML file | Nothing |
253
+ | 6 | A viewer: as a single file, as a site, and live | `atlas ui-catalog export`, `atlas client build`, and `/catalog` on `atlas serve` | Nothing |
254
+ | 7 | A command line (list, run, report, publish, watch) | `atlas ui-catalog plan\|publish\|status\|diff\|export`; watching is the live `/catalog` and `atlas report stream` | **Run**: the runner that drives your product to each state |
255
+ | 8 | A publish step to a hosting platform | Not in Atlas, see below | **The deploy step** of your platform |
256
+
257
+ What stays is, in one list: the states and profiles to capture, the inventory of surfaces, the fixtures and test data, the runner that
258
+ drives the product to each state, the readiness condition of each screen, and the deploy step of your hosting platform. Nothing else is
259
+ specific to your product, so a project that installs a release holds none of the reporting and exploring code, only that.
260
+
261
+ **A hosting publish step is not a command, on purpose.** Publishing to a platform needs that platform's credentials and API, which Atlas
262
+ neither holds nor should. Hand the platform what Atlas produces: the `export` file (or a folder of them) for any host that serves
263
+ files, the output of `atlas client build` for a static site, or run `atlas serve` for a live one. Keep the credentials in the CI of the
264
+ project, where they already are.
265
+
266
+ **Not built:** reconciliation against an external inventory beyond `inventory` entries and `covers`; a comparison of pictures by pixels
267
+ (only by content); a comparison fetched from the server (the viewer and the command line read the layers themselves).
package/docs/ui.md CHANGED
@@ -167,6 +167,11 @@ it works without a server. Cards are Mantine `Card`s in a responsive `SimpleGrid
167
167
  incomplete. Waiting follows the loading policy below, and layers still arriving are counted.
168
168
  - Pictures are `loading="lazy"` and only the cards on screen are fetched. The grid is bounded with *Show more*.
169
169
  - The open state shows every profile side by side with the layer, backend, producer, shard, commit and time of each picture.
170
+ - **Compare** (`catalog/UiCatalogCompare.tsx`, `?compare=commit|profile&from&to`) sets two commits or two profiles against each other. Two
171
+ commits need the layers a newer layer replaced, which the held report no longer has, so `catalog/history.ts` reads the layer list and
172
+ the rows of the two commits only while the comparison is open (sharing the rows cache). Say what "changed" means (the stored bytes
173
+ differ, not a pixel measure), never call a one-sided state a regression, and say a layer that could not be read is unknown. The
174
+ coverage filter does not apply to a comparison.
170
175
 
171
176
  ## Reliability
172
177
 
@@ -384,7 +389,7 @@ The narrowly scoped exceptions are:
384
389
  | Area | Screens/interactions | Mantine composition |
385
390
  | --- | --- | --- |
386
391
  | Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
387
- | UI catalog | Captured UI states per profile as cards with coverage badges, filters in the index column, surface coverage, an open state with its profiles side by side and where each picture came from, manifest download | AppShell navbar, SimpleGrid, Card, Image, MultiSelect, TextInput, Drawer, Accordion, Table, Badge, Alert |
392
+ | UI catalog | Captured UI states per profile as cards with coverage badges, filters in the index column, surface coverage, an open state with its profiles side by side and where each picture came from, manifest download, a Compare of two commits or profiles (SegmentedControl, NativeSelect, Switch) | AppShell navbar, SimpleGrid, Card, Image, MultiSelect, TextInput, Drawer, Accordion, Table, Badge, Alert |
388
393
  | Project home | Tests/runs/artifacts/compare/catalog health tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
389
394
  | Test detail | Current outcomes, statistics, duration graph, product latency with budgets and baselines, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
390
395
  | Specs | All six existing kinds, overview/index with title search and folder or declared-domain grouping, Gherkin rules/scenarios/examples with a coverage summary and Read / Watch / Inspect per behavior, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Tabs, Table, CodeHighlightTabs, ResultRow |
package/llms-full.txt CHANGED
@@ -173,6 +173,11 @@ JSON Schema (`@crvouga/atlas/schemas/<name>.schema.json`, draft 2020-12):
173
173
  | `atlas report doctor` | `doctor-result` |
174
174
  | `atlas report status` | `report-status` |
175
175
  | `atlas report verify-recordings` | `verify-recordings-result` |
176
+ | `atlas ui-catalog plan` | `ui-catalog-publish-result` |
177
+ | `atlas ui-catalog publish` | `ui-catalog-publish-result` |
178
+ | `atlas ui-catalog status` | `ui-catalog-status` |
179
+ | `atlas ui-catalog diff` | `ui-catalog-diff-result` |
180
+ | `atlas ui-catalog export` | `ui-catalog-export-result` |
176
181
  | `a command that was wrong (standard error)` | `command-error` |
177
182
  <!-- /generated:json-command-schemas -->
178
183
 
@@ -1493,6 +1498,18 @@ two ways without touching a check's result:
1493
1498
  `--format json` prints the `verify-recordings-result` document. `--layer <id>` and `--limit <n>` narrow the work, since
1494
1499
  it reads blobs. A store that cannot answer is `unreadable`, never verified. Test results are unchanged either way.
1495
1500
 
1501
+ - **In the viewer.** A publish token starts the same read-back on the server with
1502
+ `POST /api/reports/recordings/verification` (body optional: `{ "layers": ["<id prefix>"], "limit": 20 }`; the newest
1503
+ 20 layers by default, at most 200, one read-back at a time; a reader token, a browser sign-in box or an anonymous
1504
+ caller is refused because it reads blobs). Anyone who can read the report then sees the result with
1505
+ `GET /api/reports/recordings/verification` (which reads no blob): per layer, its run, how many videos were read back, how
1506
+ many passed and each gap. The Runs tab and an opened run show `Recording unavailable: 1 of 3 stored videos failed the
1507
+ read-back (1 file gone from storage)` beside the green outcomes, or `3 stored videos verified`, and a run whose stored
1508
+ videos all failed offers no Watch action. A run whose layers were never read back shows nothing: unknown is not
1509
+ verified. The result lives in the server's memory only, so a restart empties it, and a layer that garbage collection
1510
+ removed drops out. An older server has no such route and the viewer then shows nothing. No status, slot or winner is
1511
+ affected; `capabilities.recordingVerification` in `GET /api/server` says a server offers it.
1512
+
1496
1513
  Both are statements about stored bytes. Atlas does not check that a stored video plays in a given browser (a valid header
1497
1514
  can still have an unsupported codec or be truncated), and acknowledging an upload is not playback. Videos given only by
1498
1515
  `uri` are counted as referenced and not checked.
@@ -1508,16 +1525,19 @@ thousands of captures can be watched while it happens and split across as many m
1508
1525
 
1509
1526
  Nothing about it is new to the report contract. A state is an ordinary check with a screenshot, the expected set is an ordinary
1510
1527
  layer, and the stream is the one every viewer already holds. This page is the convention that makes those usable as a catalog, the
1511
- decisions behind it, and what is not built yet.
1528
+ decisions behind it, the command line, and what stays in your project.
1512
1529
 
1513
1530
  - [How a state is written](#how-a-state-is-written)
1514
1531
  - [What is expected](#what-is-expected)
1515
1532
  - [Publishing from any collector](#publishing-from-any-collector)
1533
+ - [The command line](#the-command-line)
1516
1534
  - [Sharding](#sharding)
1517
1535
  - [Streaming and loading](#streaming-and-loading)
1518
1536
  - [Exploring it](#exploring-it)
1537
+ - [Comparing two commits or two profiles](#comparing-two-commits-or-two-profiles)
1538
+ - [A single file that needs no server](#a-single-file-that-needs-no-server)
1519
1539
  - [Decisions](#decisions)
1520
- - [What stays in your project, and what is not built yet](#what-stays-in-your-project-and-what-is-not-built-yet)
1540
+ - [What stays in your project, and what Atlas does not do](#what-stays-in-your-project-and-what-atlas-does-not-do)
1521
1541
 
1522
1542
  ## How a state is written
1523
1543
 
@@ -1602,6 +1622,35 @@ for a readiness check (bounded), photographs the page and returns the capture fo
1602
1622
  a `failed` capture that says why (with a picture of what it showed), never an exception that ends the run. Nothing that reads,
1603
1623
  composes or shows the catalog imports a collection framework, and a test keeps it that way.
1604
1624
 
1625
+ ## The command line
1626
+
1627
+ `atlas ui-catalog` does the same without a script. Every subcommand prints a JSON document with `--format json`
1628
+ ([`atlas ui-catalog --help`](reference/cli.md) lists every flag), and exits `0` it worked, `1` the answer is no, `2` the command was wrong.
1629
+
1630
+ ```sh
1631
+ # The expected set under one profile. The file is {"states":[{"id":"checkout/payment","covers":["payment-form"]},{"id":"settings","inventory":true}]}.
1632
+ atlas ui-catalog plan --states plan.json --producer catalog-plan --environment mobile-web --param viewport=390x844 --param scheme=dark
1633
+
1634
+ # The states one collector or shard captured. "screenshot" names a file beside captures.json.
1635
+ atlas ui-catalog publish --captures captures.json --producer web-capture --environment mobile-web --param viewport=390x844 --param scheme=dark --shard 2/8
1636
+
1637
+ atlas ui-catalog status # exit 1 while anything failed, is missing, is still being captured or could not be read
1638
+ atlas ui-catalog diff --from 3f9a2c1 --to 8b41d70 # two commits (the whole id, or at least 7 digits)
1639
+ atlas ui-catalog export --out public/catalog.html # one file, no server
1640
+ ```
1641
+
1642
+ `captures.json` is `{"states":[…]}` (or just the list); a state is `{"id","title"?,"status"?,"detail"?,"route"?,"fixture"?,"cohort"?,"area"?,
1643
+ "flags"?,"covers"?,"labels"?,"tags"?,"durationMs"?,"screenshot"?}` with `screenshot` a file name or `{"path"|"uri","width"?,"height"?,"contentType"?}`.
1644
+ A misspelled field is refused with its path, not ignored. A picture file is read from beside the file and from each `--attach-root`,
1645
+ never from a hidden file or a link out. **A `passed` state whose picture cannot be read is published as `failed`, with the reason**: a
1646
+ state without its picture is not shown as captured. `--recorded-at` defaults to the file's modification time, so publishing the same
1647
+ file twice is the same layer.
1648
+
1649
+ `status` is the gate: it exits `1` unless the catalog holds a state and nothing needs attention (a failed capture, a state a plan expects
1650
+ with nothing captured, a capture still in progress, a backend or layer that could not be read). Without a plan nothing is expected, so
1651
+ nothing can be missing: it says so. A skipped cell is counted, not a problem. Schemas: `ui-catalog-publish-result`,
1652
+ `ui-catalog-status`, `ui-catalog-diff-result`, `ui-catalog-export-result`.
1653
+
1605
1654
  ## Sharding
1606
1655
 
1607
1656
  A shard is **any publisher that does not know the others**: by case, by profile, by area or by machine; Atlas does not care which.
@@ -1645,6 +1694,47 @@ curl "$ATLAS_SERVER_URL/api/reports/ui-catalog?coverage=failed,missing&profile=
1645
1694
  `@crvouga/atlas/reports/ui-catalog`), filtered by `q`, `coverage`, `profile`, `area`, `route`, `cohort`, `flag`, `surface` and
1646
1695
  `producer`, paged with `limit` and `offset`, with an ETag. Its picture `path` is relative to `/api/reports`.
1647
1696
 
1697
+ ## Comparing two commits or two profiles
1698
+
1699
+ The viewer's **Compare** button on `/catalog` (`?compare=commit&from=<commit>&to=<commit>`, or `?compare=profile&from=<profile>&to=<profile>`,
1700
+ so a link reproduces it) and `atlas ui-catalog diff` set one side against the other. Every pair is one state in one profile.
1701
+
1702
+ | A pair is | When |
1703
+ | --- | --- |
1704
+ | **Unchanged** | the same coverage and, when captured, the same stored picture |
1705
+ | **Picture changed** | captured on both sides, and the picture's bytes (SHA-256) differ. This is not a measure of how many pixels differ |
1706
+ | **Coverage changed** | it stands differently (captured against failed, failed against not captured yet…). A capture that did not pass is never compared by picture |
1707
+ | **Cannot tell** | captured on both sides, but a picture has no content hash (it is given only by address, or was left out) |
1708
+ | **Only after** / **Only before** | one side has no such cell. It may be new, renamed, or the other side's shard may not have run: it is not called a regression |
1709
+
1710
+ - **Two commits** compares the layers recorded at each commit. A layer replaces the older one of its slot, so the report keeps only the
1711
+ newest capture of every state; the comparison therefore reads the layers themselves (the viewer asks for the layer list and the rows of
1712
+ the two commits only; the command line reads the stores). Layers recorded from a checkout with uncommitted changes are not a commit's
1713
+ own result and are left out. A commit is the whole id or at least 7 digits that name exactly one commit; anything else is an error
1714
+ that lists the commits found, never a guess.
1715
+ - **Two profiles** sets each state's cell in one profile beside its cell in another (desktop against mobile, light against dark) in
1716
+ the report the viewer holds.
1717
+ - A layer or backend that could not be read is named, and what it held is unknown, never "unchanged": `diff` exits `1` for it.
1718
+ `--fail-on-change` also exits `1` for any difference, for a gate that must stay the same.
1719
+ - The filters (words, area, route, cohort, flag, surface, producer) narrow a comparison; coverage does not, because coverage is what is
1720
+ compared.
1721
+
1722
+ ## A single file that needs no server
1723
+
1724
+ `atlas ui-catalog export --out catalog.html` writes the catalog as **one HTML file**: the pictures embedded as `data:` addresses, the
1725
+ manifest embedded as JSON (`<script type="application/json" id="ui-catalog">`), a search box and a coverage filter. Open it from a disk,
1726
+ attach it to a ticket or serve it as a file. It is the catalog at the moment it was made, and says so; it does not stream (for that,
1727
+ serve the store with `atlas serve` or build the site with `atlas client build`, which shows the same catalog).
1728
+
1729
+ - **Size.** Hosting platforms cap the size of one file. `--max-mb` (default 32) limits the embedded pictures; pictures past it are listed
1730
+ in the page as left out, with the reason, in the order the page shows them, and the manifest stays complete. `--images none` embeds none.
1731
+ `--manifest catalog.json` also writes the manifest as its own file.
1732
+ - **Safe to open.** The page is generated from data written by test runs, and treats it so: text is escaped, only PNG, JPEG, GIF and WebP
1733
+ bytes (by their first bytes, never SVG, never a layer's claimed type) are embedded, a picture given by address is linked only if it is
1734
+ `http` or `https`, and a Content-Security-Policy forbids every network request. Without scripts every state is simply shown.
1735
+ - It is a generated document, not the viewer: it carries its own small inline style and script because it must work with no build
1736
+ and no server, which [the viewer's UI conventions](ui.md) forbid inside the viewer.
1737
+
1648
1738
  ## Decisions
1649
1739
 
1650
1740
  The questions the original proposal left open, and how they are answered here. Each is the simplest answer that stays inside the
@@ -1662,16 +1752,37 @@ The questions the original proposal left open, and how they are answered here. E
1662
1752
  | What moves | Reading, composing, the viewer, the document and the API, the publish builders, a Playwright capture adapter. Staying with a product: its state declarations, its inventory, its fixtures and its runner. |
1663
1753
  | Where it lives | `@crvouga/atlas/reports/ui-catalog` (model), `…/ui-catalog-publish` (builders), `…/ui-state` (convention); the viewer's `/catalog`; the server's `/api/reports/ui-catalog`; `@crvouga/atlas/playwright`'s `captureUiState`. |
1664
1754
  | Exploration capabilities | Search, filters, profile comparison, coverage and surface coverage, failure diagnostics, deep links, manifest export. |
1665
- | Static export | The catalog is computed from the layers a viewer holds, so a static export (`atlas client build`) shows it; it just does not stream. |
1755
+ | Static export | Two ways, neither needs a server. The catalog is computed from the layers a viewer holds, so the static site (`atlas client build`) shows it, without streaming. `atlas ui-catalog export` writes it as one self-contained HTML file with a size budget. |
1756
+ | Comparing | Two commits (from the layers recorded at each, because the report keeps only the newest) or two profiles; pictures by content hash, never by a pixel measure; one-sided states are not regressions. |
1757
+ | Command line | `atlas ui-catalog plan`, `publish`, `status`, `diff` and `export`, each with `--format json`. Running a collector is not one: that is the product's. |
1666
1758
 
1667
- ## What stays in your project, and what is not built yet
1759
+ ## What stays in your project, and what Atlas does not do
1668
1760
 
1669
- **Stays in a project** (it is specific to the product): the list of states and profiles to capture, the inventory of surfaces,
1670
- fixtures and test data, the runner that drives the product to each state, and the readiness conditions of each screen.
1761
+ A project that had its own catalog moves each role it had to Atlas, and keeps only what is specific to its product. The eight roles, and
1762
+ where each now is:
1671
1763
 
1672
- **Not built yet:** a command line to list and publish a catalog without a script (use `atlas report publish --checks` today), a
1673
- dedicated static single-file export, diff views that compare two commits of a state, and a hosting-platform publish step beyond
1674
- `atlas serve`. The first slice makes the catalog explorable, streaming, shardable and independent of how it was collected.
1764
+ | # | Role | In Atlas | Stays in your project |
1765
+ | --- | --- | --- | --- |
1766
+ | 1 | Declaration of the states to capture, and the plan of what is expected | The plan format (`ui-plan` layers), `uiPlanLayer`, `atlas ui-catalog plan` | **The list itself**: which states and profiles your product has |
1767
+ | 2 | A capture helper, with readiness checks and diagnostics | An optional adapter, `captureUiState` in `@crvouga/atlas/playwright`; any other tool writes the same layer | **The readiness condition of each screen**, and the helper for a framework Atlas has no adapter for |
1768
+ | 3 | An evidence store: one atomic record per capture, images by content | Layers and the stores that hold them (`publishChecks`, a directory, S3, a server) | Nothing |
1769
+ | 4 | An inventory of the product's surfaces, and reconciliation against it | Reconciliation: `inventory` plan entries and `covers`, the surface coverage table | **The inventory**: the list of surfaces your product has |
1770
+ | 5 | A manifest and report builder | `uiCatalog`, the JSON manifest, Markdown, the single HTML file | Nothing |
1771
+ | 6 | A viewer: as a single file, as a site, and live | `atlas ui-catalog export`, `atlas client build`, and `/catalog` on `atlas serve` | Nothing |
1772
+ | 7 | A command line (list, run, report, publish, watch) | `atlas ui-catalog plan\|publish\|status\|diff\|export`; watching is the live `/catalog` and `atlas report stream` | **Run**: the runner that drives your product to each state |
1773
+ | 8 | A publish step to a hosting platform | Not in Atlas, see below | **The deploy step** of your platform |
1774
+
1775
+ What stays is, in one list: the states and profiles to capture, the inventory of surfaces, the fixtures and test data, the runner that
1776
+ drives the product to each state, the readiness condition of each screen, and the deploy step of your hosting platform. Nothing else is
1777
+ specific to your product, so a project that installs a release holds none of the reporting and exploring code, only that.
1778
+
1779
+ **A hosting publish step is not a command, on purpose.** Publishing to a platform needs that platform's credentials and API, which Atlas
1780
+ neither holds nor should. Hand the platform what Atlas produces: the `export` file (or a folder of them) for any host that serves
1781
+ files, the output of `atlas client build` for a static site, or run `atlas serve` for a live one. Keep the credentials in the CI of the
1782
+ project, where they already are.
1783
+
1784
+ **Not built:** reconciliation against an external inventory beyond `inventory` entries and `covers`; a comparison of pictures by pixels
1785
+ (only by content); a comparison fetched from the server (the viewer and the command line read the layers themselves).
1675
1786
  </doc>
1676
1787
 
1677
1788
  <doc path="docs/server.md">
@@ -2015,6 +2126,14 @@ edit, and the server's time. Nothing about the layer, its results or the report'
2015
2126
  See [Tagging artifacts and demos](rich-evidence.md#tagging-artifacts-and-demos) for the tag rules. Reviewer tags are
2016
2127
  shown in the viewer's evidence; the tests API (`artifactTag`) searches the producers' tags.
2017
2128
 
2129
+ ## Reading recordings back
2130
+
2131
+ `POST /api/reports/recordings/verification` (publish, `Authorization: Bearer`) reads the stored videos of the newest layers
2132
+ back and checks them; `GET` (read) returns the newest result per layer without reading any blob, so the viewer can mark a
2133
+ run whose recording was stored and then lost, apart from its test results. The result is in memory only and layers
2134
+ that were never read back are absent (unknown, never "fine"). See
2135
+ [Is the recording still there?](rich-evidence.md#is-the-recording-still-there) for what is checked and what is not.
2136
+
2018
2137
  ## Review collections
2019
2138
 
2020
2139
  A team can publish a named, curated review of a few behaviors: recordings from several explicit executions, notes on
@@ -2194,6 +2313,10 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
2194
2313
  | `GET /api/projects/{project}/reports/artifact-tags` | read | Reviewer tags of attachments: every recorded edit. |
2195
2314
  | `POST /api/reports/artifact-tags` | publish | Tag or untag one attachment after upload. |
2196
2315
  | `POST /api/projects/{project}/reports/artifact-tags` | publish | Tag or untag one attachment after upload. |
2316
+ | `GET /api/reports/recordings/verification` | read | Which stored recordings the server last read back, and which were gone or damaged. |
2317
+ | `GET /api/projects/{project}/reports/recordings/verification` | read | Which stored recordings the server last read back, and which were gone or damaged. |
2318
+ | `POST /api/reports/recordings/verification` | publish | Read stored recordings back and check them (publish token). |
2319
+ | `POST /api/projects/{project}/reports/recordings/verification` | publish | Read stored recordings back and check them (publish token). |
2197
2320
  | `GET /api/reports/review-collections` | read | Review collections: curated sets of recordings, newest first. |
2198
2321
  | `GET /api/projects/{project}/reports/review-collections` | read | Review collections: curated sets of recordings, newest first. |
2199
2322
  | `POST /api/reports/review-collections` | publish | Publish an immutable review collection (or a new revision of one). |
@@ -4624,6 +4747,11 @@ it works without a server. Cards are Mantine `Card`s in a responsive `SimpleGrid
4624
4747
  incomplete. Waiting follows the loading policy below, and layers still arriving are counted.
4625
4748
  - Pictures are `loading="lazy"` and only the cards on screen are fetched. The grid is bounded with *Show more*.
4626
4749
  - The open state shows every profile side by side with the layer, backend, producer, shard, commit and time of each picture.
4750
+ - **Compare** (`catalog/UiCatalogCompare.tsx`, `?compare=commit|profile&from&to`) sets two commits or two profiles against each other. Two
4751
+ commits need the layers a newer layer replaced, which the held report no longer has, so `catalog/history.ts` reads the layer list and
4752
+ the rows of the two commits only while the comparison is open (sharing the rows cache). Say what "changed" means (the stored bytes
4753
+ differ, not a pixel measure), never call a one-sided state a regression, and say a layer that could not be read is unknown. The
4754
+ coverage filter does not apply to a comparison.
4627
4755
 
4628
4756
  ## Reliability
4629
4757
 
@@ -4841,7 +4969,7 @@ The narrowly scoped exceptions are:
4841
4969
  | Area | Screens/interactions | Mantine composition |
4842
4970
  | --- | --- | --- |
4843
4971
  | Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
4844
- | UI catalog | Captured UI states per profile as cards with coverage badges, filters in the index column, surface coverage, an open state with its profiles side by side and where each picture came from, manifest download | AppShell navbar, SimpleGrid, Card, Image, MultiSelect, TextInput, Drawer, Accordion, Table, Badge, Alert |
4972
+ | UI catalog | Captured UI states per profile as cards with coverage badges, filters in the index column, surface coverage, an open state with its profiles side by side and where each picture came from, manifest download, a Compare of two commits or profiles (SegmentedControl, NativeSelect, Switch) | AppShell navbar, SimpleGrid, Card, Image, MultiSelect, TextInput, Drawer, Accordion, Table, Badge, Alert |
4845
4973
  | Project home | Tests/runs/artifacts/compare/catalog health tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
4846
4974
  | Test detail | Current outcomes, statistics, duration graph, product latency with budgets and baselines, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
4847
4975
  | Specs | All six existing kinds, overview/index with title search and folder or declared-domain grouping, Gherkin rules/scenarios/examples with a coverage summary and Read / Watch / Inspect per behavior, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Tabs, Table, CodeHighlightTabs, ResultRow |
@@ -5217,7 +5345,7 @@ carries it. Upgrade (`bun update @crvouga/atlas --latest`) and remove your worka
5217
5345
 
5218
5346
  > Generated from the usage text each command prints (`atlas <command> --help`) by `bun run docs`. Do not edit.
5219
5347
 
5220
- Add `--format json` (alias `--json`) to `init`, `report publish`, `stream`, `merge`, `doctor` and `status` for a JSON document on standard output (schemas in `@crvouga/atlas/schemas`). Exit codes: `0` it worked, `1` it ran and the answer is no, `2` the command was wrong.
5348
+ Add `--format json` (alias `--json`) to `init`, `report publish`, `stream`, `merge`, `doctor`, `status` and every `ui-catalog` command for a JSON document on standard output (schemas in `@crvouga/atlas/schemas`). Exit codes: `0` it worked, `1` it ran and the answer is no, `2` the command was wrong.
5221
5349
 
5222
5350
  ## atlas
5223
5351
 
@@ -5236,6 +5364,7 @@ atlas — executable statecharts
5236
5364
  atlas dev [--port 5180] develop the Atlas server and client on one host
5237
5365
  atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
5238
5366
  atlas report publish|stream|status|merge|doctor|policy … layered evidence for any kind of spec (atlas report --help)
5367
+ atlas ui-catalog plan|publish|status|diff|export … the UI catalog of captured states (atlas ui-catalog --help)
5239
5368
 
5240
5369
  --workers path attempts that run at once (default: one per CPU, capped by the driver)
5241
5370
  --seed only paths that start from this seed ("start" for paths setup starts); repeatable
@@ -5387,6 +5516,60 @@ atlas report — layered evidence for any kind of spec
5387
5516
 
5388
5517
  Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.
5389
5518
  ```
5519
+
5520
+ ## atlas ui-catalog
5521
+
5522
+ ```text
5523
+ atlas ui-catalog — a browsable inventory of captured UI states, from any collector
5524
+
5525
+ atlas ui-catalog plan --states <file.json> --producer <name> [--store <dir|s3://bucket/prefix|https://…>]
5526
+ [--environment <name>] [--param key=value]... [--shard <label>]
5527
+ [--branch <b>] [--sha <s>] [--run-url <u>] [--run-id <id>] [--run-title <t>] [--recorded-at <iso|ms>]
5528
+ atlas ui-catalog publish --captures <file.json> --producer <name> [the same flags as plan]
5529
+ [--attach-root <dir>]... [--max-picture-mb <n>]
5530
+ atlas ui-catalog status [--store <dir|s3://…|id=url>]... [--backends <module>] [filters] [--branch <name>]...
5531
+ [--ignore-modified] [--format text|json|markdown]
5532
+ atlas ui-catalog diff --from <commit> --to <commit> [--store …]... [--backends <module>] [filters]
5533
+ [--fail-on-change] [--format text|json|markdown]
5534
+ atlas ui-catalog export --out <file.html> [--store …]... [--backends <module>] [filters]
5535
+ [--images inline|none] [--max-mb <n>] [--manifest <file.json>] [--title <t>]
5536
+
5537
+ filters (status, diff, export): --q <words> --coverage a,b --profile <key|label>... --area a --route r --cohort c
5538
+ --flag f --surface s --producer p
5539
+
5540
+ plan Write one layer of kind ui-plan: the states (and with "inventory": true, the surfaces) that should exist under
5541
+ this profile. Every entry is not-run, so a plan never counts as a pass. The file is {"states":[{"id":…,
5542
+ "title"?,"route"?,"fixture"?,"cohort"?,"area"?,"flags"?,"covers"?,"inventory"?}]} or just that list.
5543
+ publish Write one layer of kind ui-state: the states one collector or shard captured under this profile. The file is
5544
+ {"states":[{"id":…,"title"?,"status"?:"passed|failed|running|not-run","detail"?,"route"?,"fixture"?,"cohort"?,
5545
+ "area"?,"flags"?,"covers"?,"labels"?,"tags"?,"durationMs"?,"screenshot"?:"file.png"|{"path"|"uri",
5546
+ "width"?,"height"?,"contentType"?}}]}. A picture file is read from beside the file and each --attach-root, never
5547
+ from a hidden file or a link out. A passed state whose picture cannot be read is published as failed, with the
5548
+ reason: a state without its picture is not shown as captured.
5549
+ --environment and each --param key=value are the profile (a viewport, a color scheme, a device): another
5550
+ profile is another result side by side. --shard only describes the layer; it never splits a result.
5551
+ --recorded-at defaults to the modification time of the file, so publishing the same file twice is one layer.
5552
+ status Read every store and print the catalog's summary. Exit 1 unless the catalog holds a state and there is nothing
5553
+ to attend to: a failed capture, a state a plan expects with nothing captured, a capture still in progress, a
5554
+ backend or layer that could not be read. Without a plan nothing is expected, so nothing is missing.
5555
+ --profile takes a profile's key or its label, as printed by status.
5556
+ diff Set the catalog recorded at one commit (--from) against another (--to): each state in each profile is paired
5557
+ with itself and is unchanged, changed in coverage, changed in picture (the stored bytes differ), unverified (a
5558
+ side has no content hash), only in --to (added) or only in --from (removed). A commit is the whole id or at
5559
+ least 7 digits of it. Exit 0 unless a store could not be read, or, with --fail-on-change, anything differs.
5560
+ export Write the catalog as one HTML file with its pictures embedded and its manifest in it: no server, no network.
5561
+ It is the catalog at the moment it was made. --images none leaves the pictures out. --max-mb (default 32) limits
5562
+ the embedded pictures; those past it are listed in the page as left out, and the manifest stays complete.
5563
+ --manifest writes the catalog's JSON manifest too.
5564
+
5565
+ --format json (alias --json) every subcommand prints one JSON document instead of prose
5566
+ (schemas: ui-catalog-publish-result, ui-catalog-status, ui-catalog-diff-result, ui-catalog-export-result).
5567
+ A command that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
5568
+ A store is a directory, s3://bucket/prefix or an Atlas server (https://…), exactly as for atlas report; select a project
5569
+ with --project <slug> or ATLAS_PROJECT. When no --store is given, ATLAS_STORE is used.
5570
+
5571
+ Exit codes: 0 ok, 1 the answer is no (see each command), 2 the command was wrong.
5572
+ ```
5390
5573
  </doc>
5391
5574
 
5392
5575
  <doc path="docs/reference/http-api.md">
@@ -6086,6 +6269,58 @@ Scoped to one project. The same project may be selected on the unprefixed API us
6086
6269
 
6087
6270
  Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `413` Request too large.; `422` Not a tag edit: a bad target, an empty or over-long tag, or nothing to change.
6088
6271
 
6272
+ ### Which stored recordings the server last read back, and which were gone or damaged
6273
+
6274
+ `GET /api/reports/recordings/verification` · role: read
6275
+
6276
+ The newest read-back the server did of stored videos, per layer: how many it checked, how many passed and the `gaps` (a blob that is gone, bytes of another size or hash than were published, or a file that does not begin like its container), each naming the check that holds the video. `run` is the key of the run the layer belongs to (a `RunSummary.key`), so a viewer can mark that run's recordings as stored-then-unavailable beside its test results.
6277
+
6278
+ This route never reads a blob: it returns what a publish token's `POST` found earlier. A layer that was never read back is absent, which means "not verified", not "fine"; the result is kept in the server's memory only, so a restart empties it. Only layers a store still holds are listed (one removed by garbage collection drops out). No status, slot or winner depends on it. It checks stored bytes, not browser playback. Narrow with `?run=` (a run key) and `?layer=` (an id prefix of at least 7 hex digits, repeatable).
6279
+
6280
+ - `run` (query): Only layers of this run (a key from `GET /api/reports/runs`).
6281
+ - `layer` (query): Only layers whose id starts with this prefix. Repeatable or comma-separated.
6282
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
6283
+
6284
+ Responses: `200` The held read-backs.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
6285
+
6286
+ ### Which stored recordings the server last read back, and which were gone or damaged
6287
+
6288
+ `GET /api/projects/{project}/reports/recordings/verification` · role: read
6289
+
6290
+ The newest read-back the server did of stored videos, per layer: how many it checked, how many passed and the `gaps` (a blob that is gone, bytes of another size or hash than were published, or a file that does not begin like its container), each naming the check that holds the video. `run` is the key of the run the layer belongs to (a `RunSummary.key`), so a viewer can mark that run's recordings as stored-then-unavailable beside its test results.
6291
+
6292
+ This route never reads a blob: it returns what a publish token's `POST` found earlier. A layer that was never read back is absent, which means "not verified", not "fine"; the result is kept in the server's memory only, so a restart empties it. Only layers a store still holds are listed (one removed by garbage collection drops out). No status, slot or winner depends on it. It checks stored bytes, not browser playback. Narrow with `?run=` (a run key) and `?layer=` (an id prefix of at least 7 hex digits, repeatable).
6293
+
6294
+ 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.
6295
+
6296
+ - `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.
6297
+ - `run` (query): Only layers of this run (a key from `GET /api/reports/runs`).
6298
+ - `layer` (query): Only layers whose id starts with this prefix. Repeatable or comma-separated.
6299
+
6300
+ Responses: `200` The held read-backs.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
6301
+
6302
+ ### Read stored recordings back and check them (publish token)
6303
+
6304
+ `POST /api/reports/recordings/verification` · role: publish
6305
+
6306
+ Reads the videos of the newest layers (`limit`, default 20, at most 200; or the layers `layers` names) back from storage and checks that each is still stored, still has the size and SHA-256 published, and still begins like its media type's container. This reads blobs, so it needs a publish (or admin) token in a Bearer header; a reader-only token, a browser sign-in box and an anonymous reader are refused. One read-back runs at a time (`409` otherwise). It writes nothing to storage and changes no status. The answer is the same document `GET` returns, with `verified`: how many layers this pass read back.
6307
+
6308
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
6309
+
6310
+ Responses: `200` The read-backs now held.; `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 read-back is already running.; `413` Request too large.; `422` Not a verification request.; `502` A store could not be read.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
6311
+
6312
+ ### Read stored recordings back and check them (publish token)
6313
+
6314
+ `POST /api/projects/{project}/reports/recordings/verification` · role: publish
6315
+
6316
+ Reads the videos of the newest layers (`limit`, default 20, at most 200; or the layers `layers` names) back from storage and checks that each is still stored, still has the size and SHA-256 published, and still begins like its media type's container. This reads blobs, so it needs a publish (or admin) token in a Bearer header; a reader-only token, a browser sign-in box and an anonymous reader are refused. One read-back runs at a time (`409` otherwise). It writes nothing to storage and changes no status. The answer is the same document `GET` returns, with `verified`: how many layers this pass read back.
6317
+
6318
+ 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.
6319
+
6320
+ - `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.
6321
+
6322
+ Responses: `200` The read-backs now held.; `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 read-back is already running.; `413` Request too large.; `422` Not a verification request.; `502` A store could not be read.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
6323
+
6089
6324
  ### Review collections: curated sets of recordings, newest first
6090
6325
 
6091
6326
  `GET /api/reports/review-collections` · role: read
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/atlas",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "Atlas: specs of any kind, reported as layered evidence. Publish checks from any runner (JUnit, CTRF, a script), combine them across nodes, branches, environments and backends, gate CI on the result, and run statecharts against the real app.",
5
5
  "license": "MIT",
6
6
  "type": "module",