@crvouga/atlas 1.20.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 (33) hide show
  1. package/CHANGELOG.md +7 -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/cli-results.d.ts +318 -1
  15. package/dist/schema/src/cli-results.js +89 -1
  16. package/docs/agents.md +5 -0
  17. package/docs/reference/cli.md +56 -1
  18. package/docs/reference/reports.md +392 -0
  19. package/docs/reference/schema.md +41 -1
  20. package/docs/ui-catalog.md +106 -12
  21. package/docs/ui.md +6 -1
  22. package/llms-full.txt +170 -11
  23. package/package.json +1 -1
  24. package/packages/schema/schemas/ui-catalog-diff-result.schema.json +201 -0
  25. package/packages/schema/schemas/ui-catalog-export-result.schema.json +83 -0
  26. package/packages/schema/schemas/ui-catalog-publish-result.schema.json +116 -0
  27. package/packages/schema/schemas/ui-catalog-status.schema.json +197 -0
  28. package/packages/visualizer/src/catalog/Picture.tsx +32 -0
  29. package/packages/visualizer/src/catalog/UiCatalogCompare.tsx +289 -0
  30. package/packages/visualizer/src/catalog/UiCatalogPage.tsx +18 -28
  31. package/packages/visualizer/src/catalog/history.ts +61 -0
  32. package/packages/visualizer/src/catalog/search.ts +14 -3
  33. package/packages/visualizer/src/evidence/evidence-context.tsx +2 -1
@@ -576,7 +576,7 @@ of every layer. States are in id order and cells in profile order, so the same l
576
576
 
577
577
  Source: `packages/schema/src/cli-results.ts`
578
578
 
579
- What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings` print on standard output with `--format json` (alias
579
+ What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings`, and `atlas ui-catalog plan`, `publish`, `status`, `diff` and `export`, print on standard output with `--format json` (alias
580
580
  `--json`), and what any `atlas` command prints on standard error for a command that was wrong: one JSON document,
581
581
  so an agent or a script never has to parse prose. (`status` and `policy` already print documents:
582
582
  `ReportStatusSchema`, and an IAM policy.) Each is also published as a JSON Schema.
@@ -686,6 +686,46 @@ export const InitPlanSchema = …
686
686
  What `atlas init --format json` prints: everything found about the project, and everything it would change.
687
687
  Without `--write` nothing is changed. With `--write`, the files are written and `written` says which.
688
688
 
689
+ #### `UiCatalogPublishResultSchema`
690
+
691
+ ```ts
692
+ export const UiCatalogPublishResultSchema = …
693
+ ```
694
+
695
+ What `atlas ui-catalog plan --format json` and `atlas ui-catalog publish --format json` print: the one layer that was written.
696
+
697
+ #### `UiCatalogProblemSchema`
698
+
699
+ ```ts
700
+ export const UiCatalogProblemSchema = …
701
+ ```
702
+
703
+ One thing that keeps a UI catalog from being complete and good.
704
+
705
+ #### `UiCatalogStatusSchema`
706
+
707
+ ```ts
708
+ export const UiCatalogStatusSchema = …
709
+ ```
710
+
711
+ What `atlas ui-catalog status --format json` prints.
712
+
713
+ #### `UiCatalogDiffResultSchema`
714
+
715
+ ```ts
716
+ export const UiCatalogDiffResultSchema = …
717
+ ```
718
+
719
+ What `atlas ui-catalog diff --format json` prints.
720
+
721
+ #### `UiCatalogExportResultSchema`
722
+
723
+ ```ts
724
+ export const UiCatalogExportResultSchema = …
725
+ ```
726
+
727
+ What `atlas ui-catalog export --format json` prints.
728
+
689
729
  #### `CLI_RESULTS`
690
730
 
