@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
@@ -414,7 +414,7 @@ Responses: `200` It exists.; `401` No token, or an unknown one. Send `Authorizat
414
414
 
415
415
  ### Upload an object
416
416
 
417
- `PUT /api/store/{key}` · role: publish
417
+ `PUT /api/store/{key}` · role: critic
418
418
 
419
419
  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:
420
420
 
@@ -426,6 +426,8 @@ The write half of the `ObjectStore` interface. The server checks what it can fro
426
426
 
427
427
  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.
428
428
 
429
+ 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.
430
+
429
431
  - `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.
430
432
  - `content-type` (header): Stored with the object and returned when it is read.
431
433
  - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
@@ -434,7 +436,7 @@ Responses: `200` Already there with the same bytes.; `201` Stored.; `401` No tok
434
436
 
435
437
  ### Upload an object
436
438
 
437
- `PUT /api/projects/{project}/store/{key}` · role: publish
439
+ `PUT /api/projects/{project}/store/{key}` · role: critic
438
440
 
439
441
  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:
440
442
 
@@ -446,6 +448,8 @@ The write half of the `ObjectStore` interface. The server checks what it can fro
446
448
 
447
449
  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.
448
450
 
451
+ 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.
452
+
449
453
  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.
450
454
 
451
455
  - `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.
@@ -1366,10 +1366,10 @@ A report: the results laid over the specs, with what was read and what could not
1366
1366
  #### `countResults`
1367
1367
 
1368
1368
  ```ts
1369
- export function countResults(results: readonly EvidenceResult[]): StatusCounts & { total: number }
1369
+ export function countResults(all: readonly EvidenceResult[]): StatusCounts & { total: number }
1370
1370
  ```
1371
1371
 
1372
- Counts results by status. Diagnostic results (parts of kind `performance`, which only qualify a check) are left out
1372
+ Counts results by status. Opinion results (parts of kind `critique`) are never counted. Diagnostic results (parts of kind `performance`, which only qualify a check) are left out
1373
1373
  whenever any other result is present; if only diagnostics exist, they are counted so the report is not empty.
1374
1374
 
1375
1375
  #### `summarize`
@@ -2371,7 +2371,8 @@ export function checkSlots(results: readonly EvidenceResult[]): CheckSlot[]
2371
2371
 
2372
2372
  Groups the combined results (the winners of the slots) by check, in the order the checks were first seen.
2373
2373
  A check's status is the worst of its non-diagnostic results; if it has only diagnostics (timings), those are its
2374
- outcomes. The title and id shown are the newest result's.
2374
+ outcomes. The title and id shown are the newest result's. Opinions (parts of kind `critique`, see `isOpinionKind`) are
2375
+ not checks and are left out.
2375
2376
 
2376
2377
  #### `SpecStatus`
2377
2378
 
@@ -4582,6 +4583,17 @@ export function isDiagnosticKind(kind: string): boolean
4582
4583
 
4583
4584
  Whether a part `kind` only qualifies an outcome (today only `performance`) and so is not counted as a check when anything else is recorded.
4584
4585
 
4586
+ #### `isOpinionKind`
4587
+
4588
+ ```ts
4589
+ export function isOpinionKind(kind: string): boolean
4590
+ ```
4591
+
4592
+ Whether a part `kind` records an opinion rather than an outcome (today only `critique`, an automated reviewer's
4593
+ judgement of a captured UI). An opinion is never counted as a check, never gates, and is not folded into a spec's
4594
+ rollup, even when it is the only thing recorded: a report of opinions alone has no test results. The check's `status`
4595
+ of such a part says whether the reviewer ran, not whether anything passed.
4596
+
4585
4597
  #### `emptyCounts`
4586
4598
 
4587
4599
  ```ts
@@ -4593,11 +4605,12 @@ A fresh status tally with every status at zero.
4593
4605
  #### `outcomeCounts`
4594
4606
 
4595
4607
  ```ts
4596
- export function outcomeCounts(rows: readonly LayerRow[]): StatusCounts
4608
+ export function outcomeCounts(allRows: readonly LayerRow[]): StatusCounts
4597
4609
  ```
