@crvouga/atlas 1.20.0 → 1.22.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 (114) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/dist/core/src/cli.d.ts +2 -2
  3. package/dist/core/src/cli.js +7 -1
  4. package/dist/critics/src/catalog-client.d.ts +91 -0
  5. package/dist/critics/src/catalog-client.js +194 -0
  6. package/dist/critics/src/cli-calibrate.d.ts +4 -0
  7. package/dist/critics/src/cli-calibrate.js +211 -0
  8. package/dist/critics/src/cli-common.d.ts +108 -0
  9. package/dist/critics/src/cli-common.js +155 -0
  10. package/dist/critics/src/cli-review.d.ts +4 -0
  11. package/dist/critics/src/cli-review.js +174 -0
  12. package/dist/critics/src/cli-watch.d.ts +4 -0
  13. package/dist/critics/src/cli-watch.js +191 -0
  14. package/dist/critics/src/cli.d.ts +13 -0
  15. package/dist/critics/src/cli.js +490 -0
  16. package/dist/critics/src/index.d.ts +3 -0
  17. package/dist/critics/src/index.js +3 -0
  18. package/dist/critics/src/judging.d.ts +8 -0
  19. package/dist/critics/src/llm.d.ts +120 -0
  20. package/dist/critics/src/llm.js +266 -0
  21. package/dist/critics/src/marks.d.ts +74 -0
  22. package/dist/critics/src/marks.js +76 -0
  23. package/dist/critics/src/registry.d.ts +125 -0
  24. package/dist/critics/src/registry.js +203 -0
  25. package/dist/critics/src/spool.d.ts +32 -0
  26. package/dist/critics/src/spool.js +129 -0
  27. package/dist/critics/src/uiclip.d.ts +82 -0
  28. package/dist/critics/src/uiclip.js +167 -0
  29. package/dist/reports/src/compose.d.ts +23 -1
  30. package/dist/reports/src/compose.js +29 -19
  31. package/dist/reports/src/critique-review.d.ts +111 -0
  32. package/dist/reports/src/critique-review.js +123 -0
  33. package/dist/reports/src/critique-row.d.ts +24 -0
  34. package/dist/reports/src/critique-row.js +39 -0
  35. package/dist/reports/src/evidence-types.d.ts +5 -0
  36. package/dist/reports/src/layer-merge.js +5 -0
  37. package/dist/reports/src/report-cli.d.ts +13 -0
  38. package/dist/reports/src/report-cli.js +6 -4
  39. package/dist/reports/src/report-index.d.ts +6 -0
  40. package/dist/reports/src/report-index.js +12 -0
  41. package/dist/reports/src/ui-catalog-cli.d.ts +10 -0
  42. package/dist/reports/src/ui-catalog-cli.js +391 -0
  43. package/dist/reports/src/ui-catalog-diff.d.ts +104 -0
  44. package/dist/reports/src/ui-catalog-diff.js +130 -0
  45. package/dist/reports/src/ui-catalog-entries.d.ts +147 -0
  46. package/dist/reports/src/ui-catalog-entries.js +190 -0
  47. package/dist/reports/src/ui-catalog-export.d.ts +82 -0
  48. package/dist/reports/src/ui-catalog-export.js +221 -0
  49. package/dist/reports/src/ui-catalog-publish.js +2 -2
  50. package/dist/reports/src/ui-catalog.d.ts +2 -0
  51. package/dist/reports/src/ui-catalog.js +1 -1
  52. package/dist/reports/src/ui-state.d.ts +2 -0
  53. package/dist/reports/src/ui-state.js +2 -2
  54. package/dist/schema/src/api.d.ts +653 -1
  55. package/dist/schema/src/api.js +44 -0
  56. package/dist/schema/src/cli-results.d.ts +991 -1
  57. package/dist/schema/src/cli-results.js +265 -1
  58. package/dist/schema/src/critique.d.ts +31 -0
  59. package/dist/schema/src/critique.js +26 -0
  60. package/dist/schema/src/server-config.d.ts +3 -3
  61. package/dist/server/src/app.js +28 -1
  62. package/dist/server/src/openapi.js +19 -2
  63. package/docs/agents.md +16 -2
  64. package/docs/critics.md +273 -9
  65. package/docs/extending.md +1 -1
  66. package/docs/reference/cli.md +126 -1
  67. package/docs/reference/core.md +1 -1
  68. package/docs/reference/critics.md +1131 -29
  69. package/docs/reference/http-api.md +40 -0
  70. package/docs/reference/reports.md +855 -0
  71. package/docs/reference/schema.md +197 -1
  72. package/docs/report-contract.md +2 -1
  73. package/docs/server.md +2 -0
  74. package/docs/ui-catalog.md +110 -13
  75. package/docs/ui.md +69 -2
  76. package/llms-full.txt +636 -26
  77. package/llms.txt +1 -1
  78. package/package.json +9 -1
  79. package/packages/critics/README.md +1 -0
  80. package/packages/schema/schemas/critic-calibrate-result.schema.json +274 -0
  81. package/packages/schema/schemas/critic-doctor-result.schema.json +74 -0
  82. package/packages/schema/schemas/critic-list-result.schema.json +90 -0
  83. package/packages/schema/schemas/critic-review-result.schema.json +397 -0
  84. package/packages/schema/schemas/critic-run-result.schema.json +249 -0
  85. package/packages/schema/schemas/critic-setup-result.schema.json +34 -0
  86. package/packages/schema/schemas/critic-watch-result.schema.json +211 -0
  87. package/packages/schema/schemas/critique-mark.schema.json +81 -0
  88. package/packages/schema/schemas/effective-delta.schema.json +337 -0
  89. package/packages/schema/schemas/effective-evidence.schema.json +337 -0
  90. package/packages/schema/schemas/layer-rows.schema.json +337 -0
  91. package/packages/schema/schemas/ui-catalog-diff-result.schema.json +201 -0
  92. package/packages/schema/schemas/ui-catalog-export-result.schema.json +83 -0
  93. package/packages/schema/schemas/ui-catalog-publish-result.schema.json +116 -0
  94. package/packages/schema/schemas/ui-catalog-status.schema.json +197 -0
  95. package/packages/server/openapi.json +6755 -4738
  96. package/packages/visualizer/src/app/AppShell.tsx +3 -1
  97. package/packages/visualizer/src/app/router.tsx +20 -7
  98. package/packages/visualizer/src/catalog/Picture.tsx +32 -0
  99. package/packages/visualizer/src/catalog/UiCatalogCompare.tsx +289 -0
  100. package/packages/visualizer/src/catalog/UiCatalogPage.tsx +18 -28
  101. package/packages/visualizer/src/catalog/history.ts +61 -0
  102. package/packages/visualizer/src/catalog/search.ts +14 -3
  103. package/packages/visualizer/src/components/Header.tsx +16 -4
  104. package/packages/visualizer/src/critiques/CritiquesPage.tsx +402 -0
  105. package/packages/visualizer/src/critiques/FindingOverlay.tsx +50 -0
  106. package/packages/visualizer/src/critiques/ranking.ts +54 -0
  107. package/packages/visualizer/src/critiques/search.ts +41 -0
  108. package/packages/visualizer/src/evidence/applicability-view.tsx +11 -7
  109. package/packages/visualizer/src/evidence/behaviors-page.tsx +22 -15
  110. package/packages/visualizer/src/evidence/evidence-context.tsx +2 -1
  111. package/packages/visualizer/src/explorer/Explorer.tsx +5 -0
  112. package/packages/visualizer/src/explorer/TestDetail.tsx +4 -1
  113. package/packages/visualizer/src/explorer/parts.tsx +19 -1
  114. package/packages/visualizer/src/explorer/playlist.tsx +11 -7
