@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
@@ -572,11 +572,27 @@ export const UiCatalogSchema = …
572
572
  `GET /api/reports/ui-catalog`: the UI catalog, computed from the winning results of the `ui-state` and `ui-plan` parts
573
573
  of every layer. States are in id order and cells in profile order, so the same layers always give the same document.
574
574
 
575
+ #### `CatalogEntrySchema`
576
+
577
+ ```ts
578
+ export const CatalogEntrySchema = …
579
+ ```
580
+
581
+ An entry of the catalog as a critic reads it: one captured state in one profile, with the hash of its stored picture and the critiques already made of it.
582
+
583
+ #### `CatalogEntriesSchema`
584
+
585
+ ```ts
586
+ export const CatalogEntriesSchema = …
587
+ ```
588
+
589
+ `GET /api/reports/ui-catalog/entries`: the UI catalog as a flat, cursor-paged list of captured states for a critic.
590
+
575
591
  ### Command results (`--format json`)
576
592
 
577
593
  Source: `packages/schema/src/cli-results.ts`
578
594
 
579
- What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings` print on standard output with `--format json` (alias
595
+ What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings`, `atlas ui-catalog plan`, `publish`, `status`, `diff` and `export`, and `atlas critic list`, `setup`, `doctor` and `run`, print on standard output with `--format json` (alias
580
596
  `--json`), and what any `atlas` command prints on standard error for a command that was wrong: one JSON document,
581
597
  so an agent or a script never has to parse prose. (`status` and `policy` already print documents:
582
598
  `ReportStatusSchema`, and an IAM policy.) Each is also published as a JSON Schema.
@@ -686,6 +702,142 @@ export const InitPlanSchema = …
686
702
  What `atlas init --format json` prints: everything found about the project, and everything it would change.
687
703
  Without `--write` nothing is changed. With `--write`, the files are written and `written` says which.
688
704
 
705
+ #### `CriticListEntrySchema`
706
+
707
+ ```ts
708
+ export const CriticListEntrySchema = …
709
+ ```
710
+
711
+ One critic as `atlas critic list` shows it.
712
+
713
+ #### `CriticListResultSchema`
714
+
715
+ ```ts
716
+ export const CriticListResultSchema = …
717
+ ```
718
+
719
+ What `atlas critic list --format json` prints.
720
+
721
+ #### `CriticSetupResultSchema`
722
+
723
+ ```ts
724
+ export const CriticSetupResultSchema = …
725
+ ```
726
+
727
+ What `atlas critic setup --format json` prints.
728
+
729
+ #### `CriticDoctorCheckSchema`
730
+
731
+ ```ts
732
+ export const CriticDoctorCheckSchema = …
733
+ ```
734
+
735
+ One thing `atlas critic doctor` checked.
736
+
737
+ #### `CriticDoctorResultSchema`
738
+
739
+ ```ts
740
+ export const CriticDoctorResultSchema = …
741
+ ```
742
+
743
+ What `atlas critic doctor --format json` prints.
744
+
745
+ #### `CriticRunCriticSchema`
746
+
747
+ ```ts
748
+ export const CriticRunCriticSchema = …
749
+ ```
750
+
751
+ What `atlas critic run` did for one critic.
752
+
753
+ #### `CriticRunResultSchema`
754
+
755
+ ```ts
756
+ export const CriticRunResultSchema = …
757
+ ```
758
+
759
+ What `atlas critic run --format json` prints. A dry run publishes and sends nothing.
760
+
761
+ #### `CriticWatchCriticSchema`
762
+
763
+ ```ts
764
+ export const CriticWatchCriticSchema = …
765
+ ```
766
+
767
+ What `atlas critic watch` did for one critic.
768
+
769
+ #### `CriticWatchResultSchema`
770
+
771
+ ```ts
772
+ export const CriticWatchResultSchema = …
773
+ ```
774
+
775
+ What `atlas critic watch --format json` prints, once, when the watch ends.
776
+
777
+ #### `CriticMarkStandingSchema`
778
+
779
+ ```ts
780
+ export const CriticMarkStandingSchema = …
781
+ ```
782
+
783
+ What reviewers decided about one opinion, as `atlas critic review` reports it.
784
+
785
+ #### `CriticReviewResultSchema`
786
+
787
+ ```ts
788
+ export const CriticReviewResultSchema = …
789
+ ```
790
+
791
+ What `atlas critic review --format json` prints.
792
+
793
+ #### `CriticCalibrateResultSchema`
794
+
795
+ ```ts
796
+ export const CriticCalibrateResultSchema = …
797
+ ```
798
+
799
+ What `atlas critic calibrate --format json` prints. A dry run measures nothing and writes nothing.
800
+
801
+ #### `UiCatalogPublishResultSchema`
802
+
803
+ ```ts
804
+ export const UiCatalogPublishResultSchema = …
805
+ ```
806
+
807
+ What `atlas ui-catalog plan --format json` and `atlas ui-catalog publish --format json` print: the one layer that was written.
808
+
809
+ #### `UiCatalogProblemSchema`
810
+
811
+ ```ts
812
+ export const UiCatalogProblemSchema = …
813
+ ```
814
+
815
+ One thing that keeps a UI catalog from being complete and good.
816
+
817
+ #### `UiCatalogStatusSchema`
818
+
819
+ ```ts
820
+ export const UiCatalogStatusSchema = …
821
+ ```
822
+
823
+ What `atlas ui-catalog status --format json` prints.
824
+
825
+ #### `UiCatalogDiffResultSchema`
826
+
827
+ ```ts
828
+ export const UiCatalogDiffResultSchema = …
829
+ ```
830
+
831
+ What `atlas ui-catalog diff --format json` prints.
832
+
833
+ #### `UiCatalogExportResultSchema`
834
+
835
+ ```ts
836
+ export const UiCatalogExportResultSchema = …
837
+ ```
838
+
839
+ What `atlas ui-catalog export --format json` prints.
840
+
689
841
  #### `CLI_RESULTS`