4598
4610
 
4599
- Counts rows by status, leaving out the diagnostic rows (see `isDiagnosticKind`) that only qualify an outcome.
4600
- When every row is diagnostic they are counted, so a layer of timings alone is not empty.
4611
+ Counts rows by status, leaving out the opinion rows (see `isOpinionKind`) always, and the diagnostic rows (see
4612
+ `isDiagnosticKind`) that only qualify an outcome. When every remaining row is diagnostic they are counted, so a
4613
+ layer of timings alone is not empty.
4601
4614
 
4602
4615
  #### `layerEvidence`
4603
4616
 
@@ -6981,7 +6994,7 @@ Returns a `ReportStatus` (schemaVersion 1): `ok` is true exactly when `reasons`
6981
6994
  checks (or none completed), failed checks, running checks, specs without results (only with `requireAllSpecs`),
6982
6995
  each required environment with no checks (the name is normalised, so `iOS` means `ios`), then every notice of the
6983
6996
  report (an unreadable layer or backend). `failing` lists each failed or running winner, in a fixed order that does
6984
- not depend on read order; diagnostic `performance` checks are left out unless nothing else was recorded.
6997
+ not depend on read order; diagnostic `performance` checks are left out unless nothing else was recorded, and `critique` opinions are always left out.
6985
6998
  `recordings` reports recording completeness of the same results apart from `ok`: an incomplete or unknown
6986
6999
  completeness never changes the gate (use `atlas report publish --require-recordings` to fail on a missing video).
6987
7000
 
@@ -775,12 +775,128 @@ export function transitionId(source: string, event: string)
775
775
  Builds the id of a transition, the key of `transitions` in a manifest: the source state's name,
776
776
  `TRANSITION_ID_SEPARATOR`, then the event name.
777
777
 