package/llms-full.txt CHANGED
@@ -113,6 +113,8 @@ The complete public export map is generated from the package manifest (wildcards
113
113
  @crvouga/atlas/schema
114
114
  @crvouga/atlas/server
115
115
  @crvouga/atlas/critics
116
+ @crvouga/atlas/critics/uiclip
117
+ @crvouga/atlas/critics/llm
116
118
  @crvouga/atlas/reports/*
117
119
  @crvouga/atlas/schemas/*
118
120
  @crvouga/atlas/openapi.json
@@ -160,7 +162,7 @@ and authentication. Keep synthetic failure checks in a scratch store rather than
160
162
  ## What an agent can rely on
161
163
 
162
164
  **Machine-readable output.** Add `--format json` (alias `--json`) to `atlas init`, `atlas report publish`, `stream`,
163
- `merge`, `doctor` and `status`. Each prints one JSON document on standard output that validates against a published
165
+ `merge`, `doctor`, `status` and `verify-recordings`, to `atlas ui-catalog`, and to `atlas critic list`, `setup`, `doctor`, `run`, `watch`, `review` and `calibrate`. Each prints one JSON document on standard output that validates against a published
164
166
  JSON Schema (`@crvouga/atlas/schemas/<name>.schema.json`, draft 2020-12):
165
167
 
166
168
  <!-- generated:json-command-schemas -->
@@ -173,6 +175,18 @@ JSON Schema (`@crvouga/atlas/schemas/<name>.schema.json`, draft 2020-12):
173
175
  | `atlas report doctor` | `doctor-result` |
174
176
  | `atlas report status` | `report-status` |
175
177
  | `atlas report verify-recordings` | `verify-recordings-result` |
178
+ | `atlas critic list` | `critic-list-result` |
179
+ | `atlas critic setup` | `critic-setup-result` |
180
+ | `atlas critic doctor` | `critic-doctor-result` |
181
+ | `atlas critic run` | `critic-run-result` |
182
+ | `atlas critic watch` | `critic-watch-result` |
183
+ | `atlas critic review` | `critic-review-result` |
184
+ | `atlas critic calibrate` | `critic-calibrate-result` |
185
+ | `atlas ui-catalog plan` | `ui-catalog-publish-result` |
186
+ | `atlas ui-catalog publish` | `ui-catalog-publish-result` |
187
+ | `atlas ui-catalog status` | `ui-catalog-status` |
188
+ | `atlas ui-catalog diff` | `ui-catalog-diff-result` |
189
+ | `atlas ui-catalog export` | `ui-catalog-export-result` |
176
190
  | `a command that was wrong (standard error)` | `command-error` |
177
191
  <!-- /generated:json-command-schemas -->
178
192
 
@@ -181,7 +195,7 @@ pass, a store that needs attention, checks that could not be sent, a token that
181
195
  itself was wrong (nothing was written). With `--format json` a wrong command is `{"ok":false,"error":{"message":…,"code":"usage"}}`
182
196
  on standard error.
183
197
 
184
- **No prompts, no interactive steps, no hidden state.** Configuration is flags and environment variables:
198
+ **No prompts, no interactive steps, no hidden state.** (`atlas critic` asks questions only when a person is at a terminal and never with `--non-interactive` or `--format json`.) Configuration is flags and environment variables:
185
199
  `ATLAS_STORE` (a folder, `s3://…`, or the server's `https://…`), `ATLAS_TOKEN` (or `ATLAS_TOKEN_FILE`). A token is
186
200
  a secret of one server and is never sent over plain `http://` beyond this machine.
187
201
  For HTTP stores, `ATLAS_PROJECT` (or `--project <slug>` on report commands) selects the project. Native runner
@@ -1520,16 +1534,19 @@ thousands of captures can be watched while it happens and split across as many m
1520
1534
 
1521
1535
  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
1536
  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.
1537
+ decisions behind it, the command line, and what stays in your project.
1524
1538
 
1525
1539
  - [How a state is written](#how-a-state-is-written)
1526
1540
  - [What is expected](#what-is-expected)
1527
1541
  - [Publishing from any collector](#publishing-from-any-collector)
1542
+ - [The command line](#the-command-line)
1528
1543
  - [Sharding](#sharding)
1529
1544
  - [Streaming and loading](#streaming-and-loading)
1530
1545
  - [Exploring it](#exploring-it)
1546
+ - [Comparing two commits or two profiles](#comparing-two-commits-or-two-profiles)
1547
+ - [A single file that needs no server](#a-single-file-that-needs-no-server)
1531
1548
  - [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)
1549
+ - [What stays in your project, and what Atlas does not do](#what-stays-in-your-project-and-what-atlas-does-not-do)
1533
1550
 
1534
1551
  ## How a state is written
1535
1552
 
@@ -1541,7 +1558,7 @@ A state is one **check** in a layer whose part has `kind` **`ui-state`**.
1541
1558
  | How the capture went | `status`: `passed` is **captured**, `failed` is attempted and failed (say why in `detail`), `running` is being taken, `not-run` is skipped. |
1542
1559
  | The picture | An attachment with `role: "screenshot"` (else the first image), stored in the layer by content or given as an `https` `uri`. `width` and `height` help the viewer lay it out. |
1543
1560
  | The profile | The layer's `environment` (`desktop-web`, `mobile-web`, `ios`, `android`) and `parameters` (`{ "viewport": "390x844", "scheme": "dark" }`). Parameters split a slot, so the same state at another viewport is **another result, side by side**, never a replacement. |
1544
- | Facts the producer declares | The check's `extensions["atlas.ui-state"]`: `route`, `fixture`, `cohort`, `area`, `flags` (a list), `covers` (the product surfaces the state shows). All optional. |
1561
+ | Facts the producer declares | The check's `extensions["atlas.ui-state"]`: `route`, `fixture`, `cohort`, `area`, `kind` (the kind of UI state: `screen`, `overlay`, `loading`, `error`, `empty`… an open vocabulary that [critics](critics.md#reading-the-catalog) judge a state by), `flags` (a list), `covers` (the product surfaces the state shows). All optional. |
1545
1562
  | Facts that describe without splitting | The check's `labels` (`shard`, `browser`) and `tags`. Labels never make a different slot. |
1546
1563
  | Where it came from | The layer's `producer`, `sha`, `branch`, `runUrl`, `freshness`, `recordedAtMs`: shown with every capture, with the layer id and the backend that holds it. |
1547
1564
 
@@ -1614,6 +1631,35 @@ for a readiness check (bounded), photographs the page and returns the capture fo
1614
1631
  a `failed` capture that says why (with a picture of what it showed), never an exception that ends the run. Nothing that reads,
1615
1632
  composes or shows the catalog imports a collection framework, and a test keeps it that way.
1616
1633
 
1634
+ ## The command line
1635
+
1636
+ `atlas ui-catalog` does the same without a script. Every subcommand prints a JSON document with `--format json`
1637
+ ([`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.
1638
+
1639
+ ```sh
1640
+ # The expected set under one profile. The file is {"states":[{"id":"checkout/payment","covers":["payment-form"]},{"id":"settings","inventory":true}]}.
1641
+ atlas ui-catalog plan --states plan.json --producer catalog-plan --environment mobile-web --param viewport=390x844 --param scheme=dark
1642
+
1643
+ # The states one collector or shard captured. "screenshot" names a file beside captures.json.
1644
+ atlas ui-catalog publish --captures captures.json --producer web-capture --environment mobile-web --param viewport=390x844 --param scheme=dark --shard 2/8
1645
+
1646
+ atlas ui-catalog status # exit 1 while anything failed, is missing, is still being captured or could not be read
1647
+ atlas ui-catalog diff --from 3f9a2c1 --to 8b41d70 # two commits (the whole id, or at least 7 digits)
1648
+ atlas ui-catalog export --out public/catalog.html # one file, no server
1649
+ ```
1650
+
1651
+ `captures.json` is `{"states":[…]}` (or just the list); a state is `{"id","title"?,"status"?,"detail"?,"route"?,"fixture"?,"cohort"?,"area"?,
1652
+ "flags"?,"covers"?,"labels"?,"tags"?,"durationMs"?,"screenshot"?}` with `screenshot` a file name or `{"path"|"uri","width"?,"height"?,"contentType"?}`.
1653
+ A misspelled field is refused with its path, not ignored. A picture file is read from beside the file and from each `--attach-root`,
1654
+ 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
1655
+ state without its picture is not shown as captured. `--recorded-at` defaults to the file's modification time, so publishing the same
1656
+ file twice is the same layer.
1657
+
1658
+ `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
1659
+ with nothing captured, a capture still in progress, a backend or layer that could not be read). Without a plan nothing is expected, so
1660
+ nothing can be missing: it says so. A skipped cell is counted, not a problem. Schemas: `ui-catalog-publish-result`,
1661
+ `ui-catalog-status`, `ui-catalog-diff-result`, `ui-catalog-export-result`.
1662
+
1617
1663
  ## Sharding
1618
1664
 
1619
1665
  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.
@@ -1653,10 +1699,54 @@ and time. *Download the manifest* saves the filtered catalog as JSON.
1653
1699
  curl "$ATLAS_SERVER_URL/api/reports/ui-catalog?coverage=failed,missing&profile=…" # JSON; add format=markdown for text
1654
1700
  ```
1655
1701
 
1702
+ A separate process that reviews the pictures (a [critic](critics.md#reading-the-catalog)) reads the same catalog as a flat list at
1703
+ `GET /api/reports/ui-catalog/entries`: cursor-paged, `since` a report version, with the hash of each stored picture.
1704
+
1656
1705
  `GET /api/reports/ui-catalog` answers the same document the viewer computes (`uiCatalog` from
1657
1706
  `@crvouga/atlas/reports/ui-catalog`), filtered by `q`, `coverage`, `profile`, `area`, `route`, `cohort`, `flag`, `surface` and
1658
1707
  `producer`, paged with `limit` and `offset`, with an ETag. Its picture `path` is relative to `/api/reports`.
1659
1708
 
1709
+ ## Comparing two commits or two profiles
1710
+
1711
+ The viewer's **Compare** button on `/catalog` (`?compare=commit&from=<commit>&to=<commit>`, or `?compare=profile&from=<profile>&to=<profile>`,
1712
+ so a link reproduces it) and `atlas ui-catalog diff` set one side against the other. Every pair is one state in one profile.
1713
+
1714
+ | A pair is | When |
1715
+ | --- | --- |
1716
+ | **Unchanged** | the same coverage and, when captured, the same stored picture |
1717
+ | **Picture changed** | captured on both sides, and the picture's bytes (SHA-256) differ. This is not a measure of how many pixels differ |
1718
+ | **Coverage changed** | it stands differently (captured against failed, failed against not captured yet…). A capture that did not pass is never compared by picture |
1719
+ | **Cannot tell** | captured on both sides, but a picture has no content hash (it is given only by address, or was left out) |
1720
+ | **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 |
1721
+
1722
+ - **Two commits** compares the layers recorded at each commit. A layer replaces the older one of its slot, so the report keeps only the
1723
+ newest capture of every state; the comparison therefore reads the layers themselves (the viewer asks for the layer list and the rows of
1724
+ the two commits only; the command line reads the stores). Layers recorded from a checkout with uncommitted changes are not a commit's
1725
+ 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
1726
+ that lists the commits found, never a guess.
1727
+ - **Two profiles** sets each state's cell in one profile beside its cell in another (desktop against mobile, light against dark) in
1728
+ the report the viewer holds.
1729
+ - A layer or backend that could not be read is named, and what it held is unknown, never "unchanged": `diff` exits `1` for it.
1730
+ `--fail-on-change` also exits `1` for any difference, for a gate that must stay the same.
1731
+ - The filters (words, area, route, cohort, flag, surface, producer) narrow a comparison; coverage does not, because coverage is what is
1732
+ compared.
1733
+
1734
+ ## A single file that needs no server
1735
+
1736
+ `atlas ui-catalog export --out catalog.html` writes the catalog as **one HTML file**: the pictures embedded as `data:` addresses, the
1737
+ manifest embedded as JSON (`<script type="application/json" id="ui-catalog">`), a search box and a coverage filter. Open it from a disk,
1738
+ 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,
1739
+ serve the store with `atlas serve` or build the site with `atlas client build`, which shows the same catalog).
1740
+
1741
+ - **Size.** Hosting platforms cap the size of one file. `--max-mb` (default 32) limits the embedded pictures; pictures past it are listed
1742
+ 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.
1743
+ `--manifest catalog.json` also writes the manifest as its own file.
1744
+ - **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
1745
+ 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
1746
+ `http` or `https`, and a Content-Security-Policy forbids every network request. Without scripts every state is simply shown.
1747
+ - It is a generated document, not the viewer: it carries its own small inline style and script because it must work with no build
1748
+ and no server, which [the viewer's UI conventions](ui.md) forbid inside the viewer.
1749
+
1660
1750
  ## Decisions
1661
1751
 
1662
1752
  The questions the original proposal left open, and how they are answered here. Each is the simplest answer that stays inside the
@@ -1674,16 +1764,37 @@ The questions the original proposal left open, and how they are answered here. E
1674
1764
  | 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
1765
  | 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
1766
  | 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. |
1767
+ | 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. |
1768
+ | 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. |
1769
+ | 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
1770
 
1679
- ## What stays in your project, and what is not built yet
1771
+ ## What stays in your project, and what Atlas does not do
1680
1772
 
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.
1773
+ 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
1774
+ where each now is:
1683
1775
 
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.
1776
+ | # | Role | In Atlas | Stays in your project |
1777
+ | --- | --- | --- | --- |
1778
+ | 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 |
1779
+ | 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 |
1780
+ | 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 |
1781
+ | 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 |
1782
+ | 5 | A manifest and report builder | `uiCatalog`, the JSON manifest, Markdown, the single HTML file | Nothing |
1783
+ | 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 |
1784
+ | 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 |
1785
+ | 8 | A publish step to a hosting platform | Not in Atlas, see below | **The deploy step** of your platform |
1786
+
1787
+ What stays is, in one list: the states and profiles to capture, the inventory of surfaces, the fixtures and test data, the runner that
1788
+ drives the product to each state, the readiness condition of each screen, and the deploy step of your hosting platform. Nothing else is
1789
+ specific to your product, so a project that installs a release holds none of the reporting and exploring code, only that.
1790
+
1791
+ **A hosting publish step is not a command, on purpose.** Publishing to a platform needs that platform's credentials and API, which Atlas
1792
+ neither holds nor should. Hand the platform what Atlas produces: the `export` file (or a folder of them) for any host that serves
1793
+ 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
1794
+ project, where they already are.
1795
+
1796
+ **Not built:** reconciliation against an external inventory beyond `inventory` entries and `covers`; a comparison of pictures by pixels
1797
+ (only by content); a comparison fetched from the server (the viewer and the command line read the layers themselves).
1687
1798
  </doc>
1688
1799
 
1689
1800
  <doc path="docs/server.md">
@@ -2234,6 +2345,8 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
2234
2345
  | `GET /api/projects/{project}/reports/artifacts` | read | Recorded results that carry evidence, across the whole history. |
2235
2346
  | `GET /api/reports/ui-catalog` | read | The UI catalog: captured UI states per profile, and what is still expected. |
2236
2347
  | `GET /api/projects/{project}/reports/ui-catalog` | read | The UI catalog: captured UI states per profile, and what is still expected. |
2348
+ | `GET /api/reports/ui-catalog/entries` | read | The UI catalog as a list a critic iterates: captured states with their picture hash and critiques. |
2349
+ | `GET /api/projects/{project}/reports/ui-catalog/entries` | read | The UI catalog as a list a critic iterates: captured states with their picture hash and critiques. |
2237
2350
  | `GET /api/reports/runs/detail` | read | Every test one run recorded. |
2238
2351
  | `GET /api/projects/{project}/reports/runs/detail` | read | Every test one run recorded. |
2239
2352
  | `GET /api/reports/outline` | read | The specs a report is laid over. |
@@ -3048,7 +3161,8 @@ anything else is recorded. A measurement over its allowance is reported and neve
3048
3161
 
3049
3162
  Opinions (parts of kind `critique`, the verdicts of [automated UI reviewers](critics.md)) are never counted as checks,
3050
3163
  even when they are all that was recorded, and are never part of the gate: a low score, or a critic that crashed, leaves
3051
- `atlas report status` as it was. The `status` of a critique check says only whether the critic ran.
3164
+ `atlas report status` as it was. The `status` of a critique check says only whether the critic ran. A person's accept or dismiss of an
3165
+ opinion (`atlas.critique-mark`) is evidence in the same kind of part and has the same properties.
3052
3166
 
3053
3167
  **Freshness never changes a status.** A layer whose spec fingerprint no longer matches, whose
3054
3168
  checkout was modified, or whose related code changed still shows its recorded outcome, with a
@@ -3476,7 +3590,7 @@ Annotation `type`: `note`, `warning`, `deprecation`, `issue`, `accessibility`, `
3476
3590
  | A kind of file Atlas has never seen | Attach it with its media type and a `role`. It is stored and offered for download today. |
3477
3591
  | Structured data of your own | Put it in `extensions` under a name you own. It is kept, and shown as raw data. |
3478
3592
  | A new annotation type, role or step kind | Use it. Open vocabularies need no registration. |
3479
- | An automated reviewer's opinion of a captured UI | A [critique](critics.md): a part of kind `critique` with the `atlas.critique` extension. It is never counted and never gates. |
3593
+ | An automated reviewer's opinion of a captured UI | A [critique](critics.md): a part of kind `critique` with the `atlas.critique` extension. It is never counted and never gates. A person's accept or dismiss of it is the same kind of part with the `atlas.critique-mark` extension. |
3480
3594
  | Something that should be shown well | Ask for a renderer (below). |
3481
3595
  | A field every tool should have | Open an issue. It is added as an optional field, and the rules above guarantee nothing breaks. |
3482
3596
 
@@ -3513,10 +3627,20 @@ is not human review: it never changes whether a report passes.
3513
3627
  - [What is available](#what-is-available)
3514
3628
  - [The critic contract](#the-critic-contract)
3515
3629
  - [A critique is a layer](#a-critique-is-a-layer)
3630
+ - [Reading the catalog](#reading-the-catalog)
3516
3631
  - [Running critics](#running-critics)
3632
+ - [The `atlas critic` command](#the-atlas-critic-command)
3633
+ - [Watching the catalog](#watching-the-catalog)
3634
+ - [Reviewing and marking](#reviewing-and-marking)
3635
+ - [Calibrating](#calibrating)
3636
+ - [Configuring critics](#configuring-critics)
3637
+ - [The built-in UIClip critic](#the-built-in-uiclip-critic)
3638
+ - [The LLM critic and bringing your own AI](#the-llm-critic-and-bringing-your-own-ai)
3639
+ - [When the server goes away](#when-the-server-goes-away)
3517
3640
  - [State-aware judging, calibration and modes](#state-aware-judging-calibration-and-modes)
3518
3641
  - [Authentication and authorization](#authentication-and-authorization)
3519
3642
  - [Credentials](#credentials)
3643
+ - [The viewer: needs polish](#the-viewer-needs-polish)
3520
3644
  - [What is not here yet](#what-is-not-here-yet)
3521
3645
 
3522
3646
  ## What is available
@@ -3526,11 +3650,17 @@ Import `@crvouga/atlas/critics`. It needs no model, no provider SDK and no netwo
3526
3650
  | You want | Use |
3527
3651
  | --- | --- |
3528
3652
  | Define a critic | `defineCritic` (the contract below) |
3653
+ | List, page through, follow and fetch the pictures of the UI catalog | `createCatalogClient` ([below](#reading-the-catalog)) |
3529
3654
  | Judge many images with caching, retries, rate limits, timeouts and budgets | `runCritic` |
3530
3655
  | Store the opinions | `critiqueLayer`, `publishCritiques` (a layer of parts of kind `critique`) |
3531
3656
  | Read them back | `readCritique`, `critiquesOfLayer`, `critiqueCurrency` |
3532
3657
  | Read a score in context, measure a threshold, decide an exit code | `relativeVerdict`, `calibrate`, `gateDecision` |
3533
3658
  | Keep keys out of configuration and logs | `parseCredentialRef`, `resolveCredential`, `assertNoLiteralCredentials`, `createRedactor` |
3659
+ | Keep critiques that could not be sent and send them later | `spoolingStore`, `flushSpool` |
3660
+ | A UIClip critic, an LLM critic over any provider | `@crvouga/atlas/critics/uiclip`, `@crvouga/atlas/critics/llm` |
3661
+ | Run all of it from a terminal or CI | `atlas critic list \| setup \| doctor \| run \| watch \| review \| calibrate` ([below](#the-atlas-critic-command)) |
3662
+ | Record that a person agrees or disagrees with an opinion | `markLayer`, `publishMarks`, `markStanding` ([below](#reviewing-and-marking)) |
3663
+ | Rank states, compare critics | `rankCritiques`, `critiqueAgreement` (`@crvouga/atlas/reports/critique-review`) |
3534
3664
  | Let a token publish critiques and nothing else | the `critic` role (below) |
3535
3665
 
3536
3666
  The JSON Schema of the evidence is [`critique.schema.json`](../packages/schema/schemas/critique.schema.json). The
@@ -3609,6 +3739,226 @@ score does not fail it, and neither does a critic that crashed. A project that w
3609
3739
  with `gateDecision` in `gate` mode, which is computed by the reader from critique data and never changes a check's
3610
3740
  status, a slot or a winner.
3611
3741
 
3742
+ ## Reading the catalog
3743
+
3744
+ A critic reads the [UI catalog](ui-catalog.md) from the server; it never reads a store directly and needs no browser or
3745
+ collection framework. `GET /api/reports/ui-catalog/entries` lists **entries**: one per captured state per profile that has a picture
3746
+ stored in its layer, each with the hash of that picture (`image.sha256`), where its bytes are (`image.path`), its `state` kind
3747
+ (`screen`, `overlay`, `loading`, `error`, `empty`, or `unknown` when the producer declared none: set `kind` in the state's
3748
+ `atlas.ui-state` facts) and the `critiques` already made of it, each marked `current` or `earlier-image`.
3749
+
3750
+ | A critic wants to | It asks |
3751
+ | --- | --- |
3752
+ | Iterate everything, a page at a time | `?limit=` and the opaque `nextCursor` of the previous page as `?cursor=`. Entries are in key order and the cursor names the last key, so each entry is seen once and in the same order |
3753
+ | Resume where it stopped | keep the `version` of the **first** page; later ask `?since=<version>` for the entries whose layer was added or changed since (an entry that is included but not new is harmless: the cache skips an image it has judged). A version the server does not remember is answered with the whole catalog and `since.honored: false` |
3754
+ | Judge only what it has not judged | `?unjudgedBy=<critic>@<version>`: entries with no current critique with a verdict from that critic (a failed critique is retried) |
3755
+ | Narrow | `?state=`, `?area=`, `?profile=`, `?route=`, `?q=` |
3756
+ | Be told something changed | hold `/api/events`; a `report` event carries the new version, then read `?since=`. A missed event costs nothing: the next read covers it |
3757
+ | Get a picture | `GET /api/reports` + `image.path`, the same safe attachment path every viewer uses; check the bytes against `image.sha256` |
3758
+
3759
+ ```ts
3760
+ import { createCatalogClient } from '@crvouga/atlas/critics';
3761
+
3762
+ const catalog = createCatalogClient({ url: 'https://atlas.example.com', token: process.env.ATLAS_CRITIC_TOKEN });
3763
+ const { version, entries } = await catalog.read({ unjudgedBy: { name: 'uiclip', version: '1.0.0' } });
3764
+ for (const entry of entries) judge(entry, await catalog.image(entry)); // image() refuses bytes that are not the hash
3765
+ for await (const batch of catalog.follow({ since: version })) handle(batch.entries); // catch up on connecting and after every drop, none twice
3766
+ ```
3767
+
3768
+ `follow` connects to the event stream, catches up with a `since` read, and then reads again after every `report` event. When the
3769
+ connection drops it reconnects after a pause and the next read starts from the version it holds, so nothing is missed; an entry
3770
+ is handed over once per picture. The token is sent as `Authorization: Bearer` and is never put in an address or an error. The
3771
+ list is a function of the report, so the same layers in any order give the same entries.
3772
+
3773
+ ## The `atlas critic` command
3774
+
3775
+ `atlas critic list | setup | doctor | run | watch | review | calibrate` runs critics on this machine (a laptop, CI, a worker). Every
3776
+ command takes `--format json`; the documents validate against `critic-list-result`, `critic-setup-result`, `critic-doctor-result`,
3777
+ `critic-run-result`, `critic-watch-result`, `critic-review-result` and `critic-calibrate-result`. The full usage is in the
3778
+ [command reference](reference/cli.md).
3779
+
3780
+ ```sh
3781
+ atlas critic list # what is configured, ready, and where images go
3782
+ atlas critic setup uiclip # fetch and verify the model checkpoint (the only download)
3783
+ atlas critic doctor # server, token scope, catalog, model files, credentials, providers
3784
+ atlas critic run --critic uiclip --unjudged # judge what uiclip has not judged yet
3785
+ atlas critic run --critic vision --all --dry-run # how many, and where images would go; sends and publishes nothing
3786
+ atlas critic watch --critic uiclip # a worker: judge new and changed entries as they arrive
3787
+ atlas critic review # the ranked needs-polish list; accept or dismiss what critics found
3788
+ atlas critic calibrate uiclip --good ok/ --bad broken/ --out calibration/uiclip.json
3789
+ ```
3790
+
3791
+ - **Interactive when there is a terminal.** Missing choices (the critic, the server, the scope) are asked for, and a run that
3792
+ sends images to a third party names the destination and the number of images and waits for a yes.
3793
+ - **Never interactive without one** (no terminal, `--non-interactive`, or `--format json`): every choice is a flag, a missing one
3794
+ exits `2` and names the flags, and sending images to a third party needs `--yes`. Nothing is ever prompted for.
3795
+ - **Exit codes.** `0` it worked; `1` the answer is no (a critic failed on an entry, a gate was crossed, `doctor` found a
3796
+ problem); `2` the command was wrong (a missing flag, a gate refused for lack of evidence, a configuration that holds a
3797
+ credential). Stopping at a budget is not a failure.
3798
+ - **`doctor`** reports each problem separately with how to fix it: the server unreachable; the token missing or not accepted; its
3799
+ scope (a `read` token cannot publish, a `publish` or `admin` token can do more than a critic needs, and a server with no
3800
+ tokens is open, so **every caller is an administrator**); a server too old to list the catalog; and, for each configured
3801
+ critic, its model file and checksum, its optional dependencies, its credential, and whether its provider answers.
3802
+ - **`setup uiclip`** downloads the checkpoint only if it is missing, verifies it against the pinned SHA-256 (a bad checksum is
3803
+ an error, a bad download is discarded) and uses a verified file as it is. Judging never downloads anything and refuses a
3804
+ checkpoint that does not match.
3805
+ - **Scope.** `--all`, `--unjudged`, `--since <version>`, or filters (`--area`, `--profile`, `--state`, `--route`, `--q`); at least one
3806
+ is required. A score is read against the project's scores of the same state kind and against the entry's previous picture
3807
+ (`verdict.relative`).
3808
+ - **Cost.** `--max-images`, `--max-tokens`, `--max-cost`, `--concurrency`, `--timeout-ms`; see [running critics](#running-critics).
3809
+ - **Modes.** `--mode report|warn|gate`, `--threshold state=score`, `--calibration file.json`; see
3810
+ [modes](#state-aware-judging-calibration-and-modes). `gate` is refused (exit `2`, before anything is spent) without a calibration of
3811
+ this critic and version with held-out evidence for every state kind in scope.
3812
+
3813
+ ## Watching the catalog
3814
+
3815
+ `atlas critic watch --critic <name>… --server <url>` is `run` as a worker. It follows the catalog (`createCatalogClient(…).follow`): it
3816
+ catches up on connecting, then judges what arrives, and after every announcement and every reconnection it reads again from the version
3817
+ it holds, so it misses no entry and judges none twice. A picture a critic has already judged is skipped by the cache without a call, so
3818
+ starting a watch on a judged catalog costs nothing.
3819
+
3820
+ ```sh
3821
+ atlas critic watch --critic uiclip --critic vision --yes --max-cost 5 # until Ctrl-C, or the money is spent
3822
+ atlas critic watch --critic uiclip --since <version> --state overlay,loading # resume from the version the last watch printed
3823
+ ```
3824
+
3825
+ - **Ends cleanly.** On Ctrl-C (or a termination signal), after `--max-batches n` batches, or when a budget is spent. What it finished
3826
+ is published as it goes; at the end it prints what it did and the report version to resume from (`--since`). A budget
3827
+ (`--max-images`, `--max-tokens`, `--max-cost`) is for the whole watch, not for each batch.
3828
+ - **Same promises as `run`.** Images that leave the machine need `--yes` (or a yes at the terminal, asked once before it starts);
3829
+ critiques the server cannot take are spooled and sent by the next invocation, in order, byte for byte; a score is read against its
3830
+ state kind; a failed entry is recorded with its reason and exits `1` at the end. It reports and never gates: there is no `--mode`.
3831
+ - `--dry-run` reads the catalog once and says what would be judged; nothing is sent or published. The one JSON document
3832
+ (`critic-watch-result`) is printed when the watch ends.
3833
+ - The server never runs a model and never holds a key: the critic runs where `watch` runs, such as a small worker beside CI.
3834
+
3835
+ ## Reviewing and marking
3836
+
3837
+ `atlas critic review` is where people meet the opinions. It lists one critic's entries lowest first (by standing within the state kind
3838
+ when every score has one, as the viewer does), with every finding and the marks already made.
3839
+
3840
+ ```sh
3841
+ atlas critic review --critic vision --limit 10 # the list; with a terminal it then asks about each entry
3842
+ atlas critic review --entry 'home@ios' --accept --finding f1 --note 'Really cramped'
3843
+ atlas critic review --entry 'home@ios' --dismiss # the whole opinion is wrong
3844
+ ```
3845
+
3846
+ - **A mark says whether the UI needs polish.** *Accept*: the opinion is right (the finding is valid). *Dismiss*: it is not. A mark is
3847
+ about a whole opinion, or about one finding (`--finding <id>`, repeatable; a finding without an `id` can only be marked with the
3848
+ whole opinion). With a terminal and no `--entry`, `review` walks the list and asks `[a]ccept, [d]ismiss, [s]kip, [q]uit` for each
3849
+ finding; without one, every choice is a flag.
3850
+ - **A mark is a layer.** It is a check in a part of kind `critique` with the decision in `extensions["atlas.critique-mark"]`
3851
+ ([schema](../packages/schema/schemas/critique-mark.schema.json)), published under the producer `critic.review.<reviewer>`
3852
+ (`--reviewer`, `ATLAS_REVIEWER`, else `reviewer`; a name the reviewer chooses, never an account). That is within what a `critic`-role
3853
+ token may publish, so marking needs no more power than critiquing (a `publish` or `admin` token may too, a `read` token may not, and
3854
+ the server answers `403` and stores nothing). The server may refuse a mark; it never alters one.
3855
+ - **Evidence, never a result.** A mark is in a part of kind `critique`, so it is never counted, never in `atlas report status`, and
3856
+ never changes a status, a slot or a winner. A reviewer holds one mark per critic and finding of an entry: a newer mark of theirs
3857
+ replaces the older, and reviewers who disagree are shown as disagreeing (`disputed`). A mark names the picture it was made about; when
3858
+ the picture changes, the mark is shown as about an earlier picture and counts for nothing now, like the critique it was about.
3859
+ - **Read back** on each entry of `GET /api/reports/ui-catalog/entries` as `marks`, in `atlas critic review --format json`
3860
+ (`critic-review-result`), by `markStanding`, and in the viewer. A server that predates marks sends none, which means none.
3861
+ - **It feeds calibration.** See [calibrating](#calibrating).
3862
+
3863
+ ## Calibrating
3864
+
3865
+ `atlas critic calibrate <name>` measures a critic against labelled examples, per state kind (`calibrate` as a command), so a threshold is
3866
+ chosen from evidence and `gate` mode can be allowed.
3867
+
3868
+ ```sh
3869
+ # Pictures in directories. A subdirectory's name is the state kind of the pictures in it; pictures directly in a directory get --state.
3870
+ atlas critic calibrate uiclip --good examples/ok --bad examples/broken --heldout-good holdout/ok --heldout-bad holdout/broken --out calibration/uiclip.json
3871
+ # Or what reviewers decided: an accepted whole opinion is a bad example, a dismissed one a good example.
3872
+ atlas critic calibrate uiclip --from-marks --server https://atlas.example.com --out calibration/uiclip.json
3873
+ atlas critic run --critic uiclip --all --mode gate --calibration calibration/uiclip.json
3874
+ ```
3875
+
3876
+ - The critic judges the pictures here, with the same consent (`--yes` for a critic that sends images away), timeouts and budgets as `run`;
3877
+ `--dry-run` counts them and says where they would go. Nothing is published: the calibration is a document, with the critic's name,
3878
+ version, model and parameters, that you commit beside the configuration.
3879
+ - It records, per state kind, the sample sizes, how many are bad and good, the threshold chosen, precision and recall, and **whether the
3880
+ figures are in-sample**. A threshold is chosen only on examples that are not held out; `--heldout-*` pictures measure it. Marks are
3881
+ never held out, so figures from marks alone are in-sample.
3882
+ - Examples left out are named with the reason: the critic could not score the picture, reviewers disagree, the marks were about an
3883
+ earlier picture, or the critic judged it as another version. Finding-level marks are not examples.
3884
+ - The result says whether `gate` would accept it (`gate.eligible`) and, if not, what is missing per state kind (at least 10 bad and 10
3885
+ good examples, measured on held-out examples).
3886
+
3887
+ ## Configuring critics
3888
+
3889
+ `atlas critic` reads `atlas.critics.json` (or `--config`). It may be committed: it names a credential by **reference**
3890
+ (`env:NAME`, `file:PATH`) and a literal credential in it is refused, with a message that says where and never repeats it.
3891
+ Paths are relative to the file.
3892
+
3893
+ ```json
3894
+ {
3895
+ "server": { "url": "https://atlas.example.com", "token": "env:ATLAS_CRITIC_TOKEN" },
3896
+ "critics": {
3897
+ "uiclip": { "type": "uiclip", "checkpoint": { "path": ".atlas/models/uiclip.onnx", "sha256": "<64 hex digits you pinned>", "url": "https://models.example.com/uiclip.onnx" } },
3898
+ "vision": { "type": "llm", "baseUrl": "https://llm.example.com/v1", "apiKey": "env:LLM_API_KEY", "model": "<model id>", "rubricFile": "rubric.md" },
3899
+ "claude": { "type": "llm", "provider": "anthropic", "apiKey": "env:ANTHROPIC_API_KEY", "model": "<model id>" },
3900
+ "gemini": { "type": "llm", "provider": "gemini", "apiKey": "env:GEMINI_API_KEY", "model": "<model id>" },
3901
+ "contrast": { "type": "module", "module": "./critics/contrast.ts" }
3902
+ }
3903
+ }
3904
+ ```
3905
+
3906
+ The server is `--server`, `server.url`, `ATLAS_SERVER_URL` or an https `ATLAS_STORE`; the token is `server.token`,
3907
+ `ATLAS_CRITIC_TOKEN` or `ATLAS_TOKEN`, and is refused over plain `http://` beyond this machine. A `module` critic is any
3908
+ file whose default export is a `defineCritic` result (a heuristic, a classic model, a wrapper around your own service), on
3909
+ equal footing with the others; its `name` must equal its key, because the name is the producer of its layers. Critiques are
3910
+ published as `critic.<name>`.
3911
+
3912
+ ## The built-in UIClip critic
3913
+
3914
+ `@crvouga/atlas/critics/uiclip` is a model-based critic that gives one score per image and keeps images on this machine. It is an
3915
+ **adapter**: importing it needs nothing installed, and the model runs in an ONNX Runtime session opened only when a run starts
3916
+ (`loadOnnxRuntime`, which needs the optional `onnxruntime-node` and `sharp` and names whichever is missing). A test or another
3917
+ runtime passes its own `UiclipRuntime` (`score(image)`), so no model is needed to test anything.
3918
+
3919
+ - **Pinned checkpoint.** You pin the SHA-256 of the checkpoint in the configuration. Atlas ships no checkpoint address: the
3920
+ weights' license must be checked before a project downloads them, and that choice is yours.
3921
+ - **`setupUiclip`** is the only place anything is downloaded (https only, hashed as it arrives, discarded on a mismatch);
3922
+ `verifyCheckpoint` refuses a missing, unpinned or different file; inference never downloads.
3923
+ - **Report-only by design.** A single global score cannot say what to fix, and ordinary loading overlays and bottom sheets score
3924
+ poorly. A run reports the score with its standing among the scores of the same state kind, so a loading overlay is compared with
3925
+ other loading overlays; calibrate before reading it as a threshold.
3926
+ - **Not validated against a particular checkpoint.** The preprocessing is CLIP's (resize to 224 by 224, normalise) and the score is
3927
+ the first output; measure the critic on your own labelled examples with `calibrate` before trusting it.
3928
+
3929
+ ## The LLM critic and bringing your own AI
3930
+
3931
+ `@crvouga/atlas/critics/llm` asks a vision-capable model to review an image against a **rubric** and answer with structured
3932
+ findings. It is provider-agnostic: an `LlmProvider` is `complete(request)` plus declared capabilities, so a developer brings their own
3933
+ model and account. Three adapters ship, all over plain `fetch` so there is no SDK to install: `openAiCompatibleProvider` speaks the
3934
+ widely implemented chat-completions protocol (hosted services and local servers alike), `anthropicProvider` the Messages API and
3935
+ `geminiProvider` the Gemini API. In the configuration `provider` is `openai-compatible` (the default, needs `baseUrl`), `anthropic` or
3936
+ `gemini` (their own hosts by default; `apiKey` is required). A provider for anything else is a few lines.
3937
+
3938
+ - The rubric is recorded as a hash. The answer must be one JSON object with a `score` and optional `findings`; anything else is
3939
+ `invalid-output` and no score is invented. A provider's `429` (and Anthropic's `529`, overloaded) becomes a rate limit with its `Retry-After`; a timeout, a refused
3940
+ credential and an unreachable provider are failed critiques with a reason code.
3941
+ - The key is read from its reference when the critic is built, sent only in a request header (`Authorization`, `x-api-key` or `x-goog-api-key`, never in an address), and kept out of every result,
3942
+ progress line, error and JSON document (the runner redacts what it records). Plain `http` is allowed only for a provider on this
3943
+ machine.
3944
+ - **Images may leave the machine.** `describe().sendsImagesTo` is the provider's host, shown by `list`, `doctor` and `run --dry-run`
3945
+ and confirmed before a run that sends images there.
3946
+
3947
+ ```ts
3948
+ import { createLlmCritic, openAiCompatibleProvider } from '@crvouga/atlas/critics/llm';
3949
+
3950
+ const critic = createLlmCritic({ model: 'my-vision-model', rubric: 'Judge spacing and contrast.', provider: openAiCompatibleProvider({ baseUrl: 'https://llm.example.com/v1', apiKey }) });
3951
+ ```
3952
+
3953
+ ## When the server goes away
3954
+
3955
+ A run that cannot reach the server while publishing does not lose its work. The objects of the layer (its files, the layer, its
3956
+ catalog entry) are written to the spool directory (`.atlas/critic-spool`, or `--spool`, or `spool` in the configuration); the run
3957
+ reports how many, and exits normally. The next `atlas critic run` sends them first, **byte for byte what was written and in the
3958
+ order it was written**, and removes each once the server has it. A layer is content-addressed and the store protocol is
3959
+ idempotent, so one that did arrive is not duplicated, and a spool that is empty is not an error. A refusal (`401`, `403`, `409`,
3960
+ `413`, `422`) is not spooled: it will not pass by waiting. `spoolingStore` and `flushSpool` do the same for your own code.
3961
+
3612
3962
  ## Running critics
3613
3963
 
3614
3964
  `runCritic` owns everything around the judgement; a critic only judges one image.
@@ -3677,18 +4027,46 @@ run time (`resolveCredential`) and every message the runner records goes through
3677
4027
  `createRedactor`) that removes them, in their URL-encoded and base64 forms too, and anything shaped like a provider key,
3678
4028
  token or JWT. A credential never reaches a layer, a progress event or an error message.
3679
4029
 
4030
+ ## The viewer: needs polish
4031
+
4032
+ `Needs polish` (`/critiques`, in the spec-type menu once any critic has recorded an opinion) ranks the captured states one critic
4033
+ judged, lowest first, so the likeliest problems are reviewed first. When every ranked score carries its standing within its state
4034
+ kind, that standing ranks them (a loading overlay is not compared with a full screen); otherwise the raw score does, read as
4035
+ higher-is-better. The address holds the critic, the state kind and the open entry.
4036
+
4037
+ - **Opinions, not results.** Critiques have their own place, wording and mark, never a passed or failed mark, and are never in a test
4038
+ count or the gate.
4039
+ - **Anchored to pixels.** A state whose picture changed since it was judged is listed apart as *not judged at the current picture*, its
4040
+ older opinion shown as judging an earlier picture; it is never ranked and never hidden.
4041
+ - **Failures are data.** A critic that could not judge says why (the reason code) and that this says nothing about the picture.
4042
+ - **Detail.** An open entry shows the verdict and its standing, every finding (category, severity, where, why, suggestion; unknown
4043
+ categories and severities are shown as they are) and where the opinion came from: critic, version, model, parameters, the hash of
4044
+ the picture it judged, the layer and the cost.
4045
+ - **Findings on the picture.** An open entry draws each finding that has a region as a numbered box on the picture, in its severity's
4046
+ color and never by color alone: the number matches the findings table, where *Locate* highlights one. Regions in `normalized`
4047
+ (0..1) or `px` (pixels of the stored picture, using the picture's size) units are drawn; a region in another unit, or none, is
4048
+ listed and not drawn. A critique that judged an earlier picture is not drawn on the new one.
4049
+ - **Agreement across critics.** The *Agreement across critics* tab compares each critic's opinion of the picture an entry has now. A
4050
+ critic *flags* a UI when its label says `needs-polish`, when it has findings, or, with neither, when its score is in the lowest quarter
4051
+ of its state kind; a bare score with no standing, or a critic that failed, gives *no view* and never makes critics agree. Entries that
4052
+ every critic flags come first, then those they disagree about; an entry only one critic judged is counted, not compared. It is a
4053
+ reading for people, not a verdict, a status or a gate.
4054
+ - **Marks.** The list and the open entry show what reviewers decided (accepted, dismissed, disputed, not reviewed), per finding and for
4055
+ the whole opinion, with their notes. The viewer only reads: a browser's sign-in box may look but not write, so it names the
4056
+ `atlas critic review` command that marks.
4057
+
3680
4058
  ## What is not here yet
3681
4059
 
3682
- This release delivers the contract, the evidence format, the runner, state-aware judging and calibration, credential
3683
- handling and the scoped `critic` role. Still to come, tracked in [issue 50](https://github.com/crvouga/atlas/issues/50):
4060
+ Out of scope until the open questions of [issue 50](https://github.com/crvouga/atlas/issues/50) are settled, because they need the
4061
+ weights, a license or a network that this repository does not have:
3684
4062
 
3685
- - the **catalog read surface** for critics (cursor pagination, `since`, realtime events, images by hash), which builds on
3686
- the UI catalog of [issue 43](https://github.com/crvouga/atlas/issues/43);
3687
- - the **`atlas critic` command** (`list`, `setup`, `doctor`, `run`, `watch`, `review`, `calibrate`), its interactive mode
3688
- and its `--format json` documents;
3689
- - the **built-in UIClip critic** and the **LLM critic** with provider adapters;
3690
- - the **viewer**: a ranked "needs polish" list, finding overlays and agreement across critics. Until then critiques are
3691
- stored, composed and kept out of test counts, and the viewer lists only test results.
4063
+ - the **UIClip critic's parity** with the in-house package it is meant to absorb: its exact preprocessing and score head, checked
4064
+ against a real checkpoint. No checkpoint address ships until the weights' license is settled, so the pin is yours; measure it
4065
+ on your own examples with `atlas critic calibrate` before trusting a score;
4066
+ - adapters for providers beyond OpenAI-compatible, Anthropic and Gemini, and access by a local command-line agent or a provider's
4067
+ OAuth sign-in (whether a consumer subscription may be used that way is per provider's terms);
4068
+ - heuristic and classic ML critics shipped in the package, a flow-level or before-and-after critic, and server-hosted execution (never
4069
+ planned: the server does not run models).
3692
4070
  </doc>
3693
4071
 
3694
4072
  <doc path="docs/report-backends.md">
@@ -4600,6 +4978,9 @@ same coverage state, revisions and recording in both.
4600
4978
  - The search words (`q`), the coverage filter (`coverage`), the selection (`behavior`) and the expanded state (`workspace`) are in the
4601
4979
  address. Selecting shows the Read / Watch / Inspect panel beside the list (below it on narrow screens), and *Read and watch* in the
4602
4980
  selected-behavior bar is reachable before the list in keyboard order.
4981
+ - The same release controls as a feature page (*Release build*, *commit*, *platform*, and the *Stale or unverified* filter) sit above the list;
4982
+ they are the address (`release`, `releaseSha`, `releasePlatform`, `evidence=stale`), and the selected behavior and the workspace read their
4983
+ evidence against that release.
4603
4984
  - **Read and watch workspace.** Expanding a behavior replaces the index with a page that uses the width: a two-column
4604
4985
  `SimpleGrid` (one column below `md`) with *What should happen* (the steps, an outline's example filled in) beside *Recording* (the
4605
4986
  player, status and provenance, promise and evidence revisions, applicability), the scoped summary below, and Inspect behind a
@@ -4616,6 +4997,39 @@ same coverage state, revisions and recording in both.
4616
4997
  budget and breaches). Absent evidence is stated as unknown, never as good; the test's duration is not product latency, and the
4617
4998
  summary says it describes the checks Atlas holds, not production. The full reliability and latency detail is one link away.
4618
4999
 
5000
+ ## The review destination
5001
+
5002
+ A team asks four connected questions of one project: what behavior it promises (**Read**), what that looks like (**Watch**), what
5003
+ evidence supports it (**Inspect**) and where the gaps are. The viewer answers them in three places that lead to one another, and
5004
+ `tests/review-destination.test.tsx` walks the whole workflow against a real server, one test per acceptance behavior of the
5005
+ tracking issue (#20).
5006
+
5007
+ | Step | Where | Built by |
5008
+ | --- | --- | --- |
5009
+ | Find a promise by approximate words, a domain, a device and recordings; see each in its own coverage state | **Behaviors** (`/behaviors`) | #55, #74, #57, #44 |
5010
+ | Name the build and device to read the evidence against | the applicability controls of Behaviors and of a feature | #61, #73 |
5011
+ | Read what should happen beside the recording, with the scoped outcome, reliability and latency | the Read and watch workspace | #74, #60, #62, #63 |
5012
+ | Find recordings by tag and media type and play them as a labelled playlist | **Artifacts** (`?tab=artifacts`), Watch these N results | #49, #56, #59, #75 |
5013
+ | Go from a clip to its promise (**Read the promise**, also in a test's detail) or to its exact run (**Developer details**) | the playlist | #47, #54 |
5014
+ | See which run and execution a result came from, and which recordings never arrived or are gone | **Runs** (`?tab=runs`) | #45, #46, #48, #70, #72, #76 |
5015
+ | Seek to the step in a recording and share the moment | Timeline, Watch | #68 |
5016
+ | Freeze a selection with notes and gaps | **Collections** | #66 |
5017
+ | Compare two pinned runs | **Compare** | #67 |
5018
+
5019
+ How the pieces stay honest, and what a test in that file holds them to:
5020
+
5021
+ - The four states (not executed, API only, failed with a recording, demonstrated) are told apart in words and never counted as one
5022
+ pass. A recording that was expected and not uploaded, or stored and then lost, is shown beside its green tests, never in place of them.
5023
+ - Behaviors, Runs and Videos and artifacts link to one another (the header of Behaviors and the Specs block of the explorer), and a
5024
+ result in the playlist or in a test's detail links back to the exact check of its feature. Read names the check and no run;
5025
+ Inspect names the run (`layer`), so it never falls back to the latest combined evidence.
5026
+ - The address holds the scope: the release (`release`, `releaseSha`, `releasePlatform`) and `evidence=stale` on Behaviors as on a feature,
5027
+ the filters and the open playlist on Artifacts, the pinned runs on Compare, and the revision of a frozen collection.
5028
+ A frozen collection does not change when newer runs arrive; the filtered view it was frozen from does.
5029
+ - An older producer (no spec fingerprint, no recording expectation, no tags) or an older server (no artifact search, read-back,
5030
+ collections or health route) stays readable. What it cannot say is unknown: the revision is "not recorded", a run is not "complete",
5031
+ and an unreported behavior is "Not executed", never passed.
5032
+
4619
5033
  ## Catalog health
4620
5034
 
4621
5035
  The **Catalog health** tab (`explorer/HealthView.tsx`, `?tab=health`; `@crvouga/atlas/reports/catalog-health`) explains why the Specs
@@ -4648,6 +5062,36 @@ it works without a server. Cards are Mantine `Card`s in a responsive `SimpleGrid
4648
5062
  incomplete. Waiting follows the loading policy below, and layers still arriving are counted.
4649
5063
  - Pictures are `loading="lazy"` and only the cards on screen are fetched. The grid is bounded with *Show more*.
4650
5064
  - The open state shows every profile side by side with the layer, backend, producer, shard, commit and time of each picture.
5065
+ - **Compare** (`catalog/UiCatalogCompare.tsx`, `?compare=commit|profile&from&to`) sets two commits or two profiles against each other. Two
5066
+ commits need the layers a newer layer replaced, which the held report no longer has, so `catalog/history.ts` reads the layer list and
5067
+ the rows of the two commits only while the comparison is open (sharing the rows cache). Say what "changed" means (the stored bytes
5068
+ 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
5069
+ coverage filter does not apply to a comparison.
5070
+
5071
+ ## Needs polish
5072
+
5073
+ `/critiques` (`critiques/`, in the spec-type menu once a critic has recorded an opinion) ranks the captured UI states one critic
5074
+ judged, lowest first ([the guide](critics.md#the-viewer-needs-polish)). It is computed in the page from the layers the viewer already
5075
+ holds (`critiques/ranking.ts`), so a critique appears as its layer arrives.
5076
+
5077
+ - A critique is an opinion: no `StatusMark`, no passed or failed words, never in a test count. Use a Mantine `Badge` with the
5078
+ label the critic gave (an open string), the severity as text, and never a color alone.
5079
+ - A state judged at an earlier picture, or that the critic could not judge, is listed in its own table with the reason and is not
5080
+ ranked. Unknown categories and severities are shown as written.
5081
+ - The critic and the state kind are `NativeSelect`s whose values are the address (`critic`, `kind`, `entry`); the open entry is a
5082
+ `Drawer` with the findings in a `Table` and the provenance in an `Accordion`.
5083
+ - Pictures are `loading="lazy"`. Waiting, an unavailable backend and incomplete layers are named as on the UI catalog.
5084
+ - **Finding overlays** (`critiques/FindingOverlay.tsx`): in the open entry each finding with a region is a numbered box over the picture,
5085
+ positioned with Mantine style props (`pos`, `left`, `top`, `w`, `h` as percentages of the picture, `bd`) and a numbered `Badge`; no
5086
+ `style`, `className` or stylesheet. The number is the finding's row in the table (where a `Button` locates it, `aria-pressed`), so color
5087
+ is never the only signal; the boxes are `aria-hidden` because the table says the same in words, and a `Switch` hides them. A region in
5088
+ an unknown unit, or of a critique that judged an earlier picture, is not drawn.
5089
+ - **Agreement** is a `Tabs` view of the same page (`?view=agreement`, `keepMounted={false}`): a `Table` with one column per critic, each
5090
+ cell a `Badge` in words (*Flags it*, *Does not flag it*, *No view*) with a link to that critic's critique, and the outcome as text.
5091
+ The rule for a stance is stated on the page (`reports/critique-review`); a score without standing or a failure gives no view.
5092
+ - **Marks** are read, never written, by the viewer (a browser sign-in only looks): each finding and the whole opinion show a `Badge` in
5093
+ words (*Accepted by*, *Dismissed by*, *Reviewers disagree*, *Not reviewed*) with the reviewers' notes, and the page names
5094
+ `atlas critic review`. No mark uses a passed or failed word or a status color.
4651
5095
 
4652
5096
  ## Reliability
4653
5097
 
@@ -4736,7 +5180,8 @@ over the same filtered set the page lists, in the same order (`explorer/playlist
4736
5180
  Speed, "Play the next clip when one ends" and "Repeat" are labelled controls. Captions (`text/vtt` attachments) are offered
4737
5181
  as `<track>` elements.
4738
5182
  - The scenario (title, recorded steps, why it failed) sits beside the player with the recording's scope badges
4739
- (paced, scripted, silent, synthetic) and the sentence that the length is the recording's, not production latency. Developer
5183
+ (paced, scripted, silent, synthetic) and the sentence that the length is the recording's, not production latency. **Read the promise** opens the feature on that
5184
+ exact check (the playlist stays in the address, so Back returns to the same clip); Developer
4740
5185
  details opens the test pinned to that run's record and closes the playlist.
4741
5186
 
4742
5187
  ## Steps and moments in a recording
@@ -4865,7 +5310,7 @@ The narrowly scoped exceptions are:
4865
5310
  | Area | Screens/interactions | Mantine composition |
4866
5311
  | --- | --- | --- |
4867
5312
  | 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 |
5313
+ | 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
5314
  | 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
5315
  | 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
5316
  | 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 +5686,7 @@ carries it. Upgrade (`bun update @crvouga/atlas --latest`) and remove your worka
5241
5686
 
5242
5687
  > Generated from the usage text each command prints (`atlas <command> --help`) by `bun run docs`. Do not edit.
5243
5688
 
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.
5689
+ Add `--format json` (alias `--json`) to `init`, `report publish`, `stream`, `merge`, `doctor`, `status`, every `ui-catalog` command and `critic list`, `setup`, `doctor`, `run`, `watch`, `review` and `calibrate` 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
5690
 
5246
5691
  ## atlas
5247
5692
 
@@ -5260,6 +5705,8 @@ atlas — executable statecharts
5260
5705
  atlas dev [--port 5180] develop the Atlas server and client on one host
5261
5706
  atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
5262
5707
  atlas report publish|stream|status|merge|doctor|policy … layered evidence for any kind of spec (atlas report --help)
5708
+ atlas critic list|setup|doctor|run … automated reviewers that mark which UIs need polish (atlas critic --help)
5709
+ atlas ui-catalog plan|publish|status|diff|export … the UI catalog of captured states (atlas ui-catalog --help)
5263
5710
 
5264
5711
  --workers path attempts that run at once (default: one per CPU, capped by the driver)
5265
5712
  --seed only paths that start from this seed ("start" for paths setup starts); repeatable
@@ -5411,6 +5858,129 @@ atlas report — layered evidence for any kind of spec
5411
5858
 
5412
5859
  Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.
5413
5860
  ```
5861
+
5862
+ ## atlas ui-catalog
5863
+
5864
+ ```text
5865
+ atlas ui-catalog — a browsable inventory of captured UI states, from any collector
5866
+
5867
+ atlas ui-catalog plan --states <file.json> --producer <name> [--store <dir|s3://bucket/prefix|https://…>]
5868
+ [--environment <name>] [--param key=value]... [--shard <label>]
5869
+ [--branch <b>] [--sha <s>] [--run-url <u>] [--run-id <id>] [--run-title <t>] [--recorded-at <iso|ms>]
5870
+ atlas ui-catalog publish --captures <file.json> --producer <name> [the same flags as plan]
5871
+ [--attach-root <dir>]... [--max-picture-mb <n>]
5872
+ atlas ui-catalog status [--store <dir|s3://…|id=url>]... [--backends <module>] [filters] [--branch <name>]...
5873
+ [--ignore-modified] [--format text|json|markdown]
5874
+ atlas ui-catalog diff --from <commit> --to <commit> [--store …]... [--backends <module>] [filters]
5875
+ [--fail-on-change] [--format text|json|markdown]
5876
+ atlas ui-catalog export --out <file.html> [--store …]... [--backends <module>] [filters]
5877
+ [--images inline|none] [--max-mb <n>] [--manifest <file.json>] [--title <t>]
5878
+
5879
+ filters (status, diff, export): --q <words> --coverage a,b --profile <key|label>... --area a --route r --cohort c
5880
+ --flag f --surface s --producer p
5881
+
5882
+ plan Write one layer of kind ui-plan: the states (and with "inventory": true, the surfaces) that should exist under
5883
+ this profile. Every entry is not-run, so a plan never counts as a pass. The file is {"states":[{"id":…,
5884
+ "title"?,"route"?,"fixture"?,"cohort"?,"area"?,"kind"?,"flags"?,"covers"?,"inventory"?}]} or just that list.
5885
+ publish Write one layer of kind ui-state: the states one collector or shard captured under this profile. The file is
5886
+ {"states":[{"id":…,"title"?,"status"?:"passed|failed|running|not-run","detail"?,"route"?,"fixture"?,"cohort"?,
5887
+ "area"?,"kind"?,"flags"?,"covers"?,"labels"?,"tags"?,"durationMs"?,"screenshot"?:"file.png"|{"path"|"uri",
5888
+ "width"?,"height"?,"contentType"?}}]}. A picture file is read from beside the file and each --attach-root, never
5889
+ from a hidden file or a link out. A passed state whose picture cannot be read is published as failed, with the
5890
+ reason: a state without its picture is not shown as captured.
5891
+ --environment and each --param key=value are the profile (a viewport, a color scheme, a device): another
5892
+ profile is another result side by side. --shard only describes the layer; it never splits a result.
5893
+ --recorded-at defaults to the modification time of the file, so publishing the same file twice is one layer.
5894
+ status Read every store and print the catalog's summary. Exit 1 unless the catalog holds a state and there is nothing
5895
+ to attend to: a failed capture, a state a plan expects with nothing captured, a capture still in progress, a
5896
+ backend or layer that could not be read. Without a plan nothing is expected, so nothing is missing.
5897
+ --profile takes a profile's key or its label, as printed by status.
5898
+ diff Set the catalog recorded at one commit (--from) against another (--to): each state in each profile is paired
5899
+ with itself and is unchanged, changed in coverage, changed in picture (the stored bytes differ), unverified (a
5900
+ side has no content hash), only in --to (added) or only in --from (removed). A commit is the whole id or at
5901
+ least 7 digits of it. Exit 0 unless a store could not be read, or, with --fail-on-change, anything differs.
5902
+ export Write the catalog as one HTML file with its pictures embedded and its manifest in it: no server, no network.
5903
+ It is the catalog at the moment it was made. --images none leaves the pictures out. --max-mb (default 32) limits
5904
+ the embedded pictures; those past it are listed in the page as left out, and the manifest stays complete.
5905
+ --manifest writes the catalog's JSON manifest too.
5906
+
5907
+ --format json (alias --json) every subcommand prints one JSON document instead of prose
5908
+ (schemas: ui-catalog-publish-result, ui-catalog-status, ui-catalog-diff-result, ui-catalog-export-result).
5909
+ A command that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
5910
+ A store is a directory, s3://bucket/prefix or an Atlas server (https://…), exactly as for atlas report; select a project
5911
+ with --project <slug> or ATLAS_PROJECT. When no --store is given, ATLAS_STORE is used.
5912
+
5913
+ Exit codes: 0 ok, 1 the answer is no (see each command), 2 the command was wrong.
5914
+ ```
5915
+
5916
+ ## atlas critic
5917
+
5918
+ ```text
5919
+ atlas critic — automated reviewers of the UI catalog
5920
+
5921
+ atlas critic list [--config atlas.critics.json] [--format text|json]
5922
+ atlas critic setup <name> [--config atlas.critics.json] [--format text|json]
5923
+ atlas critic doctor [--server <url>] [--critic <name>]... [--config atlas.critics.json] [--format text|json]
5924
+ atlas critic run --critic <name>... ( --all | --unjudged | --since <version> | filters ) [--server <url>]
5925
+ [--area a,b] [--profile key] [--state screen,overlay] [--route r] [--q words]
5926
+ [--dry-run] [--yes] [--non-interactive] [--force]
5927
+ [--max-images n] [--max-tokens n] [--max-cost usd] [--concurrency n] [--timeout-ms n]
5928
+ [--mode report|warn|gate] [--threshold state=score]... [--calibration file.json]
5929
+ [--spool dir] [--project slug] [--config atlas.critics.json] [--format text|json]
5930
+ atlas critic watch --critic <name>... [--server <url>] [--since <version>] [filters] [--yes] [--dry-run]
5931
+ [--max-batches n] [--max-images n] [--max-tokens n] [--max-cost usd] [--concurrency n] [--timeout-ms n]
5932
+ [--spool dir] [--project slug] [--config atlas.critics.json] [--format text|json]
5933
+ atlas critic review [--server <url>] [--critic <name>] [--state kind] [--limit n] [--list] [--non-interactive]
5934
+ [--entry <key> ( --accept | --dismiss ) [--finding <id>]... [--note text] [--reviewer name] [--dry-run]]
5935
+ [--project slug] [--config atlas.critics.json] [--format text|json]
5936
+ atlas critic calibrate <name> ( --good <dir>... --bad <dir>... [--heldout-good <dir>...] [--heldout-bad <dir>...] [--state kind]
5937
+ | --from-marks [--server <url>] [--critic-version v] ) [--out file.json] [--dry-run] [--yes]
5938
+ [--max-images n] [--concurrency n] [--timeout-ms n] [--config atlas.critics.json] [--format text|json]
5939
+
5940
+ A critic is a producer: it runs here (on a laptop, in CI or in a worker), reads the catalog from an Atlas server, and publishes
5941
+ what it thinks as layers of critiques. The server never runs a model and never holds a provider key. A critique is an opinion,
5942
+ not a test result: it never changes whether a report passes.
5943
+
5944
+ Configuration is atlas.critics.json, which may be committed: it names credentials by reference (env:NAME, file:PATH) and a
5945
+ literal credential in it is refused. The server is --server, "server.url" there, ATLAS_SERVER_URL or an https ATLAS_STORE;
5946
+ the token is ATLAS_CRITIC_TOKEN (or ATLAS_TOKEN), never a flag, and is never sent over plain http beyond this machine.
5947
+
5948
+ list The critics available and configured: type, whether each is ready or needs setup, and where it sends images.
5949
+ setup Provision a critic: fetch and verify a model checkpoint against its pinned SHA-256 (a bad checksum is an error), or
5950
+ check that a credential reference resolves. This is the only place anything is downloaded; judging never downloads.
5951
+ doctor Check the server is reachable, the token is accepted and has the scope a critic needs (and warn when the server is
5952
+ open, so every caller is an administrator), that the server lists the catalog, and for each configured critic its
5953
+ model file, dependencies, credential and provider. Each problem is reported separately, with how to fix it.
5954
+ run Judge the entries in the chosen scope. An entry that already has a current critique of the same critic, version,
5955
+ model, parameters and image is skipped without a call; a changed image is judged again. Failures are recorded
5956
+ with a reason code and retried by the next run. Critiques that cannot be sent (the server went away) are kept in
5957
+ the spool directory (default .atlas/critic-spool) and sent, byte for byte, by the next invocation.
5958
+ --dry-run send and publish nothing: say how many entries would be judged or skipped, and where images would go
5959
+ --yes allow sending images to a third party without asking (required without a terminal)
5960
+ --max-images n stop cleanly after n images; --max-tokens and --max-cost likewise, from what critics report
5961
+ --mode report (default): scores are shown and the exit code is 0; warn: also list scores below their threshold;
5962
+ gate: exit 1 when a score is below its state kind's threshold, refused (exit 2) unless a calibration
5963
+ with held-out evidence of this critic and version is given with --calibration
5964
+
5965
+ watch A worker: run in a loop over the catalog. It catches up on connecting, judges new or changed entries as they arrive (a picture
5966
+ already judged is skipped without a call), publishes as it goes, keeps what the server cannot take and sends it next time.
5967
+ It stops on Ctrl-C, after --max-batches batches, or when a budget (for the whole watch) is spent, and prints the version to
5968
+ resume from (--since). It reports; it never gates. --dry-run reads the catalog once and sends and publishes nothing.
5969
+ review The ranked "needs polish" list of one critic (lowest first) with its findings and the marks already made. With a terminal it
5970
+ walks through the list and asks: accept (the opinion is right: the UI needs polish), dismiss (it is not), skip. With
5971
+ --entry and --accept or --dismiss it records one decision, for the whole opinion or for --finding ids. A mark is a layer
5972
+ under critic.review.<reviewer> (--reviewer, ATLAS_REVIEWER); it needs no more power than a critic token and never changes
5973
+ a result, a count or the gate.
5974
+ calibrate Judge labelled pictures (--good and --bad directories; a subdirectory's name is the state kind of the pictures in it) and/or
5975
+ read the whole-opinion marks reviewers made (--from-marks), and measure the critic per state kind: sample sizes, precision,
5976
+ recall, the threshold, and whether the figures are in-sample. --heldout-* pictures are never used to choose the threshold.
5977
+ --out writes the calibration that run --calibration reads. Nothing is published.
5978
+
5979
+ --format json prints one document (schemas: critic-list-result, critic-setup-result, critic-doctor-result, critic-run-result,
5980
+ critic-watch-result, critic-review-result, critic-calibrate-result).
5981
+ A command that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
5982
+ Exit codes: 0 ok, 1 the answer is no (a critic failed, a gate crossed, a problem found), 2 the command was wrong.
5983
+ ```
5414
5984
  </doc>
5415
5985
 
5416
5986
  <doc path="docs/reference/http-api.md">
@@ -6442,6 +7012,46 @@ Scoped to one project. The same project may be selected on the unprefixed API us
6442
7012
 
6443
7013
  Responses: `200` The catalog.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
6444
7014
 
7015
+ ### The UI catalog as a list a critic iterates: captured states with their picture hash and critiques
7016
+
7017
+ `GET /api/reports/ui-catalog/entries` · role: read
7018
+
7019
+ The captured states of the UI catalog, one entry per state per profile that has a picture stored in its layer, in key order, for a process that reviews them (a critic: see the critics guide). Page with `limit` and the opaque `cursor` of the previous page (`nextCursor`): the cursor names the last key, so each entry is seen exactly once and in the same order. An entry carries `image.sha256` (the hash a critique is anchored to) and `image.path` (fetch the bytes from `/api/reports` + path, through the same safe attachment path as every file), its `state` kind, and `critiques`: those already made, newest first, each marked `current` or `earlier-image`. Keep the `version` of the first page; later ask `since=<that version>` to read only the entries whose layer was added or changed since. A `since` the server does not remember is answered with the whole catalog and `since.honored: false`. `unjudgedBy=<critic>@<version>` keeps entries with no current critique with a verdict from that critic (a failed critique is not a judgement). To be told that something changed, hold `/api/events`: a `report` event carries the new version; then read `since`. A critic-role token may read this.
7020
+
7021
+ - `limit` (query): At most this many entries per page (default 100, at most 1000).
7022
+ - `cursor` (query): The `nextCursor` of the previous page. Anything else is `400`.
7023
+ - `since` (query): A report version (`version` of an earlier answer): only entries whose layer was added or changed since. An unknown version is answered with everything.
7024
+ - `unjudgedBy` (query): `name` or `name@version` of a critic: only entries it has no current verdict for.
7025
+ - `q` (query): Words that must all appear in an entry's key, title, route or area.
7026
+ - `area` (query): Areas. Comma-separated.
7027
+ - `profile` (query): Profile keys, as listed in `profile.key`. Comma-separated.
7028
+ - `state` (query): State kinds: `screen`, `overlay`, `loading`, `error`, `empty`, `unknown`… Comma-separated.
7029
+ - `route` (query): Routes. Comma-separated.
7030
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
7031
+
7032
+ Responses: `200` One page of entries.; `304` Unchanged since the `If-None-Match` ETag.; `400` The cursor is not one this server made.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
7033
+
7034
+ ### The UI catalog as a list a critic iterates: captured states with their picture hash and critiques
7035
+
7036
+ `GET /api/projects/{project}/reports/ui-catalog/entries` · role: read
7037
+
7038
+ The captured states of the UI catalog, one entry per state per profile that has a picture stored in its layer, in key order, for a process that reviews them (a critic: see the critics guide). Page with `limit` and the opaque `cursor` of the previous page (`nextCursor`): the cursor names the last key, so each entry is seen exactly once and in the same order. An entry carries `image.sha256` (the hash a critique is anchored to) and `image.path` (fetch the bytes from `/api/reports` + path, through the same safe attachment path as every file), its `state` kind, and `critiques`: those already made, newest first, each marked `current` or `earlier-image`. Keep the `version` of the first page; later ask `since=<that version>` to read only the entries whose layer was added or changed since. A `since` the server does not remember is answered with the whole catalog and `since.honored: false`. `unjudgedBy=<critic>@<version>` keeps entries with no current critique with a verdict from that critic (a failed critique is not a judgement). To be told that something changed, hold `/api/events`: a `report` event carries the new version; then read `since`. A critic-role token may read this.
7039
+
7040
+ 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.
7041
+
7042
+ - `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.
7043
+ - `limit` (query): At most this many entries per page (default 100, at most 1000).
7044
+ - `cursor` (query): The `nextCursor` of the previous page. Anything else is `400`.
7045
+ - `since` (query): A report version (`version` of an earlier answer): only entries whose layer was added or changed since. An unknown version is answered with everything.
7046
+ - `unjudgedBy` (query): `name` or `name@version` of a critic: only entries it has no current verdict for.
7047
+ - `q` (query): Words that must all appear in an entry's key, title, route or area.
7048
+ - `area` (query): Areas. Comma-separated.
7049
+ - `profile` (query): Profile keys, as listed in `profile.key`. Comma-separated.
7050
+ - `state` (query): State kinds: `screen`, `overlay`, `loading`, `error`, `empty`, `unknown`… Comma-separated.
7051
+ - `route` (query): Routes. Comma-separated.
7052
+
7053
+ Responses: `200` One page of entries.; `304` Unchanged since the `If-None-Match` ETag.; `400` The cursor is not one this server made.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
7054
+
6445
7055
  ### Every test one run recorded
6446
7056
 
6447
7057
  `GET /api/reports/runs/detail` · role: read