690
842
 
691
843
  ```ts
@@ -983,6 +1135,50 @@ export type CritiqueFinding = z.output<typeof CritiqueFindingSchema>;
983
1135
 
984
1136
  One finding of a critique.
985
1137
 
1138
+ #### `CRITIQUE_MARK_EXTENSION`
1139
+
1140
+ ```ts
1141
+ export const CRITIQUE_MARK_EXTENSION = …
1142
+ ```
1143
+
1144
+ The key under which a reviewer's mark of a critique is stored in a check's `extensions`.
1145
+
1146
+ #### `CRITIQUE_MARK_SCHEMA_VERSION`
1147
+
1148
+ ```ts
1149
+ export const CRITIQUE_MARK_SCHEMA_VERSION = …
1150
+ ```
1151
+
1152
+ The version of the `atlas.critique-mark` extension a reader of this release understands. A mark of another version is unknown, not wrong.
1153
+
1154
+ #### `CRITIQUE_MARK_PRODUCER_PREFIX`
1155
+
1156
+ ```ts
1157
+ export const CRITIQUE_MARK_PRODUCER_PREFIX = …
1158
+ ```
1159
+
1160
+ The prefix of the `producer` of a layer of marks: `critic.review.<reviewer>`. It is within what a critic-role token may publish.
1161
+
1162
+ #### `CritiqueMarkSchema`
1163
+
1164
+ ```ts
1165
+ export const CritiqueMarkSchema = …
1166
+ ```
1167
+
1168
+ The `atlas.critique-mark` extension, version 1: a person's verdict on a critic's opinion. `accept` says the opinion was right
1169
+ (the finding is valid, the UI needs polish); `dismiss` says it was not. A mark names the entry and the exact picture it was made
1170
+ about, the critic whose opinion it marks, and optionally one finding of it; without `finding` it marks the critique as a whole.
1171
+ Like the critique it is evidence in a part of kind `critique`: it is never counted and never changes a status, a slot or the gate.
1172
+ A newer mark of the same reviewer for the same subject replaces an older one. Emitted as `critique-mark.schema.json`.
1173
+
1174
+ #### `CritiqueMark`
1175
+
1176
+ ```ts
1177
+ export type CritiqueMark = z.output<typeof CritiqueMarkSchema>;
1178
+ ```
1179
+
1180
+ A mark as stored in a check's `extensions["atlas.critique-mark"]`.
1181
+
986
1182
  ### Schema package entry point (`@crvouga/atlas/schema`)
987
1183
 
988
1184
  Source: `packages/schema/src/index.ts`. Import: `@crvouga/atlas/schema`
@@ -232,7 +232,8 @@ anything else is recorded. A measurement over its allowance is reported and neve
232
232
 
233
233
  Opinions (parts of kind `critique`, the verdicts of [automated UI reviewers](critics.md)) are never counted as checks,
234
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.
235
+ `atlas report status` as it was. The `status` of a critique check says only whether the critic ran. A person's accept or dismiss of an
236
+ opinion (`atlas.critique-mark`) is evidence in the same kind of part and has the same properties.
236
237
 
237
238
  **Freshness never changes a status.** A layer whose spec fingerprint no longer matches, whose
238
239
  checkout was modified, or whose related code changed still shows its recorded outcome, with a
package/docs/server.md CHANGED
@@ -545,6 +545,8 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
545
545
  | `GET /api/projects/{project}/reports/artifacts` | read | Recorded results that carry evidence, across the whole history. |
546
546
  | `GET /api/reports/ui-catalog` | read | The UI catalog: captured UI states per profile, and what is still expected. |
547
547
  | `GET /api/projects/{project}/reports/ui-catalog` | read | The UI catalog: captured UI states per profile, and what is still expected. |
548
+ | `GET /api/reports/ui-catalog/entries` | read | The UI catalog as a list a critic iterates: captured states with their picture hash and critiques. |
549
+ | `GET /api/projects/{project}/reports/ui-catalog/entries` | read | The UI catalog as a list a critic iterates: captured states with their picture hash and critiques. |
548
550
  | `GET /api/reports/runs/detail` | read | Every test one run recorded. |
549
551
  | `GET /api/projects/{project}/reports/runs/detail` | read | Every test one run recorded. |
550
552
  | `GET /api/reports/outline` | read | The specs a report is laid over. |
@@ -7,16 +7,19 @@ thousands of captures can be watched while it happens and split across as many m
7
7
 
8
8
  Nothing about it is new to the report contract. A state is an ordinary check with a screenshot, the expected set is an ordinary
9
9
  layer, and the stream is the one every viewer already holds. This page is the convention that makes those usable as a catalog, the
10
- decisions behind it, and what is not built yet.
10
+ decisions behind it, the command line, and what stays in your project.
11
11
 
12
12
  - [How a state is written](#how-a-state-is-written)
13
13
  - [What is expected](#what-is-expected)
14
14
  - [Publishing from any collector](#publishing-from-any-collector)
15
+ - [The command line](#the-command-line)
15
16
  - [Sharding](#sharding)
16
17
  - [Streaming and loading](#streaming-and-loading)
17
18
  - [Exploring it](#exploring-it)
19
+ - [Comparing two commits or two profiles](#comparing-two-commits-or-two-profiles)
20
+ - [A single file that needs no server](#a-single-file-that-needs-no-server)
18
21
  - [Decisions](#decisions)
19
- - [What stays in your project, and what is not built yet](#what-stays-in-your-project-and-what-is-not-built-yet)
22
+ - [What stays in your project, and what Atlas does not do](#what-stays-in-your-project-and-what-atlas-does-not-do)
20
23
 
21
24
  ## How a state is written
22
25
 
@@ -28,7 +31,7 @@ A state is one **check** in a layer whose part has `kind` **`ui-state`**.
28
31
  | How the capture went | `status`: `passed` is **captured**, `failed` is attempted and failed (say why in `detail`), `running` is being taken, `not-run` is skipped. |
29
32
  | The picture | An attachment with `role: "screenshot"` (else the first image), stored in the layer by content or given as an `https` `uri`. `width` and `height` help the viewer lay it out. |
30
33
  | The profile | The layer's `environment` (`desktop-web`, `mobile-web`, `ios`, `android`) and `parameters` (`{ "viewport": "390x844", "scheme": "dark" }`). Parameters split a slot, so the same state at another viewport is **another result, side by side**, never a replacement. |
31
- | Facts the producer declares | The check's `extensions["atlas.ui-state"]`: `route`, `fixture`, `cohort`, `area`, `flags` (a list), `covers` (the product surfaces the state shows). All optional. |
34
+ | Facts the producer declares | The check's `extensions["atlas.ui-state"]`: `route`, `fixture`, `cohort`, `area`, `kind` (the kind of UI state: `screen`, `overlay`, `loading`, `error`, `empty`… an open vocabulary that [critics](critics.md#reading-the-catalog) judge a state by), `flags` (a list), `covers` (the product surfaces the state shows). All optional. |
32
35
  | Facts that describe without splitting | The check's `labels` (`shard`, `browser`) and `tags`. Labels never make a different slot. |
33
36
  | Where it came from | The layer's `producer`, `sha`, `branch`, `runUrl`, `freshness`, `recordedAtMs`: shown with every capture, with the layer id and the backend that holds it. |
34
37
 
@@ -101,6 +104,35 @@ for a readiness check (bounded), photographs the page and returns the capture fo
101
104
  a `failed` capture that says why (with a picture of what it showed), never an exception that ends the run. Nothing that reads,
102
105
  composes or shows the catalog imports a collection framework, and a test keeps it that way.
103
106
 
107
+ ## The command line
108
+
109
+ `atlas ui-catalog` does the same without a script. Every subcommand prints a JSON document with `--format json`
110
+ ([`atlas ui-catalog --help`](reference/cli.md) lists every flag), and exits `0` it worked, `1` the answer is no, `2` the command was wrong.
111
+
112
+ ```sh
113
+ # The expected set under one profile. The file is {"states":[{"id":"checkout/payment","covers":["payment-form"]},{"id":"settings","inventory":true}]}.
114
+ atlas ui-catalog plan --states plan.json --producer catalog-plan --environment mobile-web --param viewport=390x844 --param scheme=dark
115
+
116
+ # The states one collector or shard captured. "screenshot" names a file beside captures.json.
117
+ atlas ui-catalog publish --captures captures.json --producer web-capture --environment mobile-web --param viewport=390x844 --param scheme=dark --shard 2/8
118
+
119
+ atlas ui-catalog status # exit 1 while anything failed, is missing, is still being captured or could not be read
120
+ atlas ui-catalog diff --from 3f9a2c1 --to 8b41d70 # two commits (the whole id, or at least 7 digits)
121
+ atlas ui-catalog export --out public/catalog.html # one file, no server
122
+ ```
123
+
124
+ `captures.json` is `{"states":[…]}` (or just the list); a state is `{"id","title"?,"status"?,"detail"?,"route"?,"fixture"?,"cohort"?,"area"?,
125
+ "flags"?,"covers"?,"labels"?,"tags"?,"durationMs"?,"screenshot"?}` with `screenshot` a file name or `{"path"|"uri","width"?,"height"?,"contentType"?}`.
126
+ A misspelled field is refused with its path, not ignored. A picture file is read from beside the file and from each `--attach-root`,
127
+ never from a hidden file or a link out. **A `passed` state whose picture cannot be read is published as `failed`, with the reason**: a
128
+ state without its picture is not shown as captured. `--recorded-at` defaults to the file's modification time, so publishing the same
129
+ file twice is the same layer.
130
+
131
+ `status` is the gate: it exits `1` unless the catalog holds a state and nothing needs attention (a failed capture, a state a plan expects
132
+ with nothing captured, a capture still in progress, a backend or layer that could not be read). Without a plan nothing is expected, so
133
+ nothing can be missing: it says so. A skipped cell is counted, not a problem. Schemas: `ui-catalog-publish-result`,
134
+ `ui-catalog-status`, `ui-catalog-diff-result`, `ui-catalog-export-result`.
135
+
104
136
  ## Sharding
105
137
 
106
138
  A shard is **any publisher that does not know the others**: by case, by profile, by area or by machine; Atlas does not care which.
@@ -140,10 +172,54 @@ and time. *Download the manifest* saves the filtered catalog as JSON.
140
172
  curl "$ATLAS_SERVER_URL/api/reports/ui-catalog?coverage=failed,missing&profile=…" # JSON; add format=markdown for text
141
173
  ```