778
+ ### The `atlas.critique` evidence schema
779
+
780
+ Source: `packages/schema/src/critique.ts`
781
+
782
+ What an automated reviewer (a critic) says about one captured UI. It is not a new layer format: a critique is a
783
+ check inside a part of kind `critique`, and the opinion rides in the check's `extensions["atlas.critique"]`, so by the
784
+ rules of docs/extending.md it can never decide a result. The check's `status` says only whether the critic ran
785
+ (`passed` a verdict was produced, `failed` the critic could not produce one, `running`, `not-run`).
786
+
787
+ Everything except `schema`, `subject` and `critic` is optional, so a single score (a model-based critic) and rich
788
+ findings (an LLM critic) are one shape. Every object is open, and `kind`, `category`, `severity`, `label` and `reason`
789
+ are open strings: a value nobody has heard of is kept and shown generically, never rejected. A reader that cannot read
790
+ the whole extension drops it and keeps the check and its status (`readCritique` in `@crvouga/atlas/critics`).
791
+
792
+ See docs/critics.md
793
+ See docs/extending.md
794
+
795
+ #### `CRITIQUE_EXTENSION`
796
+
797
+ ```ts
798
+ export const CRITIQUE_EXTENSION = …
799
+ ```
800
+
801
+ The key under which a critique is stored in a check's `extensions`.
802
+
803
+ #### `CRITIQUE_SCHEMA_VERSION`
804
+
805
+ ```ts
806
+ export const CRITIQUE_SCHEMA_VERSION = …
807
+ ```
808
+
809
+ The version of the `atlas.critique` extension a reader of this release understands. A critique of another version is unknown, not wrong.
810
+
811
+ #### `CRITIQUE_PART_KIND`
812
+
813
+ ```ts
814
+ export const CRITIQUE_PART_KIND = …
815
+ ```
816
+
817
+ The `kind` of the report part that holds critiques. Parts of this kind are opinions: never counted, never part of the gate.
818
+
819
+ #### `CRITIC_PRODUCER_PREFIX`
820
+
821
+ ```ts
822
+ export const CRITIC_PRODUCER_PREFIX = …
823
+ ```
824
+
825
+ The prefix of the `producer` of a layer of critiques: `critic.<critic name>`.
826
+
827
+ #### `CritiqueRegionSchema`
828
+
829
+ ```ts
830
+ export const CritiqueRegionSchema = …
831
+ ```
832
+
833
+ The region of the image a finding is about, in the stated unit (`px` of the stored image, or `normalized` 0..1).
834
+
835
+ #### `CritiqueFindingSchema`
836
+
837
+ ```ts
838
+ export const CritiqueFindingSchema = …
839
+ ```
840
+
841
+ One thing a critic found: where, what kind, how bad, why, and what to do. Nothing is required.
842
+
843
+ #### `CritiqueRelativeSchema`
844
+
845
+ ```ts
846
+ export const CritiqueRelativeSchema = …
847
+ ```
848
+
849
+ How a score reads in context. A raw score is not absolute: a loading overlay is compared with other loading overlays,
850
+ not with full screens, and a change against the same entry's previous image is more telling than the level.
851
+
852
+ #### `CritiqueVerdictSchema`
853
+
854
+ ```ts
855
+ export const CritiqueVerdictSchema = …
856
+ ```
857
+
858
+ The judgement: a score and how to read it, a label, a confidence. A critic that cannot score may give only a label.
859
+
860
+ #### `CritiqueFailureSchema`
861
+
862
+ ```ts
863
+ export const CritiqueFailureSchema = …
864
+ ```
865
+
866
+ Why a critic produced no verdict: a reason code in an open vocabulary (`error`, `timeout`, `rate-limited`, `invalid-output`) and a message that holds no credential.
867
+
868
+ #### `CritiqueSchema`
869
+
870
+ ```ts
871
+ export const CritiqueSchema = …
872
+ ```
873
+
874
+ The `atlas.critique` extension, version 1: the image judged (`subject`), who judged (`critic`, `model`,
875
+ `parameters`), the opinion (`verdict`, `findings`) or why there is none (`failure`), and what it cost (`usage`).
876
+ The prompt is recorded only as a hash (`parameters.rubric`). Emitted as `critique.schema.json`.
877
+
878
+ #### `Critique`
879
+
880
+ ```ts
881
+ export type Critique = z.output<typeof CritiqueSchema>;
882
+ ```
883
+
884
+ A critique as stored in a check's `extensions["atlas.critique"]`.
885
+
886
+ #### `CritiqueFinding`
887
+
888
+ ```ts
889
+ export type CritiqueFinding = z.output<typeof CritiqueFindingSchema>;
890
+ ```
891
+
892
+ One finding of a critique.
893
+
778
894
  ### Schema package entry point (`@crvouga/atlas/schema`)
779
895
 
780
896
  Source: `packages/schema/src/index.ts`. Import: `@crvouga/atlas/schema`
781
897
 
782
898
  Re-exports every Zod schema, constant and inferred type of the Atlas contract: the layer format and report status
783
- (`layer`), the statechart run manifest (`manifest`, `common`), the spec file formats (`spec`), the run index and
899
+ (`layer`), the `atlas.critique` evidence of automated UI reviewers (`critique`), the statechart run manifest (`manifest`, `common`), the spec file formats (`spec`), the run index and
784
900
  report sources the viewer reads (`runs-index`, `report-sources`) and the server configuration (`server-config`).
785
901
  The JSON Schemas shipped in `schemas/` are generated from these by `bun run schemas`.
786
902
 
@@ -1869,7 +1985,8 @@ See docs/server.md
1869
1985
  export const ServerRoleSchema = …