691
731
  ```ts
@@ -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
 
@@ -1520,16 +1525,19 @@ thousands of captures can be watched while it happens and split across as many m
1520
1525
 
1521
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
1522
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
1523
- decisions behind it, and what is not built yet.
1528
+ decisions behind it, the command line, and what stays in your project.
1524
1529
 
1525
1530
  - [How a state is written](#how-a-state-is-written)
1526
1531
  - [What is expected](#what-is-expected)
1527
1532
  - [Publishing from any collector](#publishing-from-any-collector)
1533
+ - [The command line](#the-command-line)
1528
1534
  - [Sharding](#sharding)
1529
1535
  - [Streaming and loading](#streaming-and-loading)
1530
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)
1531
1539
  - [Decisions](#decisions)
1532
- - [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)
1533
1541
 
1534
1542
  ## How a state is written
1535
1543
 
@@ -1614,6 +1622,35 @@ for a readiness check (bounded), photographs the page and returns the capture fo
1614
1622
  a `failed` capture that says why (with a picture of what it showed), never an exception that ends the run. Nothing that reads,
1615
1623
  composes or shows the catalog imports a collection framework, and a test keeps it that way.
1616
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
+
1617
1654
  ## Sharding
1618
1655
 
1619
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.
@@ -1657,6 +1694,47 @@ curl "$ATLAS_SERVER_URL/api/reports/ui-catalog?coverage=failed,missing&profile=
1657
1694
  `@crvouga/atlas/reports/ui-catalog`), filtered by `q`, `coverage`, `profile`, `area`, `route`, `cohort`, `flag`, `surface` and
1658
1695
  `producer`, paged with `limit` and `offset`, with an ETag. Its picture `path` is relative to `/api/reports`.
1659
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
+
1660
1738
  ## Decisions
1661
1739
 
1662
1740
  The questions the original proposal left open, and how they are answered here. Each is the simplest answer that stays inside the
@@ -1674,16 +1752,37 @@ The questions the original proposal left open, and how they are answered here. E
1674
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. |
1675
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`. |
1676
1754
  | Exploration capabilities | Search, filters, profile comparison, coverage and surface coverage, failure diagnostics, deep links, manifest export. |
1677
- | 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. |
1678
1758
 
1679
- ## What stays in your project, and what is not built yet
1759
+ ## What stays in your project, and what Atlas does not do
1680
1760
 
1681
- **Stays in a project** (it is specific to the product): the list of states and profiles to capture, the inventory of surfaces,
1682
- 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:
1683
1763
 
1684
- **Not built yet:** a command line to list and publish a catalog without a script (use `atlas report publish --checks` today), a
1685
- dedicated static single-file export, diff views that compare two commits of a state, and a hosting-platform publish step beyond
1686
- `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).
1687
1786
  </doc>
1688
1787
 
1689
1788
  <doc path="docs/server.md">
@@ -4648,6 +4747,11 @@ it works without a server. Cards are Mantine `Card`s in a responsive `SimpleGrid
4648
4747
  incomplete. Waiting follows the loading policy below, and layers still arriving are counted.
4649
4748
  - Pictures are `loading="lazy"` and only the cards on screen are fetched. The grid is bounded with *Show more*.
4650
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.
4651
4755
 
4652
4756
  ## Reliability
4653
4757
 
@@ -4865,7 +4969,7 @@ The narrowly scoped exceptions are:
4865
4969
  | Area | Screens/interactions | Mantine composition |
4866
4970
  | --- | --- | --- |
4867
4971
  | Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
4868
- | 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 |
4869
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 |
4870
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 |
4871
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 |
@@ -5241,7 +5345,7 @@ carries it. Upgrade (`bun update @crvouga/atlas --latest`) and remove your worka
5241
5345
 
5242
5346
  > Generated from the usage text each command prints (`atlas <command> --help`) by `bun run docs`. Do not edit.
5243
5347
 
5244
- 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.
5245
5349
 
5246
5350
  ## atlas
5247
5351
 
@@ -5260,6 +5364,7 @@ atlas — executable statecharts
5260
5364
  atlas dev [--port 5180] develop the Atlas server and client on one host
5261
5365
  atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
5262
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)
5263
5368
 
5264
5369
  --workers path attempts that run at once (default: one per CPU, capped by the driver)
5265
5370
  --seed only paths that start from this seed ("start" for paths setup starts); repeatable
@@ -5411,6 +5516,60 @@ atlas report — layered evidence for any kind of spec
5411
5516
 
5412
5517
  Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.
5413
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
+ ```
5414
5573
  </doc>
5415
5574
 
5416
5575
  <doc path="docs/reference/http-api.md">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/atlas",
3
- "version": "1.20.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",