@crvouga/atlas 1.11.0 → 1.13.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 (70) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/critics/src/contract.d.ts +113 -0
  3. package/dist/critics/src/contract.js +48 -0
  4. package/dist/critics/src/credentials.d.ts +72 -0
  5. package/dist/critics/src/credentials.js +136 -0
  6. package/dist/critics/src/critique.d.ts +128 -0
  7. package/dist/critics/src/critique.js +163 -0
  8. package/dist/critics/src/index.d.ts +21 -0
  9. package/dist/critics/src/index.js +21 -0
  10. package/dist/critics/src/judging.d.ts +153 -0
  11. package/dist/critics/src/judging.js +145 -0
  12. package/dist/critics/src/publish.d.ts +23 -0
  13. package/dist/critics/src/publish.js +21 -0
  14. package/dist/critics/src/runner.d.ts +104 -0
  15. package/dist/critics/src/runner.js +202 -0
  16. package/dist/critics/src/scope.d.ts +8 -0
  17. package/dist/critics/src/scope.js +26 -0
  18. package/dist/reports/src/compose.d.ts +2 -2
  19. package/dist/reports/src/compose.js +6 -5
  20. package/dist/reports/src/evidence-view.d.ts +2 -1
  21. package/dist/reports/src/evidence-view.js +5 -2
  22. package/dist/reports/src/layer-merge.d.ts +11 -3
  23. package/dist/reports/src/layer-merge.js +14 -3
  24. package/dist/reports/src/report-status.d.ts +1 -1
  25. package/dist/reports/src/report-status.js +6 -4
  26. package/dist/schema/src/api.d.ts +1 -0
  27. package/dist/schema/src/api.js +1 -1
  28. package/dist/schema/src/critique.d.ts +144 -0
  29. package/dist/schema/src/critique.js +96 -0
  30. package/dist/schema/src/index.d.ts +2 -1
  31. package/dist/schema/src/index.js +2 -1
  32. package/dist/schema/src/server-config.d.ts +7 -1
  33. package/dist/schema/src/server-config.js +6 -3
  34. package/dist/server/src/auth.d.ts +1 -1
  35. package/dist/server/src/auth.js +2 -2
  36. package/dist/server/src/config.js +2 -2
  37. package/dist/server/src/openapi.d.ts +3 -3
  38. package/dist/server/src/openapi.js +7 -4
  39. package/dist/server/src/store-api.js +39 -1
  40. package/docs/agents.md +1 -0
  41. package/docs/critics.md +190 -0
  42. package/docs/extending.md +8 -0
  43. package/docs/reference/critics.md +840 -0
  44. package/docs/reference/http-api.md +6 -2
  45. package/docs/reference/reports.md +20 -7
  46. package/docs/reference/schema.md +119 -2
  47. package/docs/reference/server.md +3 -3
  48. package/docs/report-contract.md +4 -0
  49. package/docs/rich-evidence.md +39 -0
  50. package/docs/server.md +6 -4
  51. package/docs/ui.md +22 -0
  52. package/llms-full.txt +279 -6
  53. package/llms.txt +2 -0
  54. package/package.json +7 -2
  55. package/packages/critics/README.md +18 -0
  56. package/packages/schema/schemas/critique.schema.json +254 -0
  57. package/packages/schema/schemas/server-config.schema.json +2 -1
  58. package/packages/schema/schemas/server-info.schema.json +1 -0
  59. package/packages/server/openapi.json +7 -6
  60. package/packages/visualizer/src/app/router.tsx +3 -0
  61. package/packages/visualizer/src/evidence/artifacts.tsx +88 -42
  62. package/packages/visualizer/src/evidence/behavior-panel.tsx +44 -25
  63. package/packages/visualizer/src/evidence/media-timing.test.ts +85 -0
  64. package/packages/visualizer/src/evidence/media-timing.ts +150 -0
  65. package/packages/visualizer/src/evidence/moments.tsx +457 -0
  66. package/packages/visualizer/src/evidence/outcome-summary.tsx +27 -0
  67. package/packages/visualizer/src/evidence/outcome.ts +7 -2
  68. package/packages/visualizer/src/explorer/TestDetail.tsx +2 -2
  69. package/packages/visualizer/src/explorer/playlist.tsx +89 -21
  70. package/packages/visualizer/src/explorer/search.ts +7 -1