1870
1986
  ```
1871
1987
 
1872
- What a bearer token may do: `read` (look), `publish` (also upload) or `admin` (also remove scratch objects).
1988
+ What a bearer token may do: `read` (look), `critic` (also publish critiques, and nothing else), `publish` (also upload
1989
+ anything) or `admin` (also remove scratch objects).
1873
1990
 
1874
1991
  #### `ServerRole`
1875
1992
 
@@ -93,7 +93,7 @@ What it knows lives in its backends (by default a folder on its own disk), so re
93
93
 
94
94
  Source: `packages/server/src/auth.ts`
95
95
 
96
- Who is asking, and may they. Tokens have a role (`read` < `publish` < `admin`) and are compared in constant time. Reading
96
+ Who is asking, and may they. Tokens have a role (`read` < `critic` < `publish` < `admin`) and are compared in constant time. Reading
97
97
  accepts a Bearer token or the browser's sign-in box (any user name, the token as the password); changing anything needs
98
98
  the Bearer header, because a browser sends the sign-in box's credentials to any page that asks, which would let another
99
99
  site upload with them. A server with no tokens is open, and refuses to listen beyond this machine unless told it may.
@@ -530,10 +530,10 @@ The version of the API described, `major.minor.patch`: the minor grows when an o
530
530
  #### `Role`
531
531
 
532
532
  ```ts
533
- export type Role = 'none' | 'read' | 'publish' | 'admin';
533
+ export type Role = 'none' | 'read' | 'critic' | 'publish' | 'admin';
534
534
  ```
535
535
 
536
- Who may call an operation: `none` (anyone), or the least role that may (`read` < `publish` < `admin`).
536
+ Who may call an operation: `none` (anyone), or the least role that may (`read` < `critic` < `publish` < `admin`).
537
537
 
538
538
  #### `Operation`
539
539
 
@@ -230,6 +230,10 @@ An Atlas path that passed on retry (`flaky`) is `passed`, with the detail "Passe
230
230
  Timings that only qualify a check (parts of kind `performance`) are not counted as checks when
231
231
  anything else is recorded. A measurement over its allowance is reported and never fails a check.
232
232
 
233
+ Opinions (parts of kind `critique`, the verdicts of [automated UI reviewers](critics.md)) are never counted as checks,
234
+ even when they are all that was recorded, and are never part of the gate: a low score, or a critic that crashed, leaves
235
+ `atlas report status` as it was. The `status` of a critique check says only whether the critic ran.
236
+
233
237
  **Freshness never changes a status.** A layer whose spec fingerprint no longer matches, whose
234
238
  checkout was modified, or whose related code changed still shows its recorded outcome, with a
235
239
  warning ("Potentially outdated: spec changed"). Atlas favours quick, possibly stale evidence over
@@ -27,6 +27,7 @@ decided exactly as before ([the report contract](report-contract.md)). Evidence
27
27
  | `attempts` | check | Every try of a retried check, with its own status, detail, steps and attachments. A check that passed after failing is flaky. |
28
28
  | `tags`, `labels` | check, layer | Filters and groups (`smoke`, `flaky`, an owner, a browser). |
29
29
  | `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). |
30
+ | `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). |
30
31
  | `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). |
31
32
  | `annotations`, `links` | check, step, attempt, layer | Notes, warnings and issue links next to the result. |
32
33
  | `specPath` | check | The spec a check belongs to (a `.feature`, a test file), relative to the repository. Checks are grouped by it. |
@@ -109,6 +110,44 @@ Every field is optional, and every value except the booleans is an open string.
109
110
  - **It decides nothing.** A scope never changes a status, a slot or a winner. A scope an older Atlas cannot read is
110
111
  dropped, and the attachment, check and layer stay.
111
112
 
