@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/docs/critics.md CHANGED
@@ -12,10 +12,20 @@ is not human review: it never changes whether a report passes.
12
12
  - [What is available](#what-is-available)
13
13
  - [The critic contract](#the-critic-contract)
14
14
  - [A critique is a layer](#a-critique-is-a-layer)
15
+ - [Reading the catalog](#reading-the-catalog)
15
16
  - [Running critics](#running-critics)
17
+ - [The `atlas critic` command](#the-atlas-critic-command)
18
+ - [Watching the catalog](#watching-the-catalog)
19
+ - [Reviewing and marking](#reviewing-and-marking)
20
+ - [Calibrating](#calibrating)
21
+ - [Configuring critics](#configuring-critics)
22
+ - [The built-in UIClip critic](#the-built-in-uiclip-critic)
23
+ - [The LLM critic and bringing your own AI](#the-llm-critic-and-bringing-your-own-ai)
24
+ - [When the server goes away](#when-the-server-goes-away)
16
25
  - [State-aware judging, calibration and modes](#state-aware-judging-calibration-and-modes)
17
26
  - [Authentication and authorization](#authentication-and-authorization)
18
27
  - [Credentials](#credentials)
28
+ - [The viewer: needs polish](#the-viewer-needs-polish)
19
29
  - [What is not here yet](#what-is-not-here-yet)
20
30
 
21
31
  ## What is available
@@ -25,11 +35,17 @@ Import `@crvouga/atlas/critics`. It needs no model, no provider SDK and no netwo
25
35
  | You want | Use |
26
36
  | --- | --- |
27
37
  | Define a critic | `defineCritic` (the contract below) |
38
+ | List, page through, follow and fetch the pictures of the UI catalog | `createCatalogClient` ([below](#reading-the-catalog)) |
28
39
  | Judge many images with caching, retries, rate limits, timeouts and budgets | `runCritic` |
29
40
  | Store the opinions | `critiqueLayer`, `publishCritiques` (a layer of parts of kind `critique`) |
30
41
  | Read them back | `readCritique`, `critiquesOfLayer`, `critiqueCurrency` |
31
42
  | Read a score in context, measure a threshold, decide an exit code | `relativeVerdict`, `calibrate`, `gateDecision` |
32
43
  | Keep keys out of configuration and logs | `parseCredentialRef`, `resolveCredential`, `assertNoLiteralCredentials`, `createRedactor` |
44
+ | Keep critiques that could not be sent and send them later | `spoolingStore`, `flushSpool` |
45
+ | A UIClip critic, an LLM critic over any provider | `@crvouga/atlas/critics/uiclip`, `@crvouga/atlas/critics/llm` |
46
+ | Run all of it from a terminal or CI | `atlas critic list \| setup \| doctor \| run \| watch \| review \| calibrate` ([below](#the-atlas-critic-command)) |
47
+ | Record that a person agrees or disagrees with an opinion | `markLayer`, `publishMarks`, `markStanding` ([below](#reviewing-and-marking)) |
48
+ | Rank states, compare critics | `rankCritiques`, `critiqueAgreement` (`@crvouga/atlas/reports/critique-review`) |
33
49
  | Let a token publish critiques and nothing else | the `critic` role (below) |
34
50
 
35
51
  The JSON Schema of the evidence is [`critique.schema.json`](../packages/schema/schemas/critique.schema.json). The
@@ -108,6 +124,226 @@ score does not fail it, and neither does a critic that crashed. A project that w
108
124
  with `gateDecision` in `gate` mode, which is computed by the reader from critique data and never changes a check's
109
125
  status, a slot or a winner.
110
126
 
127
+ ## Reading the catalog
128
+
129
+ A critic reads the [UI catalog](ui-catalog.md) from the server; it never reads a store directly and needs no browser or
130
+ collection framework. `GET /api/reports/ui-catalog/entries` lists **entries**: one per captured state per profile that has a picture
131
+ stored in its layer, each with the hash of that picture (`image.sha256`), where its bytes are (`image.path`), its `state` kind
132
+ (`screen`, `overlay`, `loading`, `error`, `empty`, or `unknown` when the producer declared none: set `kind` in the state's
133
+ `atlas.ui-state` facts) and the `critiques` already made of it, each marked `current` or `earlier-image`.
134
+
135
+ | A critic wants to | It asks |
136
+ | --- | --- |
137
+ | 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 |
138
+ | 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` |
139
+ | 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) |
140
+ | Narrow | `?state=`, `?area=`, `?profile=`, `?route=`, `?q=` |
141
+ | 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 |
142
+ | Get a picture | `GET /api/reports` + `image.path`, the same safe attachment path every viewer uses; check the bytes against `image.sha256` |
143
+
144
+ ```ts
145
+ import { createCatalogClient } from '@crvouga/atlas/critics';
146
+
147
+ const catalog = createCatalogClient({ url: 'https://atlas.example.com', token: process.env.ATLAS_CRITIC_TOKEN });
148
+ const { version, entries } = await catalog.read({ unjudgedBy: { name: 'uiclip', version: '1.0.0' } });
149
+ for (const entry of entries) judge(entry, await catalog.image(entry)); // image() refuses bytes that are not the hash
150
+ for await (const batch of catalog.follow({ since: version })) handle(batch.entries); // catch up on connecting and after every drop, none twice
151
+ ```
152
+
153
+ `follow` connects to the event stream, catches up with a `since` read, and then reads again after every `report` event. When the
154
+ connection drops it reconnects after a pause and the next read starts from the version it holds, so nothing is missed; an entry
155
+ is handed over once per picture. The token is sent as `Authorization: Bearer` and is never put in an address or an error. The
156
+ list is a function of the report, so the same layers in any order give the same entries.
157
+
158
+ ## The `atlas critic` command
159
+
160
+ `atlas critic list | setup | doctor | run | watch | review | calibrate` runs critics on this machine (a laptop, CI, a worker). Every
161
+ command takes `--format json`; the documents validate against `critic-list-result`, `critic-setup-result`, `critic-doctor-result`,
162
+ `critic-run-result`, `critic-watch-result`, `critic-review-result` and `critic-calibrate-result`. The full usage is in the
163
+ [command reference](reference/cli.md).
164
+
165
+ ```sh
166
+ atlas critic list # what is configured, ready, and where images go
167
+ atlas critic setup uiclip # fetch and verify the model checkpoint (the only download)
168
+ atlas critic doctor # server, token scope, catalog, model files, credentials, providers
169
+ atlas critic run --critic uiclip --unjudged # judge what uiclip has not judged yet
170
+ atlas critic run --critic vision --all --dry-run # how many, and where images would go; sends and publishes nothing
171
+ atlas critic watch --critic uiclip # a worker: judge new and changed entries as they arrive
172
+ atlas critic review # the ranked needs-polish list; accept or dismiss what critics found
173
+ atlas critic calibrate uiclip --good ok/ --bad broken/ --out calibration/uiclip.json
174
+ ```
175
+
176
+ - **Interactive when there is a terminal.** Missing choices (the critic, the server, the scope) are asked for, and a run that
177
+ sends images to a third party names the destination and the number of images and waits for a yes.
178
+ - **Never interactive without one** (no terminal, `--non-interactive`, or `--format json`): every choice is a flag, a missing one
179
+ exits `2` and names the flags, and sending images to a third party needs `--yes`. Nothing is ever prompted for.
180
+ - **Exit codes.** `0` it worked; `1` the answer is no (a critic failed on an entry, a gate was crossed, `doctor` found a
181
+ problem); `2` the command was wrong (a missing flag, a gate refused for lack of evidence, a configuration that holds a
182
+ credential). Stopping at a budget is not a failure.
183
+ - **`doctor`** reports each problem separately with how to fix it: the server unreachable; the token missing or not accepted; its
184
+ scope (a `read` token cannot publish, a `publish` or `admin` token can do more than a critic needs, and a server with no
185
+ tokens is open, so **every caller is an administrator**); a server too old to list the catalog; and, for each configured
186
+ critic, its model file and checksum, its optional dependencies, its credential, and whether its provider answers.
187
+ - **`setup uiclip`** downloads the checkpoint only if it is missing, verifies it against the pinned SHA-256 (a bad checksum is
188
+ an error, a bad download is discarded) and uses a verified file as it is. Judging never downloads anything and refuses a
189
+ checkpoint that does not match.
190
+ - **Scope.** `--all`, `--unjudged`, `--since <version>`, or filters (`--area`, `--profile`, `--state`, `--route`, `--q`); at least one
191
+ is required. A score is read against the project's scores of the same state kind and against the entry's previous picture
192
+ (`verdict.relative`).
193
+ - **Cost.** `--max-images`, `--max-tokens`, `--max-cost`, `--concurrency`, `--timeout-ms`; see [running critics](#running-critics).
194
+ - **Modes.** `--mode report|warn|gate`, `--threshold state=score`, `--calibration file.json`; see
195
+ [modes](#state-aware-judging-calibration-and-modes). `gate` is refused (exit `2`, before anything is spent) without a calibration of
196
+ this critic and version with held-out evidence for every state kind in scope.
197
+
198
+ ## Watching the catalog
199
+
200
+ `atlas critic watch --critic <name>… --server <url>` is `run` as a worker. It follows the catalog (`createCatalogClient(…).follow`): it
201
+ catches up on connecting, then judges what arrives, and after every announcement and every reconnection it reads again from the version
202
+ 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
203
+ starting a watch on a judged catalog costs nothing.
204
+
205
+ ```sh
206
+ atlas critic watch --critic uiclip --critic vision --yes --max-cost 5 # until Ctrl-C, or the money is spent
207
+ atlas critic watch --critic uiclip --since <version> --state overlay,loading # resume from the version the last watch printed
208
+ ```
209
+
210
+ - **Ends cleanly.** On Ctrl-C (or a termination signal), after `--max-batches n` batches, or when a budget is spent. What it finished
211
+ is published as it goes; at the end it prints what it did and the report version to resume from (`--since`). A budget
212
+ (`--max-images`, `--max-tokens`, `--max-cost`) is for the whole watch, not for each batch.
213
+ - **Same promises as `run`.** Images that leave the machine need `--yes` (or a yes at the terminal, asked once before it starts);
214
+ critiques the server cannot take are spooled and sent by the next invocation, in order, byte for byte; a score is read against its
215
+ 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`.
216
+ - `--dry-run` reads the catalog once and says what would be judged; nothing is sent or published. The one JSON document
217
+ (`critic-watch-result`) is printed when the watch ends.
218
+ - The server never runs a model and never holds a key: the critic runs where `watch` runs, such as a small worker beside CI.
219
+
220
+ ## Reviewing and marking
221
+
222
+ `atlas critic review` is where people meet the opinions. It lists one critic's entries lowest first (by standing within the state kind
223
+ when every score has one, as the viewer does), with every finding and the marks already made.
224
+
225
+ ```sh
226
+ atlas critic review --critic vision --limit 10 # the list; with a terminal it then asks about each entry
227
+ atlas critic review --entry 'home@ios' --accept --finding f1 --note 'Really cramped'
228
+ atlas critic review --entry 'home@ios' --dismiss # the whole opinion is wrong
229
+ ```
230
+
231
+ - **A mark says whether the UI needs polish.** *Accept*: the opinion is right (the finding is valid). *Dismiss*: it is not. A mark is
232
+ about a whole opinion, or about one finding (`--finding <id>`, repeatable; a finding without an `id` can only be marked with the
233
+ whole opinion). With a terminal and no `--entry`, `review` walks the list and asks `[a]ccept, [d]ismiss, [s]kip, [q]uit` for each
234
+ finding; without one, every choice is a flag.
235
+ - **A mark is a layer.** It is a check in a part of kind `critique` with the decision in `extensions["atlas.critique-mark"]`
236
+ ([schema](../packages/schema/schemas/critique-mark.schema.json)), published under the producer `critic.review.<reviewer>`
237
+ (`--reviewer`, `ATLAS_REVIEWER`, else `reviewer`; a name the reviewer chooses, never an account). That is within what a `critic`-role
238
+ token may publish, so marking needs no more power than critiquing (a `publish` or `admin` token may too, a `read` token may not, and
239
+ the server answers `403` and stores nothing). The server may refuse a mark; it never alters one.
240
+ - **Evidence, never a result.** A mark is in a part of kind `critique`, so it is never counted, never in `atlas report status`, and
241
+ 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
242
+ replaces the older, and reviewers who disagree are shown as disagreeing (`disputed`). A mark names the picture it was made about; when
243
+ the picture changes, the mark is shown as about an earlier picture and counts for nothing now, like the critique it was about.
244
+ - **Read back** on each entry of `GET /api/reports/ui-catalog/entries` as `marks`, in `atlas critic review --format json`
245
+ (`critic-review-result`), by `markStanding`, and in the viewer. A server that predates marks sends none, which means none.
246
+ - **It feeds calibration.** See [calibrating](#calibrating).
247
+
248
+ ## Calibrating
249
+
250
+ `atlas critic calibrate <name>` measures a critic against labelled examples, per state kind (`calibrate` as a command), so a threshold is
251
+ chosen from evidence and `gate` mode can be allowed.
252
+
253
+ ```sh
254
+ # Pictures in directories. A subdirectory's name is the state kind of the pictures in it; pictures directly in a directory get --state.
255
+ atlas critic calibrate uiclip --good examples/ok --bad examples/broken --heldout-good holdout/ok --heldout-bad holdout/broken --out calibration/uiclip.json
256
+ # Or what reviewers decided: an accepted whole opinion is a bad example, a dismissed one a good example.
257
+ atlas critic calibrate uiclip --from-marks --server https://atlas.example.com --out calibration/uiclip.json
258
+ atlas critic run --critic uiclip --all --mode gate --calibration calibration/uiclip.json
259
+ ```
260
+
261
+ - The critic judges the pictures here, with the same consent (`--yes` for a critic that sends images away), timeouts and budgets as `run`;
262
+ `--dry-run` counts them and says where they would go. Nothing is published: the calibration is a document, with the critic's name,
263
+ version, model and parameters, that you commit beside the configuration.
264
+ - It records, per state kind, the sample sizes, how many are bad and good, the threshold chosen, precision and recall, and **whether the
265
+ figures are in-sample**. A threshold is chosen only on examples that are not held out; `--heldout-*` pictures measure it. Marks are
266
+ never held out, so figures from marks alone are in-sample.
267
+ - Examples left out are named with the reason: the critic could not score the picture, reviewers disagree, the marks were about an
268
+ earlier picture, or the critic judged it as another version. Finding-level marks are not examples.
269
+ - 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
270
+ good examples, measured on held-out examples).
271
+
272
+ ## Configuring critics
273
+
274
+ `atlas critic` reads `atlas.critics.json` (or `--config`). It may be committed: it names a credential by **reference**
275
+ (`env:NAME`, `file:PATH`) and a literal credential in it is refused, with a message that says where and never repeats it.
276
+ Paths are relative to the file.
277
+
278
+ ```json
279
+ {
280
+ "server": { "url": "https://atlas.example.com", "token": "env:ATLAS_CRITIC_TOKEN" },
281
+ "critics": {
282
+ "uiclip": { "type": "uiclip", "checkpoint": { "path": ".atlas/models/uiclip.onnx", "sha256": "<64 hex digits you pinned>", "url": "https://models.example.com/uiclip.onnx" } },
283
+ "vision": { "type": "llm", "baseUrl": "https://llm.example.com/v1", "apiKey": "env:LLM_API_KEY", "model": "<model id>", "rubricFile": "rubric.md" },
284
+ "claude": { "type": "llm", "provider": "anthropic", "apiKey": "env:ANTHROPIC_API_KEY", "model": "<model id>" },
285
+ "gemini": { "type": "llm", "provider": "gemini", "apiKey": "env:GEMINI_API_KEY", "model": "<model id>" },
286
+ "contrast": { "type": "module", "module": "./critics/contrast.ts" }
287
+ }
288
+ }
289
+ ```
290
+
291
+ The server is `--server`, `server.url`, `ATLAS_SERVER_URL` or an https `ATLAS_STORE`; the token is `server.token`,
292
+ `ATLAS_CRITIC_TOKEN` or `ATLAS_TOKEN`, and is refused over plain `http://` beyond this machine. A `module` critic is any
293
+ file whose default export is a `defineCritic` result (a heuristic, a classic model, a wrapper around your own service), on
294
+ equal footing with the others; its `name` must equal its key, because the name is the producer of its layers. Critiques are
295
+ published as `critic.<name>`.
296
+
297
+ ## The built-in UIClip critic
298
+
299
+ `@crvouga/atlas/critics/uiclip` is a model-based critic that gives one score per image and keeps images on this machine. It is an
300
+ **adapter**: importing it needs nothing installed, and the model runs in an ONNX Runtime session opened only when a run starts
301
+ (`loadOnnxRuntime`, which needs the optional `onnxruntime-node` and `sharp` and names whichever is missing). A test or another
302
+ runtime passes its own `UiclipRuntime` (`score(image)`), so no model is needed to test anything.
303
+
304
+ - **Pinned checkpoint.** You pin the SHA-256 of the checkpoint in the configuration. Atlas ships no checkpoint address: the
305
+ weights' license must be checked before a project downloads them, and that choice is yours.
306
+ - **`setupUiclip`** is the only place anything is downloaded (https only, hashed as it arrives, discarded on a mismatch);
307
+ `verifyCheckpoint` refuses a missing, unpinned or different file; inference never downloads.
308
+ - **Report-only by design.** A single global score cannot say what to fix, and ordinary loading overlays and bottom sheets score
309
+ poorly. A run reports the score with its standing among the scores of the same state kind, so a loading overlay is compared with
310
+ other loading overlays; calibrate before reading it as a threshold.
311
+ - **Not validated against a particular checkpoint.** The preprocessing is CLIP's (resize to 224 by 224, normalise) and the score is
312
+ the first output; measure the critic on your own labelled examples with `calibrate` before trusting it.
313
+
314
+ ## The LLM critic and bringing your own AI
315
+
316
+ `@crvouga/atlas/critics/llm` asks a vision-capable model to review an image against a **rubric** and answer with structured
317
+ findings. It is provider-agnostic: an `LlmProvider` is `complete(request)` plus declared capabilities, so a developer brings their own
318
+ model and account. Three adapters ship, all over plain `fetch` so there is no SDK to install: `openAiCompatibleProvider` speaks the
319
+ widely implemented chat-completions protocol (hosted services and local servers alike), `anthropicProvider` the Messages API and
320
+ `geminiProvider` the Gemini API. In the configuration `provider` is `openai-compatible` (the default, needs `baseUrl`), `anthropic` or
321
+ `gemini` (their own hosts by default; `apiKey` is required). A provider for anything else is a few lines.
322
+
323
+ - The rubric is recorded as a hash. The answer must be one JSON object with a `score` and optional `findings`; anything else is
324
+ `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
325
+ credential and an unreachable provider are failed critiques with a reason code.
326
+ - 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,
327
+ progress line, error and JSON document (the runner redacts what it records). Plain `http` is allowed only for a provider on this
328
+ machine.
329
+ - **Images may leave the machine.** `describe().sendsImagesTo` is the provider's host, shown by `list`, `doctor` and `run --dry-run`
330
+ and confirmed before a run that sends images there.
331
+
332
+ ```ts
333
+ import { createLlmCritic, openAiCompatibleProvider } from '@crvouga/atlas/critics/llm';
334
+
335
+ const critic = createLlmCritic({ model: 'my-vision-model', rubric: 'Judge spacing and contrast.', provider: openAiCompatibleProvider({ baseUrl: 'https://llm.example.com/v1', apiKey }) });
336
+ ```
337
+
338
+ ## When the server goes away
339
+
340
+ A run that cannot reach the server while publishing does not lose its work. The objects of the layer (its files, the layer, its
341
+ catalog entry) are written to the spool directory (`.atlas/critic-spool`, or `--spool`, or `spool` in the configuration); the run
342
+ reports how many, and exits normally. The next `atlas critic run` sends them first, **byte for byte what was written and in the
343
+ order it was written**, and removes each once the server has it. A layer is content-addressed and the store protocol is
344
+ idempotent, so one that did arrive is not duplicated, and a spool that is empty is not an error. A refusal (`401`, `403`, `409`,
345
+ `413`, `422`) is not spooled: it will not pass by waiting. `spoolingStore` and `flushSpool` do the same for your own code.
346
+
111
347
  ## Running critics
112
348
 
113
349
  `runCritic` owns everything around the judgement; a critic only judges one image.
@@ -176,15 +412,43 @@ run time (`resolveCredential`) and every message the runner records goes through
176
412
  `createRedactor`) that removes them, in their URL-encoded and base64 forms too, and anything shaped like a provider key,
177
413
  token or JWT. A credential never reaches a layer, a progress event or an error message.
178
414
 
415
+ ## The viewer: needs polish
416
+
417
+ `Needs polish` (`/critiques`, in the spec-type menu once any critic has recorded an opinion) ranks the captured states one critic
418
+ judged, lowest first, so the likeliest problems are reviewed first. When every ranked score carries its standing within its state
419
+ kind, that standing ranks them (a loading overlay is not compared with a full screen); otherwise the raw score does, read as
420
+ higher-is-better. The address holds the critic, the state kind and the open entry.
421
+
422
+ - **Opinions, not results.** Critiques have their own place, wording and mark, never a passed or failed mark, and are never in a test
423
+ count or the gate.
424
+ - **Anchored to pixels.** A state whose picture changed since it was judged is listed apart as *not judged at the current picture*, its
425
+ older opinion shown as judging an earlier picture; it is never ranked and never hidden.
426
+ - **Failures are data.** A critic that could not judge says why (the reason code) and that this says nothing about the picture.
427
+ - **Detail.** An open entry shows the verdict and its standing, every finding (category, severity, where, why, suggestion; unknown
428
+ categories and severities are shown as they are) and where the opinion came from: critic, version, model, parameters, the hash of
429
+ the picture it judged, the layer and the cost.
430
+ - **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
431
+ color and never by color alone: the number matches the findings table, where *Locate* highlights one. Regions in `normalized`
432
+ (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
433
+ listed and not drawn. A critique that judged an earlier picture is not drawn on the new one.
434
+ - **Agreement across critics.** The *Agreement across critics* tab compares each critic's opinion of the picture an entry has now. A
435
+ 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
436
+ 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
437
+ every critic flags come first, then those they disagree about; an entry only one critic judged is counted, not compared. It is a
438
+ reading for people, not a verdict, a status or a gate.
439
+ - **Marks.** The list and the open entry show what reviewers decided (accepted, dismissed, disputed, not reviewed), per finding and for
440
+ 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
441
+ `atlas critic review` command that marks.
442
+
179
443
  ## What is not here yet
180
444
 
181
- This release delivers the contract, the evidence format, the runner, state-aware judging and calibration, credential
182
- handling and the scoped `critic` role. Still to come, tracked in [issue 50](https://github.com/crvouga/atlas/issues/50):
445
+ Out of scope until the open questions of [issue 50](https://github.com/crvouga/atlas/issues/50) are settled, because they need the
446
+ weights, a license or a network that this repository does not have:
183
447
 
184
- - the **catalog read surface** for critics (cursor pagination, `since`, realtime events, images by hash), which builds on
185
- the UI catalog of [issue 43](https://github.com/crvouga/atlas/issues/43);
186
- - the **`atlas critic` command** (`list`, `setup`, `doctor`, `run`, `watch`, `review`, `calibrate`), its interactive mode
187
- and its `--format json` documents;
188
- - the **built-in UIClip critic** and the **LLM critic** with provider adapters;
189
- - the **viewer**: a ranked "needs polish" list, finding overlays and agreement across critics. Until then critiques are
190
- stored, composed and kept out of test counts, and the viewer lists only test results.
448
+ - the **UIClip critic's parity** with the in-house package it is meant to absorb: its exact preprocessing and score head, checked
449
+ against a real checkpoint. No checkpoint address ships until the weights' license is settled, so the pin is yours; measure it
450
+ on your own examples with `atlas critic calibrate` before trusting a score;
451
+ - adapters for providers beyond OpenAI-compatible, Anthropic and Gemini, and access by a local command-line agent or a provider's
452
+ OAuth sign-in (whether a consumer subscription may be used that way is per provider's terms);
453
+ - heuristic and classic ML critics shipped in the package, a flow-level or before-and-after critic, and server-hosted execution (never
454
+ planned: the server does not run models).
package/docs/extending.md CHANGED
@@ -249,7 +249,7 @@ Annotation `type`: `note`, `warning`, `deprecation`, `issue`, `accessibility`, `
249
249
  | 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. |
250
250
  | Structured data of your own | Put it in `extensions` under a name you own. It is kept, and shown as raw data. |
251
251
  | A new annotation type, role or step kind | Use it. Open vocabularies need no registration. |
252
- | 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. |
252
+ | 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. |
253
253
  | Something that should be shown well | Ask for a renderer (below). |
254
254
  | A field every tool should have | Open an issue. It is added as an optional field, and the rules above guarantee nothing breaks. |
255
255
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Generated from the usage text each command prints (`atlas <command> --help`) by `bun run docs`. Do not edit.
4
4
 
5
- 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.
5
+ 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.
6
6
 
7
7
  ## atlas
8
8
 
@@ -21,6 +21,8 @@ atlas — executable statecharts
21
21
  atlas dev [--port 5180] develop the Atlas server and client on one host
22
22
  atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
23
23
  atlas report publish|stream|status|merge|doctor|policy … layered evidence for any kind of spec (atlas report --help)
24
+ atlas critic list|setup|doctor|run … automated reviewers that mark which UIs need polish (atlas critic --help)
25
+ atlas ui-catalog plan|publish|status|diff|export … the UI catalog of captured states (atlas ui-catalog --help)
24
26
 
25
27
  --workers path attempts that run at once (default: one per CPU, capped by the driver)
26
28
  --seed only paths that start from this seed ("start" for paths setup starts); repeatable
@@ -172,3 +174,126 @@ atlas report — layered evidence for any kind of spec
172
174
 
173
175
  Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.
174
176
  ```
177
+
178
+ ## atlas ui-catalog
179
+
180
+ ```text
181
+ atlas ui-catalog — a browsable inventory of captured UI states, from any collector
182
+
183
+ atlas ui-catalog plan --states <file.json> --producer <name> [--store <dir|s3://bucket/prefix|https://…>]
184
+ [--environment <name>] [--param key=value]... [--shard <label>]
185
+ [--branch <b>] [--sha <s>] [--run-url <u>] [--run-id <id>] [--run-title <t>] [--recorded-at <iso|ms>]
186
+ atlas ui-catalog publish --captures <file.json> --producer <name> [the same flags as plan]
187
+ [--attach-root <dir>]... [--max-picture-mb <n>]
188
+ atlas ui-catalog status [--store <dir|s3://…|id=url>]... [--backends <module>] [filters] [--branch <name>]...
189
+ [--ignore-modified] [--format text|json|markdown]
190
+ atlas ui-catalog diff --from <commit> --to <commit> [--store …]... [--backends <module>] [filters]
191
+ [--fail-on-change] [--format text|json|markdown]
192
+ atlas ui-catalog export --out <file.html> [--store …]... [--backends <module>] [filters]
193
+ [--images inline|none] [--max-mb <n>] [--manifest <file.json>] [--title <t>]
194
+
195
+ filters (status, diff, export): --q <words> --coverage a,b --profile <key|label>... --area a --route r --cohort c
196
+ --flag f --surface s --producer p
197
+
198
+ plan Write one layer of kind ui-plan: the states (and with "inventory": true, the surfaces) that should exist under
199
+ this profile. Every entry is not-run, so a plan never counts as a pass. The file is {"states":[{"id":…,
200
+ "title"?,"route"?,"fixture"?,"cohort"?,"area"?,"kind"?,"flags"?,"covers"?,"inventory"?}]} or just that list.
201
+ publish Write one layer of kind ui-state: the states one collector or shard captured under this profile. The file is
202
+ {"states":[{"id":…,"title"?,"status"?:"passed|failed|running|not-run","detail"?,"route"?,"fixture"?,"cohort"?,
203
+ "area"?,"kind"?,"flags"?,"covers"?,"labels"?,"tags"?,"durationMs"?,"screenshot"?:"file.png"|{"path"|"uri",
204
+ "width"?,"height"?,"contentType"?}}]}. A picture file is read from beside the file and each --attach-root, never
205
+ from a hidden file or a link out. A passed state whose picture cannot be read is published as failed, with the
206
+ reason: a state without its picture is not shown as captured.
207
+ --environment and each --param key=value are the profile (a viewport, a color scheme, a device): another
208
+ profile is another result side by side. --shard only describes the layer; it never splits a result.
209
+ --recorded-at defaults to the modification time of the file, so publishing the same file twice is one layer.
210
+ status Read every store and print the catalog's summary. Exit 1 unless the catalog holds a state and there is nothing
211
+ to attend to: a failed capture, a state a plan expects with nothing captured, a capture still in progress, a
212
+ backend or layer that could not be read. Without a plan nothing is expected, so nothing is missing.
213
+ --profile takes a profile's key or its label, as printed by status.
214
+ diff Set the catalog recorded at one commit (--from) against another (--to): each state in each profile is paired
215
+ with itself and is unchanged, changed in coverage, changed in picture (the stored bytes differ), unverified (a
216
+ side has no content hash), only in --to (added) or only in --from (removed). A commit is the whole id or at
217
+ least 7 digits of it. Exit 0 unless a store could not be read, or, with --fail-on-change, anything differs.
218
+ export Write the catalog as one HTML file with its pictures embedded and its manifest in it: no server, no network.
219
+ It is the catalog at the moment it was made. --images none leaves the pictures out. --max-mb (default 32) limits
220
+ the embedded pictures; those past it are listed in the page as left out, and the manifest stays complete.
221
+ --manifest writes the catalog's JSON manifest too.
222
+
223
+ --format json (alias --json) every subcommand prints one JSON document instead of prose
224
+ (schemas: ui-catalog-publish-result, ui-catalog-status, ui-catalog-diff-result, ui-catalog-export-result).
225
+ A command that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
226
+ A store is a directory, s3://bucket/prefix or an Atlas server (https://…), exactly as for atlas report; select a project
227
+ with --project <slug> or ATLAS_PROJECT. When no --store is given, ATLAS_STORE is used.
228
+
229
+ Exit codes: 0 ok, 1 the answer is no (see each command), 2 the command was wrong.
230
+ ```
231
+
232
+ ## atlas critic
233
+
234
+ ```text
235
+ atlas critic — automated reviewers of the UI catalog
236
+
237
+ atlas critic list [--config atlas.critics.json] [--format text|json]
238
+ atlas critic setup <name> [--config atlas.critics.json] [--format text|json]
239
+ atlas critic doctor [--server <url>] [--critic <name>]... [--config atlas.critics.json] [--format text|json]
240
+ atlas critic run --critic <name>... ( --all | --unjudged | --since <version> | filters ) [--server <url>]
241
+ [--area a,b] [--profile key] [--state screen,overlay] [--route r] [--q words]
242
+ [--dry-run] [--yes] [--non-interactive] [--force]
243
+ [--max-images n] [--max-tokens n] [--max-cost usd] [--concurrency n] [--timeout-ms n]
244
+ [--mode report|warn|gate] [--threshold state=score]... [--calibration file.json]
245
+ [--spool dir] [--project slug] [--config atlas.critics.json] [--format text|json]
246
+ atlas critic watch --critic <name>... [--server <url>] [--since <version>] [filters] [--yes] [--dry-run]
247
+ [--max-batches n] [--max-images n] [--max-tokens n] [--max-cost usd] [--concurrency n] [--timeout-ms n]
248
+ [--spool dir] [--project slug] [--config atlas.critics.json] [--format text|json]
249
+ atlas critic review [--server <url>] [--critic <name>] [--state kind] [--limit n] [--list] [--non-interactive]
250
+ [--entry <key> ( --accept | --dismiss ) [--finding <id>]... [--note text] [--reviewer name] [--dry-run]]
251
+ [--project slug] [--config atlas.critics.json] [--format text|json]
252
+ atlas critic calibrate <name> ( --good <dir>... --bad <dir>... [--heldout-good <dir>...] [--heldout-bad <dir>...] [--state kind]
253
+ | --from-marks [--server <url>] [--critic-version v] ) [--out file.json] [--dry-run] [--yes]
254
+ [--max-images n] [--concurrency n] [--timeout-ms n] [--config atlas.critics.json] [--format text|json]
255
+
256
+ 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
257
+ what it thinks as layers of critiques. The server never runs a model and never holds a provider key. A critique is an opinion,
258
+ not a test result: it never changes whether a report passes.
259
+
260
+ Configuration is atlas.critics.json, which may be committed: it names credentials by reference (env:NAME, file:PATH) and a
261
+ literal credential in it is refused. The server is --server, "server.url" there, ATLAS_SERVER_URL or an https ATLAS_STORE;
262
+ the token is ATLAS_CRITIC_TOKEN (or ATLAS_TOKEN), never a flag, and is never sent over plain http beyond this machine.
263
+
264
+ list The critics available and configured: type, whether each is ready or needs setup, and where it sends images.
265
+ setup Provision a critic: fetch and verify a model checkpoint against its pinned SHA-256 (a bad checksum is an error), or
266
+ check that a credential reference resolves. This is the only place anything is downloaded; judging never downloads.
267
+ doctor Check the server is reachable, the token is accepted and has the scope a critic needs (and warn when the server is
268
+ open, so every caller is an administrator), that the server lists the catalog, and for each configured critic its
269
+ model file, dependencies, credential and provider. Each problem is reported separately, with how to fix it.
270
+ run Judge the entries in the chosen scope. An entry that already has a current critique of the same critic, version,
271
+ model, parameters and image is skipped without a call; a changed image is judged again. Failures are recorded
272
+ with a reason code and retried by the next run. Critiques that cannot be sent (the server went away) are kept in
273
+ the spool directory (default .atlas/critic-spool) and sent, byte for byte, by the next invocation.
274
+ --dry-run send and publish nothing: say how many entries would be judged or skipped, and where images would go
275
+ --yes allow sending images to a third party without asking (required without a terminal)
276
+ --max-images n stop cleanly after n images; --max-tokens and --max-cost likewise, from what critics report
277
+ --mode report (default): scores are shown and the exit code is 0; warn: also list scores below their threshold;
278
+ gate: exit 1 when a score is below its state kind's threshold, refused (exit 2) unless a calibration
279
+ with held-out evidence of this critic and version is given with --calibration
280
+
281
+ 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
282
+ already judged is skipped without a call), publishes as it goes, keeps what the server cannot take and sends it next time.
283
+ It stops on Ctrl-C, after --max-batches batches, or when a budget (for the whole watch) is spent, and prints the version to
284
+ resume from (--since). It reports; it never gates. --dry-run reads the catalog once and sends and publishes nothing.
285
+ review The ranked "needs polish" list of one critic (lowest first) with its findings and the marks already made. With a terminal it
286
+ walks through the list and asks: accept (the opinion is right: the UI needs polish), dismiss (it is not), skip. With
287
+ --entry and --accept or --dismiss it records one decision, for the whole opinion or for --finding ids. A mark is a layer
288
+ under critic.review.<reviewer> (--reviewer, ATLAS_REVIEWER); it needs no more power than a critic token and never changes
289
+ a result, a count or the gate.
290
+ calibrate Judge labelled pictures (--good and --bad directories; a subdirectory's name is the state kind of the pictures in it) and/or
291
+ read the whole-opinion marks reviewers made (--from-marks), and measure the critic per state kind: sample sizes, precision,
292
+ recall, the threshold, and whether the figures are in-sample. --heldout-* pictures are never used to choose the threshold.
293
+ --out writes the calibration that run --calibration reads. Nothing is published.
294
+
295
+ --format json prints one document (schemas: critic-list-result, critic-setup-result, critic-doctor-result, critic-run-result,
296
+ critic-watch-result, critic-review-result, critic-calibrate-result).
297
+ A command that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
298
+ Exit codes: 0 ok, 1 the answer is no (a critic failed, a gate crossed, a problem found), 2 the command was wrong.
299
+ ```
@@ -9,7 +9,7 @@ Executable statecharts: loading, composing, linting, planning and running specs
9
9
  Source: `packages/core/src/cli.ts`. Import: `@crvouga/atlas/cli`
10
10
 
11
11
  Dispatches `atlas <command>`: the statechart commands (`lint`, `plan`, `run`, `merge`, `export`) and, loaded on demand so
12
- they cost nothing otherwise, `serve`, `dev`, `init` and `report`. Statechart configuration is `atlas.config.ts` (see
12
+ they cost nothing otherwise, `serve`, `dev`, `init`, `report` and `critic`. Statechart configuration is `atlas.config.ts` (see
13
13
  `defineConfig`). A command that is wrong prints a message and exits 2; a plan, lint or run that finds problems exits 1.
14
14
 
15
15
  See docs/statecharts.md