142
174
 
175
+ A separate process that reviews the pictures (a [critic](critics.md#reading-the-catalog)) reads the same catalog as a flat list at
176
+ `GET /api/reports/ui-catalog/entries`: cursor-paged, `since` a report version, with the hash of each stored picture.
177
+
143
178
  `GET /api/reports/ui-catalog` answers the same document the viewer computes (`uiCatalog` from
144
179
  `@crvouga/atlas/reports/ui-catalog`), filtered by `q`, `coverage`, `profile`, `area`, `route`, `cohort`, `flag`, `surface` and
145
180
  `producer`, paged with `limit` and `offset`, with an ETag. Its picture `path` is relative to `/api/reports`.
146
181
 
182
+ ## Comparing two commits or two profiles
183
+
184
+ The viewer's **Compare** button on `/catalog` (`?compare=commit&from=<commit>&to=<commit>`, or `?compare=profile&from=<profile>&to=<profile>`,
185
+ so a link reproduces it) and `atlas ui-catalog diff` set one side against the other. Every pair is one state in one profile.
186
+
187
+ | A pair is | When |
188
+ | --- | --- |
189
+ | **Unchanged** | the same coverage and, when captured, the same stored picture |
190
+ | **Picture changed** | captured on both sides, and the picture's bytes (SHA-256) differ. This is not a measure of how many pixels differ |
191
+ | **Coverage changed** | it stands differently (captured against failed, failed against not captured yet…). A capture that did not pass is never compared by picture |
192
+ | **Cannot tell** | captured on both sides, but a picture has no content hash (it is given only by address, or was left out) |
193
+ | **Only after** / **Only before** | one side has no such cell. It may be new, renamed, or the other side's shard may not have run: it is not called a regression |
194
+
195
+ - **Two commits** compares the layers recorded at each commit. A layer replaces the older one of its slot, so the report keeps only the
196
+ newest capture of every state; the comparison therefore reads the layers themselves (the viewer asks for the layer list and the rows of
197
+ the two commits only; the command line reads the stores). Layers recorded from a checkout with uncommitted changes are not a commit's
198
+ own result and are left out. A commit is the whole id or at least 7 digits that name exactly one commit; anything else is an error
199
+ that lists the commits found, never a guess.
200
+ - **Two profiles** sets each state's cell in one profile beside its cell in another (desktop against mobile, light against dark) in
201
+ the report the viewer holds.
202
+ - A layer or backend that could not be read is named, and what it held is unknown, never "unchanged": `diff` exits `1` for it.
203
+ `--fail-on-change` also exits `1` for any difference, for a gate that must stay the same.
204
+ - The filters (words, area, route, cohort, flag, surface, producer) narrow a comparison; coverage does not, because coverage is what is
205
+ compared.
206
+
207
+ ## A single file that needs no server
208
+
209
+ `atlas ui-catalog export --out catalog.html` writes the catalog as **one HTML file**: the pictures embedded as `data:` addresses, the
210
+ manifest embedded as JSON (`<script type="application/json" id="ui-catalog">`), a search box and a coverage filter. Open it from a disk,
211
+ attach it to a ticket or serve it as a file. It is the catalog at the moment it was made, and says so; it does not stream (for that,
212
+ serve the store with `atlas serve` or build the site with `atlas client build`, which shows the same catalog).
213
+
214
+ - **Size.** Hosting platforms cap the size of one file. `--max-mb` (default 32) limits the embedded pictures; pictures past it are listed
215
+ in the page as left out, with the reason, in the order the page shows them, and the manifest stays complete. `--images none` embeds none.
216
+ `--manifest catalog.json` also writes the manifest as its own file.
217
+ - **Safe to open.** The page is generated from data written by test runs, and treats it so: text is escaped, only PNG, JPEG, GIF and WebP
218
+ bytes (by their first bytes, never SVG, never a layer's claimed type) are embedded, a picture given by address is linked only if it is
219
+ `http` or `https`, and a Content-Security-Policy forbids every network request. Without scripts every state is simply shown.
220
+ - It is a generated document, not the viewer: it carries its own small inline style and script because it must work with no build
221
+ and no server, which [the viewer's UI conventions](ui.md) forbid inside the viewer.
222
+
147
223
  ## Decisions
148
224
 
149
225
  The questions the original proposal left open, and how they are answered here. Each is the simplest answer that stays inside the
@@ -161,13 +237,34 @@ The questions the original proposal left open, and how they are answered here. E
161
237
  | What moves | Reading, composing, the viewer, the document and the API, the publish builders, a Playwright capture adapter. Staying with a product: its state declarations, its inventory, its fixtures and its runner. |
162
238
  | Where it lives | `@crvouga/atlas/reports/ui-catalog` (model), `…/ui-catalog-publish` (builders), `…/ui-state` (convention); the viewer's `/catalog`; the server's `/api/reports/ui-catalog`; `@crvouga/atlas/playwright`'s `captureUiState`. |
163
239
  | Exploration capabilities | Search, filters, profile comparison, coverage and surface coverage, failure diagnostics, deep links, manifest export. |
164
- | Static export | The catalog is computed from the layers a viewer holds, so a static export (`atlas client build`) shows it; it just does not stream. |
165
-
166
- ## What stays in your project, and what is not built yet
167
-
168
- **Stays in a project** (it is specific to the product): the list of states and profiles to capture, the inventory of surfaces,
169
- fixtures and test data, the runner that drives the product to each state, and the readiness conditions of each screen.
170
-
171
- **Not built yet:** a command line to list and publish a catalog without a script (use `atlas report publish --checks` today), a
172
- dedicated static single-file export, diff views that compare two commits of a state, and a hosting-platform publish step beyond
173
- `atlas serve`. The first slice makes the catalog explorable, streaming, shardable and independent of how it was collected.
240
+ | Static export | Two ways, neither needs a server. The catalog is computed from the layers a viewer holds, so the static site (`atlas client build`) shows it, without streaming. `atlas ui-catalog export` writes it as one self-contained HTML file with a size budget. |
241
+ | Comparing | Two commits (from the layers recorded at each, because the report keeps only the newest) or two profiles; pictures by content hash, never by a pixel measure; one-sided states are not regressions. |
242
+ | Command line | `atlas ui-catalog plan`, `publish`, `status`, `diff` and `export`, each with `--format json`. Running a collector is not one: that is the product's. |
243
+
244
+ ## What stays in your project, and what Atlas does not do
245
+
246
+ A project that had its own catalog moves each role it had to Atlas, and keeps only what is specific to its product. The eight roles, and
247
+ where each now is:
248
+
249
+ | # | Role | In Atlas | Stays in your project |
250
+ | --- | --- | --- | --- |
251
+ | 1 | Declaration of the states to capture, and the plan of what is expected | The plan format (`ui-plan` layers), `uiPlanLayer`, `atlas ui-catalog plan` | **The list itself**: which states and profiles your product has |
252
+ | 2 | A capture helper, with readiness checks and diagnostics | An optional adapter, `captureUiState` in `@crvouga/atlas/playwright`; any other tool writes the same layer | **The readiness condition of each screen**, and the helper for a framework Atlas has no adapter for |
253
+ | 3 | An evidence store: one atomic record per capture, images by content | Layers and the stores that hold them (`publishChecks`, a directory, S3, a server) | Nothing |
254
+ | 4 | An inventory of the product's surfaces, and reconciliation against it | Reconciliation: `inventory` plan entries and `covers`, the surface coverage table | **The inventory**: the list of surfaces your product has |
255
+ | 5 | A manifest and report builder | `uiCatalog`, the JSON manifest, Markdown, the single HTML file | Nothing |
256
+ | 6 | A viewer: as a single file, as a site, and live | `atlas ui-catalog export`, `atlas client build`, and `/catalog` on `atlas serve` | Nothing |
257
+ | 7 | A command line (list, run, report, publish, watch) | `atlas ui-catalog plan\|publish\|status\|diff\|export`; watching is the live `/catalog` and `atlas report stream` | **Run**: the runner that drives your product to each state |
258
+ | 8 | A publish step to a hosting platform | Not in Atlas, see below | **The deploy step** of your platform |
259
+
260
+ What stays is, in one list: the states and profiles to capture, the inventory of surfaces, the fixtures and test data, the runner that
261
+ drives the product to each state, the readiness condition of each screen, and the deploy step of your hosting platform. Nothing else is
262
+ specific to your product, so a project that installs a release holds none of the reporting and exploring code, only that.
263
+
264
+ **A hosting publish step is not a command, on purpose.** Publishing to a platform needs that platform's credentials and API, which Atlas
265
+ neither holds nor should. Hand the platform what Atlas produces: the `export` file (or a folder of them) for any host that serves
266
+ files, the output of `atlas client build` for a static site, or run `atlas serve` for a live one. Keep the credentials in the CI of the
267
+ project, where they already are.
268
+
269
+ **Not built:** reconciliation against an external inventory beyond `inventory` entries and `covers`; a comparison of pictures by pixels
270
+ (only by content); a comparison fetched from the server (the viewer and the command line read the layers themselves).
package/docs/ui.md CHANGED
@@ -119,6 +119,9 @@ same coverage state, revisions and recording in both.
119
119
  - The search words (`q`), the coverage filter (`coverage`), the selection (`behavior`) and the expanded state (`workspace`) are in the
120
120
  address. Selecting shows the Read / Watch / Inspect panel beside the list (below it on narrow screens), and *Read and watch* in the
121
121
  selected-behavior bar is reachable before the list in keyboard order.
122
+ - The same release controls as a feature page (*Release build*, *commit*, *platform*, and the *Stale or unverified* filter) sit above the list;
123
+ they are the address (`release`, `releaseSha`, `releasePlatform`, `evidence=stale`), and the selected behavior and the workspace read their
124
+ evidence against that release.
122
125
  - **Read and watch workspace.** Expanding a behavior replaces the index with a page that uses the width: a two-column
123
126
  `SimpleGrid` (one column below `md`) with *What should happen* (the steps, an outline's example filled in) beside *Recording* (the
124
127
  player, status and provenance, promise and evidence revisions, applicability), the scoped summary below, and Inspect behind a
@@ -135,6 +138,39 @@ same coverage state, revisions and recording in both.
135
138
  budget and breaches). Absent evidence is stated as unknown, never as good; the test's duration is not product latency, and the
136
139
  summary says it describes the checks Atlas holds, not production. The full reliability and latency detail is one link away.
137
140
 
141
+ ## The review destination
142
+
143
+ A team asks four connected questions of one project: what behavior it promises (**Read**), what that looks like (**Watch**), what
144
+ evidence supports it (**Inspect**) and where the gaps are. The viewer answers them in three places that lead to one another, and
145
+ `tests/review-destination.test.tsx` walks the whole workflow against a real server, one test per acceptance behavior of the
146
+ tracking issue (#20).
147
+
148
+ | Step | Where | Built by |
149
+ | --- | --- | --- |
150
+ | Find a promise by approximate words, a domain, a device and recordings; see each in its own coverage state | **Behaviors** (`/behaviors`) | #55, #74, #57, #44 |
151
+ | Name the build and device to read the evidence against | the applicability controls of Behaviors and of a feature | #61, #73 |
152
+ | Read what should happen beside the recording, with the scoped outcome, reliability and latency | the Read and watch workspace | #74, #60, #62, #63 |
153
+ | Find recordings by tag and media type and play them as a labelled playlist | **Artifacts** (`?tab=artifacts`), Watch these N results | #49, #56, #59, #75 |
154
+ | Go from a clip to its promise (**Read the promise**, also in a test's detail) or to its exact run (**Developer details**) | the playlist | #47, #54 |
155
+ | See which run and execution a result came from, and which recordings never arrived or are gone | **Runs** (`?tab=runs`) | #45, #46, #48, #70, #72, #76 |
156
+ | Seek to the step in a recording and share the moment | Timeline, Watch | #68 |
157
+ | Freeze a selection with notes and gaps | **Collections** | #66 |
158
+ | Compare two pinned runs | **Compare** | #67 |
159
+
160
+ How the pieces stay honest, and what a test in that file holds them to:
161
+
162
+ - The four states (not executed, API only, failed with a recording, demonstrated) are told apart in words and never counted as one
163
+ pass. A recording that was expected and not uploaded, or stored and then lost, is shown beside its green tests, never in place of them.
164
+ - Behaviors, Runs and Videos and artifacts link to one another (the header of Behaviors and the Specs block of the explorer), and a
165
+ result in the playlist or in a test's detail links back to the exact check of its feature. Read names the check and no run;
166
+ Inspect names the run (`layer`), so it never falls back to the latest combined evidence.
167
+ - The address holds the scope: the release (`release`, `releaseSha`, `releasePlatform`) and `evidence=stale` on Behaviors as on a feature,
168
+ the filters and the open playlist on Artifacts, the pinned runs on Compare, and the revision of a frozen collection.
169
+ A frozen collection does not change when newer runs arrive; the filtered view it was frozen from does.
170
+ - An older producer (no spec fingerprint, no recording expectation, no tags) or an older server (no artifact search, read-back,
171
+ collections or health route) stays readable. What it cannot say is unknown: the revision is "not recorded", a run is not "complete",
172
+ and an unreported behavior is "Not executed", never passed.
173
+
138
174
  ## Catalog health
139
175
 
140
176
  The **Catalog health** tab (`explorer/HealthView.tsx`, `?tab=health`; `@crvouga/atlas/reports/catalog-health`) explains why the Specs
@@ -167,6 +203,36 @@ it works without a server. Cards are Mantine `Card`s in a responsive `SimpleGrid
167
203
  incomplete. Waiting follows the loading policy below, and layers still arriving are counted.
168
204
  - Pictures are `loading="lazy"` and only the cards on screen are fetched. The grid is bounded with *Show more*.
169
205
  - The open state shows every profile side by side with the layer, backend, producer, shard, commit and time of each picture.
206
+ - **Compare** (`catalog/UiCatalogCompare.tsx`, `?compare=commit|profile&from&to`) sets two commits or two profiles against each other. Two
207
+ commits need the layers a newer layer replaced, which the held report no longer has, so `catalog/history.ts` reads the layer list and
208
+ the rows of the two commits only while the comparison is open (sharing the rows cache). Say what "changed" means (the stored bytes
209
+ differ, not a pixel measure), never call a one-sided state a regression, and say a layer that could not be read is unknown. The
210
+ coverage filter does not apply to a comparison.
211
+
212
+ ## Needs polish
213
+
214
+ `/critiques` (`critiques/`, in the spec-type menu once a critic has recorded an opinion) ranks the captured UI states one critic
215
+ judged, lowest first ([the guide](critics.md#the-viewer-needs-polish)). It is computed in the page from the layers the viewer already
216
+ holds (`critiques/ranking.ts`), so a critique appears as its layer arrives.
217
+
218
+ - A critique is an opinion: no `StatusMark`, no passed or failed words, never in a test count. Use a Mantine `Badge` with the
219
+ label the critic gave (an open string), the severity as text, and never a color alone.
220
+ - A state judged at an earlier picture, or that the critic could not judge, is listed in its own table with the reason and is not
221
+ ranked. Unknown categories and severities are shown as written.
222
+ - The critic and the state kind are `NativeSelect`s whose values are the address (`critic`, `kind`, `entry`); the open entry is a
223
+ `Drawer` with the findings in a `Table` and the provenance in an `Accordion`.
224
+ - Pictures are `loading="lazy"`. Waiting, an unavailable backend and incomplete layers are named as on the UI catalog.
225
+ - **Finding overlays** (`critiques/FindingOverlay.tsx`): in the open entry each finding with a region is a numbered box over the picture,
226
+ positioned with Mantine style props (`pos`, `left`, `top`, `w`, `h` as percentages of the picture, `bd`) and a numbered `Badge`; no
227
+ `style`, `className` or stylesheet. The number is the finding's row in the table (where a `Button` locates it, `aria-pressed`), so color
228
+ is never the only signal; the boxes are `aria-hidden` because the table says the same in words, and a `Switch` hides them. A region in
229
+ an unknown unit, or of a critique that judged an earlier picture, is not drawn.
230
+ - **Agreement** is a `Tabs` view of the same page (`?view=agreement`, `keepMounted={false}`): a `Table` with one column per critic, each
231
+ cell a `Badge` in words (*Flags it*, *Does not flag it*, *No view*) with a link to that critic's critique, and the outcome as text.
232
+ The rule for a stance is stated on the page (`reports/critique-review`); a score without standing or a failure gives no view.
233
+ - **Marks** are read, never written, by the viewer (a browser sign-in only looks): each finding and the whole opinion show a `Badge` in
234
+ words (*Accepted by*, *Dismissed by*, *Reviewers disagree*, *Not reviewed*) with the reviewers' notes, and the page names
235
+ `atlas critic review`. No mark uses a passed or failed word or a status color.
170
236
 
171
237
  ## Reliability
172
238
 
@@ -255,7 +321,8 @@ over the same filtered set the page lists, in the same order (`explorer/playlist
255
321
  Speed, "Play the next clip when one ends" and "Repeat" are labelled controls. Captions (`text/vtt` attachments) are offered
256
322
  as `<track>` elements.
257
323
  - The scenario (title, recorded steps, why it failed) sits beside the player with the recording's scope badges
258
- (paced, scripted, silent, synthetic) and the sentence that the length is the recording's, not production latency. Developer
324
+ (paced, scripted, silent, synthetic) and the sentence that the length is the recording's, not production latency. **Read the promise** opens the feature on that
325
+ exact check (the playlist stays in the address, so Back returns to the same clip); Developer
259
326
  details opens the test pinned to that run's record and closes the playlist.
260
327
 
261
328
  ## Steps and moments in a recording
@@ -384,7 +451,7 @@ The narrowly scoped exceptions are:
384
451
  | Area | Screens/interactions | Mantine composition |
385
452
  | --- | --- | --- |
386
453
  | Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
387
- | UI catalog | Captured UI states per profile as cards with coverage badges, filters in the index column, surface coverage, an open state with its profiles side by side and where each picture came from, manifest download | AppShell navbar, SimpleGrid, Card, Image, MultiSelect, TextInput, Drawer, Accordion, Table, Badge, Alert |
454
+ | UI catalog | Captured UI states per profile as cards with coverage badges, filters in the index column, surface coverage, an open state with its profiles side by side and where each picture came from, manifest download, a Compare of two commits or profiles (SegmentedControl, NativeSelect, Switch) | AppShell navbar, SimpleGrid, Card, Image, MultiSelect, TextInput, Drawer, Accordion, Table, Badge, Alert |
388
455
  | Project home | Tests/runs/artifacts/compare/catalog health tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
389
456
  | Test detail | Current outcomes, statistics, duration graph, product latency with budgets and baselines, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
390
457
  | Specs | All six existing kinds, overview/index with title search and folder or declared-domain grouping, Gherkin rules/scenarios/examples with a coverage summary and Read / Watch / Inspect per behavior, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Tabs, Table, CodeHighlightTabs, ResultRow |