113
+ ## Place steps in a recording
114
+
115
+ A step says when it started on the check's clock (`offsetMs`, after the check started). A video has its own clock, which
116
+ began when the recorder did, and the two are not assumed to agree: a recorder usually starts before the first step, and a
117
+ 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
118
+ its moment in the video, say how the clocks relate in the `atlas.media` extension of the video attachment:
119
+
120
+ ```ts
121
+ {
122
+ name: 'checkout.webm', role: 'video', durationMs: 9000,
123
+ extensions: {
124
+ 'atlas.media': {
125
+ checkStartMs: 1200, // the check started 1.2 s into the video
126
+ pauses: [{ atMs: 900, durationMs: 2000, reason: 'title card' }], // 2 s put into the video before the step at +900 ms
127
+ marks: [{ step: 'pay', mediaMs: 4100 }] // a step whose moment in the video you know exactly
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ Every field is optional and none of it decides a status, a slot or a winner. A step is placed at `checkStartMs` + its
134
+ `offsetMs` + the `durationMs` of every pause whose `atMs` is at or before the step; a mark names the moment outright and
135
+ wins. Anything else is unavailable and said so, never guessed.
136
+
137
+ - **Without timing, nothing is placed.** A video with no `atlas.media`, no `checkStartMs`, or a step with no `offsetMs` and
138
+ no mark, is never assumed to start with the check, and a step is never placed from the wording of its name. Selecting
139
+ such a step says why it cannot be shown, and moves nothing.
140
+ - **Outside the video is not clamped.** A moment before the start, or after `durationMs` (or the length the player reports),
141
+ is unavailable. A declaration with a field Atlas cannot read (a pause without a duration) is reported and places nothing.
142
+ - **Pacing is not latency.** A video with `pauses`, or with a `scope` of `pacing: 'paced'`, is labelled as paced: the time
143
+ between its steps includes deliberate pauses and is not application latency, and no measurement is changed by it.
144
+ - **Each try has its own clock.** Put the video and the `atlas.media` of a retry on its `attempts[]` entry, with that
145
+ try's `steps`. A try is aligned only by its own video, never by the check's or an earlier try's.
146
+ - **Link to a moment.** The viewer copies a link that names the run (`layer`), the exact recording (`clip`, its digest or
147
+ stored key) and the position (`clipAt`, milliseconds into the video). Opening it selects that recording of that run, even
148
+ when a newer run exists, and goes to the position; a recording the run does not hold is reported, never replaced.
149
+ - **Older layers.** A layer without `atlas.media` reads exactly as before; the video plays and its steps list as ever.
150
+
112
151
  ## Product latency
113
152
 
114
153
  A test's `durationMs` includes fixture setup, deliberate waits, demo pacing, retries and cleanup, so it says how long the
package/docs/server.md CHANGED
@@ -205,7 +205,7 @@ is said in words and the server does not start. The file has a published
205
205
  | How often (seconds) the backends are looked at for layers written by something else | `refreshSeconds` | `--refresh <seconds>` | `ATLAS_REFRESH_SECONDS` | `10` |
206
206
  | Let anyone look, not upload | `auth.anonymousRead` | `--anonymous-read` | `ATLAS_ANONYMOUS_READ` | no |
207
207
  | Listen publicly with no tokens (something in front signs people in) | `auth.insecure` | `--insecure` | `ATLAS_INSECURE` | no |
208
- | 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 |
208
+ | 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 |
209
209
  | The configuration file | `(the file itself)` | `--config <file>` | `ATLAS_CONFIG` | `./atlas.server.json` when it exists |
210
210
  | Largest uploaded object in MiB | `limits.maxObjectMiB` | | `ATLAS_MAX_OBJECT_MIB` | `256` |
211
211
  | Largest JSON upload in MiB | `limits.maxJsonMiB` | | `ATLAS_MAX_JSON_MIB` | `32` |
@@ -238,16 +238,18 @@ backend in the client and `/readyz`, and recovers by itself.
238
238
  | Role | May |
239
239
  | --- | --- |
240
240
  | `read` | look: the client, the report, `/api/status` |
241
+ | `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 |
241
242
  | `publish` | also upload layers, specs and runs, and remove the scratch objects `atlas report doctor` writes |
242
243
  | `admin` | all server operations |
243
244
 
244
245
  Tokens are at least 12 characters (`openssl rand -hex 24`) and different from one another. Give them as
245
- `ATLAS_TOKEN` (admin), `ATLAS_PUBLISH_TOKEN`, `ATLAS_READ_TOKEN`, or under `auth.tokens` in the file, preferably
246
+ `ATLAS_TOKEN` (admin), `ATLAS_PUBLISH_TOKEN`, `ATLAS_CRITIC_TOKEN`, `ATLAS_READ_TOKEN`, or under `auth.tokens` in the file, preferably
246
247
  with `tokenEnv` so the file holds no secret.