package/llms-full.txt CHANGED
@@ -112,6 +112,7 @@ The complete public export map is generated from the package manifest (wildcards
112
112
  @crvouga/atlas/vitest
113
113
  @crvouga/atlas/schema
114
114
  @crvouga/atlas/server
115
+ @crvouga/atlas/critics
115
116
  @crvouga/atlas/reports/*
116
117
  @crvouga/atlas/schemas/*
117
118
  @crvouga/atlas/openapi.json
@@ -1014,6 +1015,7 @@ decided exactly as before ([the report contract](report-contract.md)). Evidence
1014
1015
  | `attempts` | check | Every try of a retried check, with its own status, detail, steps and attachments. A check that passed after failing is flaky. |
1015
1016
  | `tags`, `labels` | check, layer | Filters and groups (`smoke`, `flaky`, an owner, a browser). |
1016
1017
  | `tags` | attachment | Marks one recording or screenshot, for example `demo`: shown as a `#demo` chip, and searchable. See [Tagging artifacts and demos](#tagging-artifacts-and-demos). |
1018
+ | `extensions['atlas.media']` | attachment (video) | Where the check's steps are in the recording, so a click on a step seeks the video. See [Place steps in a recording](#place-steps-in-a-recording). |
1017
1019
  | `scope` | attachment, attempt, check, part, layer | What a recording does and does not show: where it ran, whether data and dependencies were real, whether audio was captured. See [Evidence scope](#evidence-scope). |
1018
1020
  | `annotations`, `links` | check, step, attempt, layer | Notes, warnings and issue links next to the result. |
1019
1021
  | `specPath` | check | The spec a check belongs to (a `.feature`, a test file), relative to the repository. Checks are grouped by it. |
@@ -1096,6 +1098,44 @@ Every field is optional, and every value except the booleans is an open string.
1096
1098
  - **It decides nothing.** A scope never changes a status, a slot or a winner. A scope an older Atlas cannot read is
1097
1099
  dropped, and the attachment, check and layer stay.
1098
1100
 
1101
+ ## Place steps in a recording
1102
+
1103
+ A step says when it started on the check's clock (`offsetMs`, after the check started). A video has its own clock, which
1104
+ began when the recorder did, and the two are not assumed to agree: a recorder usually starts before the first step, and a
1105
+ paced demo may have delays put into the video that the check never waited for. To let a reviewer click a step and land on
1106
+ its moment in the video, say how the clocks relate in the `atlas.media` extension of the video attachment:
1107
+
1108
+ ```ts
1109
+ {
1110
+ name: 'checkout.webm', role: 'video', durationMs: 9000,
1111
+ extensions: {
1112
+ 'atlas.media': {
1113
+ checkStartMs: 1200, // the check started 1.2 s into the video
1114
+ pauses: [{ atMs: 900, durationMs: 2000, reason: 'title card' }], // 2 s put into the video before the step at +900 ms
1115
+ marks: [{ step: 'pay', mediaMs: 4100 }] // a step whose moment in the video you know exactly
1116
+ }
1117
+ }
1118
+ }
1119
+ ```
1120
+
1121
+ Every field is optional and none of it decides a status, a slot or a winner. A step is placed at `checkStartMs` + its
1122
+ `offsetMs` + the `durationMs` of every pause whose `atMs` is at or before the step; a mark names the moment outright and
1123
+ wins. Anything else is unavailable and said so, never guessed.
1124
+
1125
+ - **Without timing, nothing is placed.** A video with no `atlas.media`, no `checkStartMs`, or a step with no `offsetMs` and
1126
+ no mark, is never assumed to start with the check, and a step is never placed from the wording of its name. Selecting
1127
+ such a step says why it cannot be shown, and moves nothing.
1128
+ - **Outside the video is not clamped.** A moment before the start, or after `durationMs` (or the length the player reports),
1129
+ is unavailable. A declaration with a field Atlas cannot read (a pause without a duration) is reported and places nothing.
1130
+ - **Pacing is not latency.** A video with `pauses`, or with a `scope` of `pacing: 'paced'`, is labelled as paced: the time
1131
+ between its steps includes deliberate pauses and is not application latency, and no measurement is changed by it.
1132
+ - **Each try has its own clock.** Put the video and the `atlas.media` of a retry on its `attempts[]` entry, with that
1133
+ try's `steps`. A try is aligned only by its own video, never by the check's or an earlier try's.
1134
+ - **Link to a moment.** The viewer copies a link that names the run (`layer`), the exact recording (`clip`, its digest or
1135
+ stored key) and the position (`clipAt`, milliseconds into the video). Opening it selects that recording of that run, even
1136
+ when a newer run exists, and goes to the position; a recording the run does not hold is reported, never replaced.
1137
+ - **Older layers.** A layer without `atlas.media` reads exactly as before; the video plays and its steps list as ever.
1138
+
1099
1139
  ## Product latency
1100
1140
 
1101
1141
  A test's `durationMs` includes fixture setup, deliberate waits, demo pacing, retries and cleanup, so it says how long the
@@ -1632,7 +1672,7 @@ is said in words and the server does not start. The file has a published
1632
1672
  | How often (seconds) the backends are looked at for layers written by something else | `refreshSeconds` | `--refresh <seconds>` | `ATLAS_REFRESH_SECONDS` | `10` |
1633
1673
  | Let anyone look, not upload | `auth.anonymousRead` | `--anonymous-read` | `ATLAS_ANONYMOUS_READ` | no |
1634
1674
  | Listen publicly with no tokens (something in front signs people in) | `auth.insecure` | `--insecure` | `ATLAS_INSECURE` | no |
1635
- | The admin token (also `ATLAS_PUBLISH_TOKEN` and `ATLAS_READ_TOKEN` for the other roles, or `auth.tokens` in the file) | `auth.tokens` | | `ATLAS_TOKEN` | none: the server is open and listens on this machine only |
1675
+ | The admin token (also `ATLAS_PUBLISH_TOKEN`, `ATLAS_CRITIC_TOKEN` and `ATLAS_READ_TOKEN` for the other roles, or `auth.tokens` in the file) | `auth.tokens` | | `ATLAS_TOKEN` | none: the server is open and listens on this machine only |
1636
1676
  | The configuration file | `(the file itself)` | `--config <file>` | `ATLAS_CONFIG` | `./atlas.server.json` when it exists |
1637
1677
  | Largest uploaded object in MiB | `limits.maxObjectMiB` | | `ATLAS_MAX_OBJECT_MIB` | `256` |
1638
1678
  | Largest JSON upload in MiB | `limits.maxJsonMiB` | | `ATLAS_MAX_JSON_MIB` | `32` |
@@ -1665,16 +1705,18 @@ backend in the client and `/readyz`, and recovers by itself.
1665
1705
  | Role | May |
1666
1706
  | --- | --- |
1667
1707
  | `read` | look: the client, the report, `/api/status` |
1708
+ | `critic` | also publish [critiques](critics.md#authentication-and-authorization) and nothing else: blobs and layers whose producer is `critic.<name>` and whose parts are all of kind `critique`. It cannot post test results, start live runs or change settings |
1668
1709
  | `publish` | also upload layers, specs and runs, and remove the scratch objects `atlas report doctor` writes |
1669
1710
  | `admin` | all server operations |
1670
1711
 
1671
1712
  Tokens are at least 12 characters (`openssl rand -hex 24`) and different from one another. Give them as
1672
- `ATLAS_TOKEN` (admin), `ATLAS_PUBLISH_TOKEN`, `ATLAS_READ_TOKEN`, or under `auth.tokens` in the file, preferably
1713
+ `ATLAS_TOKEN` (admin), `ATLAS_PUBLISH_TOKEN`, `ATLAS_CRITIC_TOKEN`, `ATLAS_READ_TOKEN`, or under `auth.tokens` in the file, preferably
1673
1714
  with `tokenEnv` so the file holds no secret.
1674
1715
 
1675
1716
  - **Sending a token:** `Authorization: Bearer <token>`. A browser's sign-in box also works for *looking* (any
1676
1717
  user name, the token as the password). **Uploading needs the Bearer header, never the sign-in box**: a browser
1677
1718
  sends those credentials by itself to any page that asks, which would let another site upload with them.
1719
+ - **Run critics against a server with tokens.** With no tokens every caller is an administrator, a critic included.
1678
1720
  - **No tokens** means open, and then the server refuses to listen beyond this machine unless told
1679
1721
  `--insecure` (for when a proxy in front does the signing in).
1680
1722
  - `/healthz` and `/readyz` never need a token, so a load balancer can use them.
@@ -1917,8 +1959,8 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
1917
1959
  | `GET /api/projects/{project}/store/{key}` | read | Read an object. |
1918
1960
  | `HEAD /api/store/{key}` | read | Does an object exist. |
1919
1961
  | `HEAD /api/projects/{project}/store/{key}` | read | Does an object exist. |
1920
- | `PUT /api/store/{key}` | publish | Upload an object. |
1921
- | `PUT /api/projects/{project}/store/{key}` | publish | Upload an object. |
1962
+ | `PUT /api/store/{key}` | critic | Upload an object. |
1963
+ | `PUT /api/projects/{project}/store/{key}` | critic | Upload an object. |
1922
1964
  | `DELETE /api/store/{key}` | publish | Remove a scratch object. |
1923
1965
  | `DELETE /api/projects/{project}/store/{key}` | publish | Remove a scratch object. |
1924
1966
  | `GET /api/reports/tests` | read | Every test, with insights, facets and history statistics. |
@@ -2759,6 +2801,10 @@ An Atlas path that passed on retry (`flaky`) is `passed`, with the detail "Passe
2759
2801
  Timings that only qualify a check (parts of kind `performance`) are not counted as checks when
2760
2802
  anything else is recorded. A measurement over its allowance is reported and never fails a check.
2761
2803
 
2804
+ Opinions (parts of kind `critique`, the verdicts of [automated UI reviewers](critics.md)) are never counted as checks,
2805
+ even when they are all that was recorded, and are never part of the gate: a low score, or a critic that crashed, leaves
2806
+ `atlas report status` as it was. The `status` of a critique check says only whether the critic ran.
2807
+
2762
2808
  **Freshness never changes a status.** A layer whose spec fingerprint no longer matches, whose
2763
2809
  checkout was modified, or whose related code changed still shows its recorded outcome, with a
2764
2810
  warning ("Potentially outdated: spec changed"). Atlas favours quick, possibly stale evidence over
@@ -3057,6 +3103,13 @@ when you do not know, and the viewer says the cause is unknown. It is never infe
3057
3103
  failed before the behavior started comes from the failed step's `kind`: `hook`, `setup`, `fixture`, `before`,
3058
3104
  `beforeAll`, `beforeEach` or `given` mean preparation; `when`, `then`, `assertion` or `expect` mean the behavior.
3059
3105
 
3106
+ ### Placing steps in a recording
3107
+
3108
+ A video may declare how its clock relates to the check's under the `atlas.media` extension (`checkStartMs`, `pauses`,
3109
+ `marks`), so the viewer can seek it from a step and mark the step playing. Without it the video plays as ever and no step
3110
+ is placed. The fields, the rules and the link to a moment are in
3111
+ [Place steps in a recording](rich-evidence.md#place-steps-in-a-recording).
3112
+
3060
3113
  ## Attach evidence
3061
3114
 
3062
3115
  ### From the command line
@@ -3178,6 +3231,7 @@ Annotation `type`: `note`, `warning`, `deprecation`, `issue`, `accessibility`, `
3178
3231
  | 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. |
3179
3232
  | Structured data of your own | Put it in `extensions` under a name you own. It is kept, and shown as raw data. |
3180
3233
  | A new annotation type, role or step kind | Use it. Open vocabularies need no registration. |
3234
+ | 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. |
3181
3235
  | Something that should be shown well | Ask for a renderer (below). |
3182
3236
  | A field every tool should have | Open an issue. It is added as an optional field, and the rules above guarantee nothing breaks. |
3183
3237
 
@@ -3199,6 +3253,199 @@ listed with its name, role, type and size, and offered for download, and everyth
3199
3253
  unaffected.
3200
3254
  </doc>
3201
3255
 
3256
+ <doc path="docs/critics.md">
3257
+ # Critics: automated reviewers that mark which UIs need polish
3258
+
3259
+ A **critic** looks at one captured UI (an image and what is known about it) and says how polished it is, and ideally
3260
+ where and why it is not. A critic may be a pretrained model that gives a score, a vision-capable language model that
3261
+ follows a rubric, or a classic measure such as a contrast ratio. All three are the same thing to Atlas, through one small
3262
+ contract, so people review the likeliest problems first.
3263
+
3264
+ **Critics are producers that publish layers.** Atlas supplies the contract, the runner, the evidence format and the
3265
+ credential. It never executes a model on the server and never holds a provider key. A critique is not a test result and
3266
+ is not human review: it never changes whether a report passes.
3267
+
3268
+ - [What is available](#what-is-available)
3269
+ - [The critic contract](#the-critic-contract)
3270
+ - [A critique is a layer](#a-critique-is-a-layer)
3271
+ - [Running critics](#running-critics)
3272
+ - [State-aware judging, calibration and modes](#state-aware-judging-calibration-and-modes)
3273
+ - [Authentication and authorization](#authentication-and-authorization)
3274
+ - [Credentials](#credentials)
3275
+ - [What is not here yet](#what-is-not-here-yet)
3276
+
3277
+ ## What is available
3278
+
3279
+ Import `@crvouga/atlas/critics`. It needs no model, no provider SDK and no network; a critic brings its own.
3280
+
3281
+ | You want | Use |
3282
+ | --- | --- |
3283
+ | Define a critic | `defineCritic` (the contract below) |
3284
+ | Judge many images with caching, retries, rate limits, timeouts and budgets | `runCritic` |
3285
+ | Store the opinions | `critiqueLayer`, `publishCritiques` (a layer of parts of kind `critique`) |
3286
+ | Read them back | `readCritique`, `critiquesOfLayer`, `critiqueCurrency` |
3287
+ | Read a score in context, measure a threshold, decide an exit code | `relativeVerdict`, `calibrate`, `gateDecision` |
3288
+ | Keep keys out of configuration and logs | `parseCredentialRef`, `resolveCredential`, `assertNoLiteralCredentials`, `createRedactor` |
3289
+ | Let a token publish critiques and nothing else | the `critic` role (below) |
3290
+
3291
+ The JSON Schema of the evidence is [`critique.schema.json`](../packages/schema/schemas/critique.schema.json). The
3292
+ function-level reference is generated from the code: [critics](reference/critics.md).
3293
+
3294
+ ## The critic contract
3295
+
3296
+ ```ts
3297
+ import { defineCritic } from '@crvouga/atlas/critics';
3298
+
3299
+ export default defineCritic({
3300
+ name: 'contrast', version: '1.0.0', kind: 'heuristic', // kind is an open string: model, llm, ml, heuristic…
3301
+ describe: () => ({ sendsImagesTo: null, needsSetup: false }),
3302
+ evaluate: async ({ image, imageSha256, entry, signal }) => ({
3303
+ verdict: { score: measureContrast(image), scale: '0..1, higher is better' }
3304
+ })
3305
+ });
3306
+ ```
3307
+
3308
+ A critic only judges one image. `describe()` says where images are sent (`sendsImagesTo`, `null` when they never leave
3309
+ the machine), the model, and the fixed parameters (temperature, the SHA-256 of the rubric): together with the critic's
3310
+ `name` and `version` and the image hash they are the identity of a judgement. `evaluate` returns a `verdict` (a score
3311
+ and how to read it), `findings`, or both, and may throw: throw a `RateLimitedError` (with the provider's `Retry-After` in
3312
+ milliseconds), an `InvalidOutputError`, or any `CriticError` with your own reason code. The runner records the failure
3313
+ and the next run retries it. A failure never becomes a score.
3314
+
3315
+ ## A critique is a layer
3316
+
3317
+ A critique is a check inside a part of kind `critique`, in a layer whose `producer` is `critic.<name>`. The check's
3318
+ `status` says only whether the critic ran: `passed` a verdict was produced, `failed` the critic could not produce one.
3319
+ The opinion is evidence, by the rules of [extending.md](extending.md#the-rules-that-keep-it-compatible-forever), so it
3320
+ can never decide a result. It lives in `extensions["atlas.critique"]` and is repeated as standard evidence, so a viewer
3321
+ that has never heard of critics still shows it: the score is a measurement named `polish`, and each finding is an
3322
+ annotation of type `critique`.
3323
+
3324
+ ```json
3325
+ {
3326
+ "producer": "critic.vision-llm",
3327
+ "kind": "critique",
3328
+ "checks": [{
3329
+ "id": "critique/checkout/payment-sheet",
3330
+ "status": "passed",
3331
+ "measurements": [{ "name": "polish", "value": 0.62, "unit": "score" }],
3332
+ "annotations": [{ "type": "critique", "level": "warning", "message": "spacing: Label touches both edges of the button. (suggestion: Increase horizontal padding.)" }],
3333
+ "extensions": { "atlas.critique": {
3334
+ "schema": 1,
3335
+ "subject": { "entryKey": "checkout/payment-sheet", "imageSha256": "…", "state": "overlay" },
3336
+ "critic": { "name": "vision-llm", "kind": "llm", "version": "1.0.0" },
3337
+ "model": { "id": "…", "provider": "…" },
3338
+ "parameters": { "temperature": 0, "rubric": "<sha256 of the rubric>" },
3339
+ "verdict": { "score": 0.62, "scale": "0..1, higher is better", "label": "needs-polish", "confidence": 0.7 },
3340
+ "findings": [{ "id": "f1", "category": "spacing", "severity": "minor", "region": { "unit": "px", "x": 24, "y": 612, "w": 342, "h": 48 }, "rationale": "Label touches both edges of the button.", "suggestion": "Increase horizontal padding." }]
3341
+ } }
3342
+ }]
3343
+ }
3344
+ ```
3345
+
3346
+ - A single score (a model-based critic) is a valid critique: everything except `schema`, `subject` and `critic` is optional.
3347
+ - `kind`, `category`, `severity`, `label` and failure `reason` are open strings, and every object is open. A value or field
3348
+ nobody has heard of is kept and shown generically, never rejected.
3349
+ - A reader that cannot read the whole extension (a missing `subject`, a score that is not a number, a `schema` version it
3350
+ does not know) drops the extension and keeps the check and its status. An unreadable opinion is unknown.
3351
+ - **Anchored to pixels.** `subject.imageSha256` is the hash of the image that was judged. When an entry's image changes,
3352
+ `critiqueCurrency` says the old critique judged an earlier image: it is shown, never hidden, and the entry counts as not
3353
+ yet judged.
3354
+ - **Slots.** The producer is the critic's name; the critic and model versions are labels. A newer version of a critic
3355
+ replaces an older one's critique of the same entry. Put the version in `parameters` of `critiqueLayer` to compare
3356
+ versions side by side instead.
3357
+ - **The prompt is recorded as a hash** (`parameters.rubric`), not as text.
3358
+ - **Failures are data.** A crash, a timeout, a rate limit that never cleared and an unparseable answer are `failed`
3359
+ critiques with `failure.reason` (`error`, `timeout`, `rate-limited`, `invalid-output`, `image-mismatch`…) and a message.
3360
+
3361
+ **Critiques never change the gate.** Parts of kind `critique` are left out of every count and of `atlas report status`,
3362
+ even when they are the only thing recorded (a report of opinions alone has no test results, so the gate says so). A low
3363
+ score does not fail it, and neither does a critic that crashed. A project that wants a score to fail something opts in
3364
+ with `gateDecision` in `gate` mode, which is computed by the reader from critique data and never changes a check's
3365
+ status, a slot or a winner.
3366
+
3367
+ ## Running critics
3368
+
3369
+ `runCritic` owns everything around the judgement; a critic only judges one image.
3370
+
3371
+ - **Caching.** An entry is skipped, without fetching its image, when a critique with a verdict already exists for the same
3372
+ critic, version, model, parameters and image hash (`existing`, read with `critiquesOfLayer`). Identical images across
3373
+ runs are judged once; only a changed image is judged again. A failed critique is not a cache hit. `force` judges again.
3374
+ Layers are content-addressed, so publishing the same outcomes twice is harmless.
3375
+ - **Concurrency, timeout, rate limits.** A bounded number of calls in flight. A call over `timeoutMs` is aborted and
3376
+ recorded as `timeout`; the run goes on. A rate-limit answer waits at least `Retry-After`, with exponential backoff and
3377
+ jitter, for a bounded number of attempts, and then is recorded as `rate-limited`.
3378
+ - **Budgets.** `budget: { maxImages, maxTokens, maxCostUsd }`. When one is reached the run stops cleanly and returns what
3379
+ it finished with `stoppedBy: 'budget'`. Cost and tokens come from the `usage` a critic reports.
3380
+ - **Dry run.** `dryRun` loads and sends nothing and returns what would be judged and skipped, and where images would go
3381
+ (`sendsImagesTo`).
3382
+ - **Reproducibility.** Results come back in the order of the entries, so the same inputs publish the same layer; a
3383
+ deterministic critic gives identical scores on every run. Atlas records what explains a result (critic, version,
3384
+ model, rubric hash, parameters) and does not promise that a language model answers the same twice. A re-run adds a
3385
+ newer layer instead of changing an old one.
3386
+ - **Images may leave the machine.** `describe().sendsImagesTo` is the thing to show a person before a run that sends
3387
+ images to a third party, and to require explicit confirmation for.
3388
+
3389
+ ```ts
3390
+ import { critiquesOfLayer, publishCritiques, runCritic } from '@crvouga/atlas/critics';
3391
+
3392
+ const result = await runCritic({
3393
+ critic, entries, loadImage: (entry) => fetchImage(entry),
3394
+ existing: layers.flatMap((layer) => critiquesOfLayer(layer).map((found) => found.critique)),
3395
+ budget: { maxImages: 200 }, secrets: [providerKey]
3396
+ });
3397
+ await publishCritiques(store, { critic, outcomes: result.outcomes, recordedAtMs: Date.now() });
3398
+ ```
3399
+
3400
+ ## State-aware judging, calibration and modes
3401
+
3402
+ A raw score is not absolute: a loading overlay is not a full screen.
3403
+
3404
+ - `relativeVerdict` reads a score against the project's scores of the **same state kind** (a percentile within the kind)
3405
+ and against the same entry's previous image (`delta`), and goes into `verdict.relative`.
3406
+ - `calibrate` measures a critic against labelled examples **per state kind**: sample sizes, precision, recall, the
3407
+ threshold chosen, and `inSample`, which is true unless held-out examples were given. In-sample figures flatter a critic
3408
+ and are labelled as such.
3409
+ - `gateDecision` turns scores into an exit code. Modes: `report` (default: shown, always exit 0), `warn` (a warning for each
3410
+ score below its threshold, exit 0), `gate` (exit 1 when a threshold is crossed). `gate` is **refused** (exit 2, with the
3411
+ reasons) unless every state kind present has a calibration of this critic and version with a threshold, at least 10 bad
3412
+ and 10 good examples, and held-out figures. An explicit threshold never waives that evidence.
3413
+
3414
+ ## Authentication and authorization
3415
+
3416
+ Run critics against a server that has tokens: a server with no tokens is open and every caller is an administrator, a
3417
+ critic included.
3418
+
3419
+ A token of role `critic` (`ATLAS_CRITIC_TOKEN`, or `role: "critic"` under `auth.tokens`) may do what `read` may, and
3420
+ publish critiques, and nothing else. Through the store protocol (`PUT /api/store/…`) it may upload blobs and layers, and
3421
+ the catalog entries that make them visible, whose `producer` is `critic.<name>` and whose parts are all of kind
3422
+ `critique`. Anything else is `403` naming the rule it broke, and the layer is not stored. It cannot post test results,
3423
+ start live runs, change settings or remove anything. `publish` and `admin` tokens may publish critiques too. As for every
3424
+ token: send it as `Authorization: Bearer`, never in a URL, never over plain `http` beyond this machine.
3425
+
3426
+ ## Credentials
3427
+
3428
+ Developers bring their own models and AI accounts. Configuration names a credential by **reference**
3429
+ (`env:SOME_API_KEY`, `file:/run/secrets/key`) and never contains one, so it can be committed;
3430
+ `assertNoLiteralCredentials` refuses a configuration with a literal key and says where, never what. Credentials are read at
3431
+ run time (`resolveCredential`) and every message the runner records goes through a redactor (`secrets` in `runCritic`,
3432
+ `createRedactor`) that removes them, in their URL-encoded and base64 forms too, and anything shaped like a provider key,
3433
+ token or JWT. A credential never reaches a layer, a progress event or an error message.
3434
+
3435
+ ## What is not here yet
3436
+
3437
+ This release delivers the contract, the evidence format, the runner, state-aware judging and calibration, credential
3438
+ handling and the scoped `critic` role. Still to come, tracked in [issue 50](https://github.com/crvouga/atlas/issues/50):
3439
+
3440
+ - the **catalog read surface** for critics (cursor pagination, `since`, realtime events, images by hash), which builds on
3441
+ the UI catalog of [issue 43](https://github.com/crvouga/atlas/issues/43);
3442
+ - the **`atlas critic` command** (`list`, `setup`, `doctor`, `run`, `watch`, `review`, `calibrate`), its interactive mode
3443
+ and its `--format json` documents;
3444
+ - the **built-in UIClip critic** and the **LLM critic** with provider adapters;
3445
+ - the **viewer**: a ranked "needs polish" list, finding overlays and agreement across critics. Until then critiques are
3446
+ stored, composed and kept out of test counts, and the viewer lists only test results.
3447
+ </doc>
3448
+
3202
3449
  <doc path="docs/report-backends.md">
3203
3450
 
3204
3451
  </doc>
@@ -4139,6 +4386,28 @@ over the same filtered set the page lists, in the same order (`explorer/playlist
4139
4386
  (paced, scripted, silent, synthetic) and the sentence that the length is the recording's, not production latency. Developer
4140
4387
  details opens the test pinned to that run's record and closes the playlist.
4141
4388
 
4389
+ ## Steps and moments in a recording
4390
+
4391
+ Wherever a result's steps sit beside its recording (a result's Timeline under Inspect, the Watch tab of a behavior, an
4392
+ opened watch playlist), `evidence/moments.tsx` joins them. Placement is from the recording's own declared timing
4393
+ (`evidence/media-timing.ts`, the `atlas.media` extension; see [Place steps in a recording](rich-evidence.md#place-steps-in-a-recording)),
4394
+ never from an assumption that the recording starts with the check.
4395
+
4396
+ - A step has a "Recording m:ss.s" button that seeks the selected recording; the step's test-clock offset stays visible beside it.
4397
+ A step that cannot be placed (no timing, no origin, no offset, outside the recording, unreadable declaration) is "Not aligned"
4398
+ and selecting it states the reason in a status alert. Nothing is guessed and the video is not moved.
4399
+ - While playing, the step under the playhead gets `aria-current="step"` and "Playing now". It never moves focus, scrolls
4400
+ the page or announces itself. Seeking only scrolls the player into view.
4401
+ - A paced recording (pauses declared, or a `pacing: paced` scope) says that gaps between steps include deliberate pauses and are
4402
+ not application latency.
4403
+ - A failed step has "Jump to the failing moment" in the bar above the timeline, and the failure summary has the same action
4404
+ (`requestFailureMoment`). If the recording is not on screen, or the step cannot be placed, it says so.
4405
+ - "Copy link to this moment" names the run (`layer`), the exact recording (`clip`) and the position (`clipAt`, ms), as the
4406
+ explorer address `/?test=…&layer=…&clip=…&clipAt=…` (the playlist links to the result it is playing). Opening it selects that
4407
+ recording of that exact run and seeks once the player knows its length; a position past the end or a recording that the run
4408
+ does not hold is reported and nothing else is played in its place. If the browser refuses to copy, the link is shown to copy.
4409
+ - One provider serves one holder: the check, or one try. A try's steps are placed by that try's recording and timing only.
4410
+
4142
4411
  ## Review collections
4143
4412
 
4144
4413
  The **Collections** tab (`explorer/collections.tsx`, `?tab=collections&collection=<id>&rev=<n>`) lists and opens review
@@ -5197,7 +5466,7 @@ Responses: `200` It exists.; `401` No token, or an unknown one. Send `Authorizat
5197
5466
 
5198
5467
  ### Upload an object
5199
5468
 
5200
- `PUT /api/store/{key}` · role: publish
5469
+ `PUT /api/store/{key}` · role: critic
5201
5470
 
5202
5471
  The write half of the `ObjectStore` interface. The server checks what it can from the bytes in hand and says no to what would corrupt the report for everyone:
5203
5472
 
@@ -5209,6 +5478,8 @@ The write half of the `ObjectStore` interface. The server checks what it can fro
5209
5478
 
5210
5479
  Upload in dependency order: blobs, then the manifest, then the catalog entry. `atlas report publish`, `atlas report stream` and `httpStore` do. Needs a `Bearer` token of role `publish` or `admin`; a browser's sign-in box is refused.
5211
5480
 
5481
+ A token of role `critic` may upload only what a critic publishes: blobs, and layers (and their catalog entries) whose `producer` is `critic.<name>` and whose parts are all of kind `critique`. Anything else is `403` with the rule it broke, and nothing is stored.
5482
+
5212
5483
  - `key` (path, required): An object key under `layers/`, such as `layers/manifests/<id>.json`. It contains slashes, which are not percent-encoded; every other character is.
5213
5484
  - `content-type` (header): Stored with the object and returned when it is read.
5214
5485
  - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
@@ -5217,7 +5488,7 @@ Responses: `200` Already there with the same bytes.; `201` Stored.; `401` No tok
5217
5488
 
5218
5489
  ### Upload an object
5219
5490
 
5220
- `PUT /api/projects/{project}/store/{key}` · role: publish
5491
+ `PUT /api/projects/{project}/store/{key}` · role: critic
5221
5492
 
5222
5493
  The write half of the `ObjectStore` interface. The server checks what it can from the bytes in hand and says no to what would corrupt the report for everyone:
5223
5494
 
@@ -5229,6 +5500,8 @@ The write half of the `ObjectStore` interface. The server checks what it can fro
5229
5500
 
5230
5501
  Upload in dependency order: blobs, then the manifest, then the catalog entry. `atlas report publish`, `atlas report stream` and `httpStore` do. Needs a `Bearer` token of role `publish` or `admin`; a browser's sign-in box is refused.
5231
5502
 
5503
+ A token of role `critic` may upload only what a critic publishes: blobs, and layers (and their catalog entries) whose `producer` is `critic.<name>` and whose parts are all of kind `critique`. Anything else is `403` with the rule it broke, and nothing is stored.
5504
+
5232
5505
  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.
5233
5506
 
5234
5507
  - `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.
package/llms.txt CHANGED
@@ -21,6 +21,7 @@ Given only the repository link, start at docs/agents.md: install `@crvouga/atlas
21
21
 
22
22
  - [The report contract](https://raw.githubusercontent.com/crvouga/atlas/main/docs/report-contract.md): This is the normative description of what Atlas reads and writes when it reports on specs of any kind:
23
23
  - [Evidence: attachments, timelines, snapshots and everything else](https://raw.githubusercontent.com/crvouga/atlas/main/docs/extending.md): Atlas is opinionated about specs and open about evidence.
24
+ - [Critics: automated reviewers that mark which UIs need polish](https://raw.githubusercontent.com/crvouga/atlas/main/docs/critics.md): A critic looks at one captured UI (an image and what is known about it) and says how polished it is, and ideally where and why it is not.
24
25
  - [docs/report-backends.md](https://raw.githubusercontent.com/crvouga/atlas/main/docs/report-backends.md):
25
26
  - [Executable statecharts](https://raw.githubusercontent.com/crvouga/atlas/main/docs/statecharts.md): Write down how a product behaves as statecharts in open formats, then let Atlas lint them, plan paths that cover every transition, run those paths against the real product through any driver (a browser, a phone, a pla…
26
27
 
@@ -46,6 +47,7 @@ Given only the repository link, start at docs/agents.md: install `@crvouga/atlas
46
47
  - [TypeScript API: detox](https://raw.githubusercontent.com/crvouga/atlas/main/docs/reference/detox.md): The Detox driver for statechart runs of React Native apps.
47
48
  - [TypeScript API: runners](https://raw.githubusercontent.com/crvouga/atlas/main/docs/reference/runners.md): Native reporters that upload to the project server.
48
49
  - [TypeScript API: vitest](https://raw.githubusercontent.com/crvouga/atlas/main/docs/reference/vitest.md): Run a statechart plan as Vitest tests.
50
+ - [TypeScript API: critics](https://raw.githubusercontent.com/crvouga/atlas/main/docs/reference/critics.md): Automated reviewers of the UI catalog: the critic contract, the runner (caching, retries, rate limits, budgets), credential references and redaction, the `atlas.critique` evidence as layers, calibration and report, warn and gate modes.
49
51
  - [TypeScript API: playwright-reporter](https://raw.githubusercontent.com/crvouga/atlas/main/docs/reference/playwright-reporter.md): A Playwright Test reporter that streams every test to Atlas with its duration, device labels, steps, retries, videos, screenshots and traces.
50
52
  - [JSON Schemas](https://unpkg.com/@crvouga/atlas/packages/schema/schemas/): the layer format, checks file, report status, command results, server responses, server configuration.
51
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/atlas",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Atlas: specs of any kind, reported as layered evidence. Publish checks from any runner (JUnit, CTRF, a script), combine them across nodes, branches, environments and backends, gate CI on the result, and run statecharts against the real app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -85,6 +85,10 @@
85
85
  "types": "./dist/server/src/index.d.ts",
86
86
  "default": "./dist/server/src/index.js"
87
87
  },
88
+ "./critics": {
89
+ "types": "./dist/critics/src/index.d.ts",
90
+ "default": "./dist/critics/src/index.js"
91
+ },
88
92
  "./reports/*": {
89
93
  "types": "./dist/reports/src/*.d.ts",
90
94
  "default": "./dist/reports/src/*.js"
@@ -155,7 +159,8 @@
155
159
  "docs/runners.md",
156
160
  "packages/runners/compat",
157
161
  "docs/ui.md",
158
- "docs/workspaces.md"
162
+ "docs/workspaces.md",
163
+ "docs/critics.md"
159
164
  ],
160
165
  "scripts": {
161
166
  "build": "bun scripts/build-package.ts",
@@ -0,0 +1,18 @@
1
+ # @crvouga/atlas/critics
2
+
3
+ Automated reviewers of the UI catalog that mark which UIs need polish. A critic judges one captured UI through one small
4
+ contract; the runner owns caching, retries, rate limits, timeouts and budgets; opinions are stored as ordinary layers of
5
+ parts of kind `critique` and never change whether a report passes.
6
+
7
+ Read [the critics guide](../../docs/critics.md). The function-level reference is generated from the code:
8
+ [critics](../../docs/reference/critics.md).
9
+
10
+ | Module | |
11
+ | --- | --- |
12
+ | `contract` | `defineCritic`, the `Critic` type, `CriticError`, `RateLimitedError`, `InvalidOutputError` |
13
+ | `runner` | `runCritic`: skip what was judged, bounded concurrency, timeout, backoff, budgets, dry run |
14
+ | `critique` | build, read and key critiques; `critiqueLayer`, `readCritique`, `critiqueCurrency` |
15
+ | `publish` | `publishCritiques` |
16
+ | `judging` | `relativeVerdict`, `calibrate`, `gateDecision` (report, warn, gate) |
17
+ | `credentials` | references (`env:NAME`), literal-credential refusal, redaction |
18
+ | `scope` | the rule a critic-role token is held to |