247
248
 
248
249
  - **Sending a token:** `Authorization: Bearer <token>`. A browser's sign-in box also works for *looking* (any
249
250
  user name, the token as the password). **Uploading needs the Bearer header, never the sign-in box**: a browser
250
251
  sends those credentials by itself to any page that asks, which would let another site upload with them.
252
+ - **Run critics against a server with tokens.** With no tokens every caller is an administrator, a critic included.
251
253
  - **No tokens** means open, and then the server refuses to listen beyond this machine unless told
252
254
  `--insecure` (for when a proxy in front does the signing in).
253
255
  - `/healthz` and `/readyz` never need a token, so a load balancer can use them.
@@ -490,8 +492,8 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
490
492
  | `GET /api/projects/{project}/store/{key}` | read | Read an object. |
491
493
  | `HEAD /api/store/{key}` | read | Does an object exist. |
492
494
  | `HEAD /api/projects/{project}/store/{key}` | read | Does an object exist. |
493
- | `PUT /api/store/{key}` | publish | Upload an object. |
494
- | `PUT /api/projects/{project}/store/{key}` | publish | Upload an object. |
495
+ | `PUT /api/store/{key}` | critic | Upload an object. |
496
+ | `PUT /api/projects/{project}/store/{key}` | critic | Upload an object. |
495
497
  | `DELETE /api/store/{key}` | publish | Remove a scratch object. |
496
498
  | `DELETE /api/projects/{project}/store/{key}` | publish | Remove a scratch object. |
497
499
  | `GET /api/reports/tests` | read | Every test, with insights, facets and history statistics. |
package/docs/ui.md CHANGED
@@ -150,6 +150,28 @@ over the same filtered set the page lists, in the same order (`explorer/playlist
150
150
  (paced, scripted, silent, synthetic) and the sentence that the length is the recording's, not production latency. Developer
151
151
  details opens the test pinned to that run's record and closes the playlist.
152
152
 
153
+ ## Steps and moments in a recording
154
+
155
+ Wherever a result's steps sit beside its recording (a result's Timeline under Inspect, the Watch tab of a behavior, an
156
+ opened watch playlist), `evidence/moments.tsx` joins them. Placement is from the recording's own declared timing
157
+ (`evidence/media-timing.ts`, the `atlas.media` extension; see [Place steps in a recording](rich-evidence.md#place-steps-in-a-recording)),
158
+ never from an assumption that the recording starts with the check.
159
+
160
+ - A step has a "Recording m:ss.s" button that seeks the selected recording; the step's test-clock offset stays visible beside it.
161
+ A step that cannot be placed (no timing, no origin, no offset, outside the recording, unreadable declaration) is "Not aligned"
162
+ and selecting it states the reason in a status alert. Nothing is guessed and the video is not moved.
163
+ - While playing, the step under the playhead gets `aria-current="step"` and "Playing now". It never moves focus, scrolls
164
+ the page or announces itself. Seeking only scrolls the player into view.
165
+ - A paced recording (pauses declared, or a `pacing: paced` scope) says that gaps between steps include deliberate pauses and are
166
+ not application latency.
167
+ - A failed step has "Jump to the failing moment" in the bar above the timeline, and the failure summary has the same action
168
+ (`requestFailureMoment`). If the recording is not on screen, or the step cannot be placed, it says so.
169
+ - "Copy link to this moment" names the run (`layer`), the exact recording (`clip`) and the position (`clipAt`, ms), as the
170
+ explorer address `/?test=…&layer=…&clip=…&clipAt=…` (the playlist links to the result it is playing). Opening it selects that
171
+ recording of that exact run and seeks once the player knows its length; a position past the end or a recording that the run
172
+ 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.
173
+ - One provider serves one holder: the check, or one try. A try's steps are placed by that try's recording and timing only.
174
+
153
175
  ## Review collections
154
176
 
155
177
  The **Collections** tab (`explorer/collections.tsx`, `?tab=collections&collection=<id>&rev=<n>`) lists and opens review