@crvouga/atlas 1.19.0 → 1.21.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 (48) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/core/src/cli.d.ts +1 -1
  3. package/dist/core/src/cli.js +3 -0
  4. package/dist/reports/src/compose.d.ts +23 -1
  5. package/dist/reports/src/compose.js +29 -19
  6. package/dist/reports/src/report-cli.d.ts +13 -0
  7. package/dist/reports/src/report-cli.js +6 -4
  8. package/dist/reports/src/ui-catalog-cli.d.ts +10 -0
  9. package/dist/reports/src/ui-catalog-cli.js +391 -0
  10. package/dist/reports/src/ui-catalog-diff.d.ts +104 -0
  11. package/dist/reports/src/ui-catalog-diff.js +130 -0
  12. package/dist/reports/src/ui-catalog-export.d.ts +82 -0
  13. package/dist/reports/src/ui-catalog-export.js +221 -0
  14. package/dist/schema/src/api.d.ts +55 -0
  15. package/dist/schema/src/api.js +33 -0
  16. package/dist/schema/src/cli-results.d.ts +318 -1
  17. package/dist/schema/src/cli-results.js +89 -1
  18. package/dist/server/src/app.js +3 -1
  19. package/dist/server/src/openapi.d.ts +1 -1
  20. package/dist/server/src/openapi.js +24 -2
  21. package/dist/server/src/recording-verification.d.ts +34 -0
  22. package/dist/server/src/recording-verification.js +118 -0
  23. package/docs/agents.md +5 -0
  24. package/docs/reference/cli.md +56 -1
  25. package/docs/reference/http-api.md +52 -0
  26. package/docs/reference/reports.md +392 -0
  27. package/docs/reference/schema.md +82 -1
  28. package/docs/reference/server.md +39 -0
  29. package/docs/rich-evidence.md +12 -0
  30. package/docs/server.md +12 -0
  31. package/docs/ui-catalog.md +106 -12
  32. package/docs/ui.md +6 -1
  33. package/llms-full.txt +246 -11
  34. package/package.json +1 -1
  35. package/packages/schema/schemas/ui-catalog-diff-result.schema.json +201 -0
  36. package/packages/schema/schemas/ui-catalog-export-result.schema.json +83 -0
  37. package/packages/schema/schemas/ui-catalog-publish-result.schema.json +116 -0
  38. package/packages/schema/schemas/ui-catalog-status.schema.json +197 -0
  39. package/packages/server/openapi.json +527 -1
  40. package/packages/visualizer/src/catalog/Picture.tsx +32 -0
  41. package/packages/visualizer/src/catalog/UiCatalogCompare.tsx +289 -0
  42. package/packages/visualizer/src/catalog/UiCatalogPage.tsx +18 -28
  43. package/packages/visualizer/src/catalog/history.ts +61 -0
  44. package/packages/visualizer/src/catalog/search.ts +14 -3
  45. package/packages/visualizer/src/evidence/evidence-context.tsx +2 -1
  46. package/packages/visualizer/src/explorer/RunsView.tsx +32 -6
  47. package/packages/visualizer/src/explorer/catalog.tsx +24 -1
  48. package/packages/visualizer/src/explorer/recording-verification.ts +58 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module Command results (`--format json`)
3
3
  *
4
- * What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings` print on standard output with `--format json` (alias
4
+ * What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings`, and `atlas ui-catalog plan`, `publish`, `status`, `diff` and `export`, print on standard output with `--format json` (alias
5
5
  * `--json`), and what any `atlas` command prints on standard error for a command that was wrong: one JSON document,
6
6
  * so an agent or a script never has to parse prose. (`status` and `policy` already print documents:
7
7
  * `ReportStatusSchema`, and an IAM policy.) Each is also published as a JSON Schema.
@@ -289,6 +289,153 @@ export declare const InitPlanSchema: z.ZodObject<{
289
289
  nextSteps: z.ZodArray<z.ZodString>;
290
290
  warnings: z.ZodArray<z.ZodString>;
291
291
  }, z.core.$loose>;
292
+ /** What `atlas ui-catalog plan --format json` and `atlas ui-catalog publish --format json` print: the one layer that was written. */
293
+ export declare const UiCatalogPublishResultSchema: z.ZodObject<{
294
+ ok: z.ZodLiteral<true>;
295
+ store: z.ZodString;
296
+ layer: z.ZodObject<{
297
+ id: z.ZodString;
298
+ kind: z.ZodEnum<{
299
+ "ui-state": "ui-state";
300
+ "ui-plan": "ui-plan";
301
+ }>;
302
+ producer: z.ZodString;
303
+ environment: z.ZodNullable<z.ZodString>;
304
+ parameters: z.ZodRecord<z.ZodString, z.ZodString>;
305
+ states: z.ZodNumber;
306
+ captured: z.ZodNumber;
307
+ failed: z.ZodNumber;
308
+ pictures: z.ZodNumber;
309
+ pictureBytes: z.ZodNumber;
310
+ shard: z.ZodNullable<z.ZodString>;
311
+ }, z.core.$loose>;
312
+ warnings: z.ZodArray<z.ZodString>;
313
+ }, z.core.$loose>;
314
+ /** One thing that keeps a UI catalog from being complete and good. */
315
+ export declare const UiCatalogProblemSchema: z.ZodObject<{
316
+ kind: z.ZodEnum<{
317
+ failed: "failed";
318
+ missing: "missing";
319
+ unavailable: "unavailable";
320
+ capturing: "capturing";
321
+ unreadable: "unreadable";
322
+ empty: "empty";
323
+ }>;
324
+ state: z.ZodOptional<z.ZodString>;
325
+ profile: z.ZodOptional<z.ZodString>;
326
+ detail: z.ZodOptional<z.ZodString>;
327
+ }, z.core.$loose>;
328
+ /** What `atlas ui-catalog status --format json` prints. */
329
+ export declare const UiCatalogStatusSchema: z.ZodObject<{
330
+ ok: z.ZodBoolean;
331
+ summary: z.ZodObject<{
332
+ states: z.ZodNumber;
333
+ profiles: z.ZodNumber;
334
+ cells: z.ZodNumber;
335
+ coverage: z.ZodRecord<z.ZodEnum<{
336
+ failed: "failed";
337
+ skipped: "skipped";
338
+ missing: "missing";
339
+ captured: "captured";
340
+ capturing: "capturing";
341
+ }>, z.ZodNumber>;
342
+ undeclared: z.ZodNumber;
343
+ planned: z.ZodBoolean;
344
+ surfaces: z.ZodNumber;
345
+ uncoveredSurfaces: z.ZodNumber;
346
+ }, z.core.$loose>;
347
+ profiles: z.ZodArray<z.ZodObject<{
348
+ key: z.ZodString;
349
+ label: z.ZodString;
350
+ }, z.core.$loose>>;
351
+ problems: z.ZodArray<z.ZodObject<{
352
+ kind: z.ZodEnum<{
353
+ failed: "failed";
354
+ missing: "missing";
355
+ unavailable: "unavailable";
356
+ capturing: "capturing";
357
+ unreadable: "unreadable";
358
+ empty: "empty";
359
+ }>;
360
+ state: z.ZodOptional<z.ZodString>;
361
+ profile: z.ZodOptional<z.ZodString>;
362
+ detail: z.ZodOptional<z.ZodString>;
363
+ }, z.core.$loose>>;
364
+ sources: z.ZodObject<{
365
+ layers: z.ZodNumber;
366
+ backends: z.ZodArray<z.ZodObject<{
367
+ id: z.ZodString;
368
+ label: z.ZodString;
369
+ available: z.ZodBoolean;
370
+ }, z.core.$loose>>;
371
+ notices: z.ZodArray<z.ZodString>;
372
+ }, z.core.$loose>;
373
+ }, z.core.$loose>;
374
+ /** What `atlas ui-catalog diff --format json` prints. */
375
+ export declare const UiCatalogDiffResultSchema: z.ZodObject<{
376
+ ok: z.ZodBoolean;
377
+ from: z.ZodObject<{
378
+ sha: z.ZodString;
379
+ states: z.ZodNumber;
380
+ layers: z.ZodNumber;
381
+ }, z.core.$loose>;
382
+ to: z.ZodObject<{
383
+ sha: z.ZodString;
384
+ states: z.ZodNumber;
385
+ layers: z.ZodNumber;
386
+ }, z.core.$loose>;
387
+ summary: z.ZodObject<{
388
+ coverage: z.ZodNumber;
389
+ picture: z.ZodNumber;
390
+ unverified: z.ZodNumber;
391
+ added: z.ZodNumber;
392
+ removed: z.ZodNumber;
393
+ unchanged: z.ZodNumber;
394
+ total: z.ZodNumber;
395
+ }, z.core.$loose>;
396
+ changes: z.ZodArray<z.ZodObject<{
397
+ state: z.ZodString;
398
+ title: z.ZodString;
399
+ profile: z.ZodString;
400
+ change: z.ZodEnum<{
401
+ coverage: "coverage";
402
+ removed: "removed";
403
+ unverified: "unverified";
404
+ added: "added";
405
+ picture: "picture";
406
+ }>;
407
+ from: z.ZodNullable<z.ZodEnum<{
408
+ failed: "failed";
409
+ skipped: "skipped";
410
+ missing: "missing";
411
+ captured: "captured";
412
+ capturing: "capturing";
413
+ }>>;
414
+ to: z.ZodNullable<z.ZodEnum<{
415
+ failed: "failed";
416
+ skipped: "skipped";
417
+ missing: "missing";
418
+ captured: "captured";
419
+ capturing: "capturing";
420
+ }>>;
421
+ }, z.core.$loose>>;
422
+ notices: z.ZodArray<z.ZodString>;
423
+ }, z.core.$loose>;
424
+ /** What `atlas ui-catalog export --format json` prints. */
425
+ export declare const UiCatalogExportResultSchema: z.ZodObject<{
426
+ ok: z.ZodLiteral<true>;
427
+ out: z.ZodString;
428
+ bytes: z.ZodNumber;
429
+ states: z.ZodNumber;
430
+ cells: z.ZodNumber;
431
+ pictures: z.ZodObject<{
432
+ embedded: z.ZodNumber;
433
+ embeddedBytes: z.ZodNumber;
434
+ left: z.ZodNumber;
435
+ }, z.core.$loose>;
436
+ manifest: z.ZodNullable<z.ZodString>;
437
+ warnings: z.ZodArray<z.ZodString>;
438
+ }, z.core.$loose>;
292
439
  /**
293
440
  * JSON command contracts, shared by schema publication, agent documentation and executable command checks.
294
441
  * A new result needs an entry here and a runtime scenario in the documentation contract tests.
@@ -582,6 +729,176 @@ export declare const CLI_RESULTS: {
582
729
  notices: z.ZodArray<z.ZodString>;
583
730
  }, z.core.$loose>;
584
731
  };
732
+ readonly uiCatalogPlan: {
733
+ readonly command: "atlas ui-catalog plan";
734
+ readonly schemaName: "ui-catalog-publish-result";
735
+ readonly schema: z.ZodObject<{
736
+ ok: z.ZodLiteral<true>;
737
+ store: z.ZodString;
738
+ layer: z.ZodObject<{
739
+ id: z.ZodString;
740
+ kind: z.ZodEnum<{
741
+ "ui-state": "ui-state";
742
+ "ui-plan": "ui-plan";
743
+ }>;
744
+ producer: z.ZodString;
745
+ environment: z.ZodNullable<z.ZodString>;
746
+ parameters: z.ZodRecord<z.ZodString, z.ZodString>;
747
+ states: z.ZodNumber;
748
+ captured: z.ZodNumber;
749
+ failed: z.ZodNumber;
750
+ pictures: z.ZodNumber;
751
+ pictureBytes: z.ZodNumber;
752
+ shard: z.ZodNullable<z.ZodString>;
753
+ }, z.core.$loose>;
754
+ warnings: z.ZodArray<z.ZodString>;
755
+ }, z.core.$loose>;
756
+ };
757
+ readonly uiCatalogPublish: {
758
+ readonly command: "atlas ui-catalog publish";
759
+ readonly schemaName: "ui-catalog-publish-result";
760
+ readonly schema: z.ZodObject<{
761
+ ok: z.ZodLiteral<true>;
762
+ store: z.ZodString;
763
+ layer: z.ZodObject<{
764
+ id: z.ZodString;
765
+ kind: z.ZodEnum<{
766
+ "ui-state": "ui-state";
767
+ "ui-plan": "ui-plan";
768
+ }>;
769
+ producer: z.ZodString;
770
+ environment: z.ZodNullable<z.ZodString>;
771
+ parameters: z.ZodRecord<z.ZodString, z.ZodString>;
772
+ states: z.ZodNumber;
773
+ captured: z.ZodNumber;
774
+ failed: z.ZodNumber;
775
+ pictures: z.ZodNumber;
776
+ pictureBytes: z.ZodNumber;
777
+ shard: z.ZodNullable<z.ZodString>;
778
+ }, z.core.$loose>;
779
+ warnings: z.ZodArray<z.ZodString>;
780
+ }, z.core.$loose>;
781
+ };
782
+ readonly uiCatalogStatus: {
783
+ readonly command: "atlas ui-catalog status";
784
+ readonly schemaName: "ui-catalog-status";
785
+ readonly schema: z.ZodObject<{
786
+ ok: z.ZodBoolean;
787
+ summary: z.ZodObject<{
788
+ states: z.ZodNumber;
789
+ profiles: z.ZodNumber;
790
+ cells: z.ZodNumber;
791
+ coverage: z.ZodRecord<z.ZodEnum<{
792
+ failed: "failed";
793
+ skipped: "skipped";
794
+ missing: "missing";
795
+ captured: "captured";
796
+ capturing: "capturing";
797
+ }>, z.ZodNumber>;
798
+ undeclared: z.ZodNumber;
799
+ planned: z.ZodBoolean;
800
+ surfaces: z.ZodNumber;
801
+ uncoveredSurfaces: z.ZodNumber;
802
+ }, z.core.$loose>;
803
+ profiles: z.ZodArray<z.ZodObject<{
804
+ key: z.ZodString;
805
+ label: z.ZodString;
806
+ }, z.core.$loose>>;
807
+ problems: z.ZodArray<z.ZodObject<{
808
+ kind: z.ZodEnum<{
809
+ failed: "failed";
810
+ missing: "missing";
811
+ unavailable: "unavailable";
812
+ capturing: "capturing";
813
+ unreadable: "unreadable";
814
+ empty: "empty";
815
+ }>;
816
+ state: z.ZodOptional<z.ZodString>;
817
+ profile: z.ZodOptional<z.ZodString>;
818
+ detail: z.ZodOptional<z.ZodString>;
819
+ }, z.core.$loose>>;
820
+ sources: z.ZodObject<{
821
+ layers: z.ZodNumber;
822
+ backends: z.ZodArray<z.ZodObject<{
823
+ id: z.ZodString;
824
+ label: z.ZodString;
825
+ available: z.ZodBoolean;
826
+ }, z.core.$loose>>;
827
+ notices: z.ZodArray<z.ZodString>;
828
+ }, z.core.$loose>;
829
+ }, z.core.$loose>;
830
+ };
831
+ readonly uiCatalogDiff: {
832
+ readonly command: "atlas ui-catalog diff";
833
+ readonly schemaName: "ui-catalog-diff-result";
834
+ readonly schema: z.ZodObject<{
835
+ ok: z.ZodBoolean;
836
+ from: z.ZodObject<{
837
+ sha: z.ZodString;
838
+ states: z.ZodNumber;
839
+ layers: z.ZodNumber;
840
+ }, z.core.$loose>;
841
+ to: z.ZodObject<{
842
+ sha: z.ZodString;
843
+ states: z.ZodNumber;
844
+ layers: z.ZodNumber;
845
+ }, z.core.$loose>;
846
+ summary: z.ZodObject<{
847
+ coverage: z.ZodNumber;
848
+ picture: z.ZodNumber;
849
+ unverified: z.ZodNumber;
850
+ added: z.ZodNumber;
851
+ removed: z.ZodNumber;
852
+ unchanged: z.ZodNumber;
853
+ total: z.ZodNumber;
854
+ }, z.core.$loose>;
855
+ changes: z.ZodArray<z.ZodObject<{
856
+ state: z.ZodString;
857
+ title: z.ZodString;
858
+ profile: z.ZodString;
859
+ change: z.ZodEnum<{
860
+ coverage: "coverage";
861
+ removed: "removed";
862
+ unverified: "unverified";
863
+ added: "added";
864
+ picture: "picture";
865
+ }>;
866
+ from: z.ZodNullable<z.ZodEnum<{
867
+ failed: "failed";
868
+ skipped: "skipped";
869
+ missing: "missing";
870
+ captured: "captured";
871
+ capturing: "capturing";
872
+ }>>;
873
+ to: z.ZodNullable<z.ZodEnum<{
874
+ failed: "failed";
875
+ skipped: "skipped";
876
+ missing: "missing";
877
+ captured: "captured";
878
+ capturing: "capturing";
879
+ }>>;
880
+ }, z.core.$loose>>;
881
+ notices: z.ZodArray<z.ZodString>;
882
+ }, z.core.$loose>;
883
+ };
884
+ readonly uiCatalogExport: {
885
+ readonly command: "atlas ui-catalog export";
886
+ readonly schemaName: "ui-catalog-export-result";
887
+ readonly schema: z.ZodObject<{
888
+ ok: z.ZodLiteral<true>;
889
+ out: z.ZodString;
890
+ bytes: z.ZodNumber;
891
+ states: z.ZodNumber;
892
+ cells: z.ZodNumber;
893
+ pictures: z.ZodObject<{
894
+ embedded: z.ZodNumber;
895
+ embeddedBytes: z.ZodNumber;
896
+ left: z.ZodNumber;
897
+ }, z.core.$loose>;
898
+ manifest: z.ZodNullable<z.ZodString>;
899
+ warnings: z.ZodArray<z.ZodString>;
900
+ }, z.core.$loose>;
901
+ };
585
902
  readonly error: {
586
903
  readonly command: "a command that was wrong (standard error)";
587
904
  readonly schemaName: "command-error";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module Command results (`--format json`)
3
3
  *
4
- * What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings` print on standard output with `--format json` (alias
4
+ * What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings`, and `atlas ui-catalog plan`, `publish`, `status`, `diff` and `export`, print on standard output with `--format json` (alias
5
5
  * `--json`), and what any `atlas` command prints on standard error for a command that was wrong: one JSON document,
6
6
  * so an agent or a script never has to parse prose. (`status` and `policy` already print documents:
7
7
  * `ReportStatusSchema`, and an IAM policy.) Each is also published as a JSON Schema.
@@ -154,6 +154,89 @@ export const InitPlanSchema = z.looseObject({
154
154
  nextSteps: z.array(z.string()).describe('What to do next, in order, as plain instructions an agent can follow'),
155
155
  warnings: z.array(z.string())
156
156
  });
157
+ /** The coverage of one state in one profile, as the UI catalog words it. */
158
+ const UiCoverageSchema = z.enum(['captured', 'failed', 'capturing', 'skipped', 'missing']);
159
+ /** What `atlas ui-catalog plan --format json` and `atlas ui-catalog publish --format json` print: the one layer that was written. */
160
+ export const UiCatalogPublishResultSchema = z.looseObject({
161
+ ok: z.literal(true).describe('True: the layer was written. A state whose picture could not be read is published as `failed` and named in `warnings`; it does not fail the command'),
162
+ store: z.string().describe('Where it was written: a folder, `s3://…` or a server URL'),
163
+ layer: z.looseObject({
164
+ id: z.string().describe('The layer id: 64 hex digits, the SHA-256 of its content. Publishing the same input again gives the same id'),
165
+ kind: z.enum(['ui-state', 'ui-plan']).describe('`ui-state`: captures (`publish`). `ui-plan`: the expected set (`plan`)'),
166
+ producer: z.string(),
167
+ environment: z.string().nullable(),
168
+ parameters: z.record(z.string(), z.string()).describe('The rest of the profile, such as `{ "viewport": "390x844" }`'),
169
+ states: z.number().int().nonnegative().describe('States in the layer'),
170
+ captured: z.number().int().nonnegative().describe('For `publish`: states that were captured (`passed`). Zero for a plan'),
171
+ failed: z.number().int().nonnegative().describe('For `publish`: states recorded as failed, including those whose picture could not be read'),
172
+ pictures: z.number().int().nonnegative().describe('Pictures stored in the layer'),
173
+ pictureBytes: z.number().int().nonnegative(),
174
+ shard: z.string().nullable().describe('The `--shard` label, which describes the layer and never splits its slot')
175
+ }),
176
+ warnings: z.array(z.string()).describe('What was skipped and why, such as a picture file that could not be read. Never fatal')
177
+ });
178
+ /** One thing that keeps a UI catalog from being complete and good. */
179
+ export const UiCatalogProblemSchema = z.looseObject({
180
+ kind: z.enum(['failed', 'missing', 'capturing', 'unavailable', 'unreadable', 'empty']).describe('`failed`: a capture was attempted and failed. `missing`: a plan expects it and nothing was captured. `capturing`: still being taken, so not known. `unavailable`: a backend could not be read. `unreadable`: a layer could not be read. `empty`: the catalog holds no state at all'),
181
+ state: z.string().optional().describe('The state id, for `failed`, `missing` and `capturing`'),
182
+ profile: z.string().optional().describe('The profile label, for `failed`, `missing` and `capturing`'),
183
+ detail: z.string().optional().describe('Why, when the producer said')
184
+ });
185
+ /** What `atlas ui-catalog status --format json` prints. */
186
+ export const UiCatalogStatusSchema = z.looseObject({
187
+ ok: z.boolean().describe('True only when the catalog holds a state and has no problem: nothing failed, nothing expected is missing, nothing is still being captured, and every backend and layer was read (otherwise the exit code is 1). Skipped cells are counted, not problems'),
188
+ summary: z.looseObject({
189
+ states: z.number().int().nonnegative(),
190
+ profiles: z.number().int().nonnegative(),
191
+ cells: z.number().int().nonnegative().describe('States counted once per profile'),
192
+ coverage: z.record(UiCoverageSchema, z.number().int().nonnegative()).describe('Cells by coverage'),
193
+ undeclared: z.number().int().nonnegative().describe('Captured cells no plan expects; zero when no plan exists'),
194
+ planned: z.boolean().describe('Whether any plan was published. Without one, `missing` is always zero because what should exist is unknown'),
195
+ surfaces: z.number().int().nonnegative(),
196
+ uncoveredSurfaces: z.number().int().nonnegative()
197
+ }),
198
+ profiles: z.array(z.looseObject({ key: z.string(), label: z.string() })),
199
+ problems: z.array(UiCatalogProblemSchema),
200
+ sources: z.looseObject({ layers: z.number().int().nonnegative(), backends: z.array(z.looseObject({ id: z.string(), label: z.string(), available: z.boolean() })), notices: z.array(z.string()) })
201
+ });
202
+ /** One side of `atlas ui-catalog diff`. */
203
+ const UiCatalogCommitSchema = z.looseObject({ sha: z.string(), states: z.number().int().nonnegative(), layers: z.number().int().nonnegative() });
204
+ /** What `atlas ui-catalog diff --format json` prints. */
205
+ export const UiCatalogDiffResultSchema = z.looseObject({
206
+ ok: z.boolean().describe('False when a layer or backend could not be read (the comparison may be wrong), or, with `--fail-on-change`, when anything differs. Otherwise true: a difference is not a failure by itself'),
207
+ from: UiCatalogCommitSchema,
208
+ to: UiCatalogCommitSchema,
209
+ summary: z.looseObject({
210
+ coverage: z.number().int().nonnegative().describe('Pairs that stand differently (captured against failed, for example)'),
211
+ picture: z.number().int().nonnegative().describe('Both captured, and the stored bytes differ. Not a measure of how many pixels'),
212
+ unverified: z.number().int().nonnegative().describe('Both captured, but a side has no content hash, so whether the picture changed is unknown'),
213
+ added: z.number().int().nonnegative().describe('Only `to` has it'),
214
+ removed: z.number().int().nonnegative().describe('Only `from` has it. Not a regression: the other side may not have run it'),
215
+ unchanged: z.number().int().nonnegative(),
216
+ total: z.number().int().nonnegative()
217
+ }),
218
+ changes: z.array(z.looseObject({
219
+ state: z.string(), title: z.string(), profile: z.string(),
220
+ change: z.enum(['coverage', 'picture', 'unverified', 'added', 'removed']),
221
+ from: UiCoverageSchema.nullable(), to: UiCoverageSchema.nullable()
222
+ })).describe('Every pair that is not unchanged'),
223
+ notices: z.array(z.string()).describe('What could not be read. Any makes `ok` false: an unreadable layer is unknown, never unchanged')
224
+ });
225
+ /** What `atlas ui-catalog export --format json` prints. */
226
+ export const UiCatalogExportResultSchema = z.looseObject({
227
+ ok: z.literal(true),
228
+ out: z.string().describe('The HTML file that was written'),
229
+ bytes: z.number().int().nonnegative().describe('Its size'),
230
+ states: z.number().int().nonnegative(),
231
+ cells: z.number().int().nonnegative(),
232
+ pictures: z.looseObject({
233
+ embedded: z.number().int().nonnegative(),
234
+ embeddedBytes: z.number().int().nonnegative(),
235
+ left: z.number().int().nonnegative().describe('Pictures not embedded (over the size limit, not stored, not an image, or `--images none`); each is listed on the page with the reason')
236
+ }),
237
+ manifest: z.string().nullable().describe('The JSON manifest written beside it with `--manifest`, or null'),
238
+ warnings: z.array(z.string()).describe('Backends and layers that could not be read, and pictures left out. The page says so too')
239
+ });
157
240
  /**
158
241
  * JSON command contracts, shared by schema publication, agent documentation and executable command checks.
159
242
  * A new result needs an entry here and a runtime scenario in the documentation contract tests.
@@ -166,5 +249,10 @@ export const CLI_RESULTS = {
166
249
  doctor: { command: 'atlas report doctor', schemaName: 'doctor-result', schema: DoctorResultSchema },
167
250
  status: { command: 'atlas report status', schemaName: 'report-status', schema: ReportStatusSchema },
168
251
  verifyRecordings: { command: 'atlas report verify-recordings', schemaName: 'verify-recordings-result', schema: VerifyRecordingsResultSchema },
252
+ uiCatalogPlan: { command: 'atlas ui-catalog plan', schemaName: 'ui-catalog-publish-result', schema: UiCatalogPublishResultSchema },
253
+ uiCatalogPublish: { command: 'atlas ui-catalog publish', schemaName: 'ui-catalog-publish-result', schema: UiCatalogPublishResultSchema },
254
+ uiCatalogStatus: { command: 'atlas ui-catalog status', schemaName: 'ui-catalog-status', schema: UiCatalogStatusSchema },
255
+ uiCatalogDiff: { command: 'atlas ui-catalog diff', schemaName: 'ui-catalog-diff-result', schema: UiCatalogDiffResultSchema },
256
+ uiCatalogExport: { command: 'atlas ui-catalog export', schemaName: 'ui-catalog-export-result', schema: UiCatalogExportResultSchema },
169
257
  error: { command: 'a command that was wrong (standard error)', schemaName: 'command-error', schema: CommandErrorSchema }
170
258
  };
@@ -44,6 +44,7 @@ import { createAuth } from './auth.js';
44
44
  import { EventHub } from './events.js';
45
45
  import { projectServer } from './projects.js';
46
46
  import { mountReviewCollections } from './review-collections.js';
47
+ import { mountRecordingVerification } from './recording-verification.js';
47
48
  import { readLimited, storeApi } from './store-api.js';
48
49
  const gzipAsync = promisify(gzip);
49
50
  const brotliAsync = promisify(brotliCompress);
@@ -260,7 +261,7 @@ function createProjectApplication(options) {
260
261
  name: 'atlas', version: options.version ?? null, role: identity?.role ?? 'read', open: auth.open,
261
262
  write: options.write, ready: index.ready, viewers: events.count, time: now(),
262
263
  backends: snapshot.backends.map(({ id, label, available }) => ({ id, label, available, write: id === options.write })),
263
- ...(({ backends: _unused, ...rest }) => rest)(index.stats()), capabilities: { events: true, ingest: true, store: true, checks: true, status: true, effective: true, live: true, tests: true, artifactTags: true, artifactSearch: true, reviewCollections: true }
264
+ ...(({ backends: _unused, ...rest }) => rest)(index.stats()), capabilities: { events: true, ingest: true, store: true, checks: true, status: true, effective: true, live: true, tests: true, artifactTags: true, artifactSearch: true, reviewCollections: true, recordingVerification: true }
264
265
  });
265
266
  });
266
267
  app.get('/api/live', async (c) => c.json({ runs: await live.list() }, 200, { 'cache-control': 'no-store' }));
@@ -565,6 +566,7 @@ function createProjectApplication(options) {
565
566
  });
566
567
  // Review collections: immutable, curated references to recorded results, with reviewers' notes kept apart.
567
568
  mountReviewCollections(app, { store: tagStore, stores, auth, index, now, maxBytes: limits.json, unauthorized });
569
+ mountRecordingVerification(app, { stores, auth, index, now, unauthorized });
568
570
  app.route('/api/reports', layers);
569
571
  // Upload: the store protocol, and a simple JSON way in for anything that can make a request.
570
572
  app.route('/api/store', storeApi({
@@ -1,5 +1,5 @@
1
1
  /** The version of the API described, `major.minor.patch`: the minor grows when an operation or field is added, the major never within `/api`. */
2
- export declare const API_VERSION = "1.6.0";
2
+ export declare const API_VERSION = "1.7.0";
3
3
  /** Who may call an operation: `none` (anyone), or the least role that may (`read` < `critic` < `publish` < `admin`). */
4
4
  export type Role = 'none' | 'read' | 'critic' | 'publish' | 'admin';
5
5
  type JsonSchema = Record<string, unknown>;
@@ -14,10 +14,10 @@
14
14
  */
15
15
  import { z } from 'zod/v4';
16
16
  import { ProjectsSchema } from '../../schema/src/project.js';
17
- import { EffectiveDeltaSchema, EffectiveEvidenceSchema, EvidencePageSchema, HealthSchema, LayerRowsSchema, CheckDetailSchema, OutlineSchema, ProblemSchema, ReadySchema, ServerInfoSchema, StoreListingSchema, StoreWriteResultSchema, ChecksResultSchema, HelloEventSchema, ReportEventSchema, PingEventSchema, TestCatalogSchema, CatalogHealthSchema, TestHistorySchema, TestReliabilitySchema, RunListSchema, RunDetailSchema, ArtifactListSchema, UiCatalogSchema, ArtifactTagEditSchema, ArtifactTagEntrySchema, ArtifactTagsSchema, ReviewCollectionRequestSchema, ReviewCollectionSchema, ReviewCollectionListSchema, ReviewCollectionDetailSchema, ReviewNoteInputSchema, ReviewNoteSchema, ReviewManifestSchema } from '../../schema/src/api.js';
17
+ import { EffectiveDeltaSchema, EffectiveEvidenceSchema, EvidencePageSchema, HealthSchema, LayerRowsSchema, CheckDetailSchema, OutlineSchema, ProblemSchema, ReadySchema, ServerInfoSchema, StoreListingSchema, StoreWriteResultSchema, ChecksResultSchema, HelloEventSchema, ReportEventSchema, PingEventSchema, TestCatalogSchema, CatalogHealthSchema, TestHistorySchema, TestReliabilitySchema, RunListSchema, RunDetailSchema, ArtifactListSchema, UiCatalogSchema, ArtifactTagEditSchema, ArtifactTagEntrySchema, ArtifactTagsSchema, ReviewCollectionRequestSchema, ReviewCollectionSchema, ReviewCollectionListSchema, ReviewCollectionDetailSchema, ReviewNoteInputSchema, ReviewNoteSchema, ReviewManifestSchema, RecordingVerificationSchema, RecordingVerifyRequestSchema } from '../../schema/src/api.js';
18
18
  import { LiveRunSchema, LiveRunStartSchema, LiveRunUpdateSchema, ServerEditableSettingsSchema, ServerSettingsSnapshotSchema, GcPolicySchema, GcSnapshotSchema, GcRunResultSchema, AtlasChangeSchema, PublishChecksSchema, ReportSourcesSchema, ReportStatusSchema, RunsIndexSchema, SpecIndexSchema } from '../../schema/src/index.js';
19
19
  /** The version of the API described, `major.minor.patch`: the minor grows when an operation or field is added, the major never within `/api`. */
20
- export const API_VERSION = '1.6.0';
20
+ export const API_VERSION = '1.7.0';
21
21
  /** The JSON Schema of a Zod schema, as OpenAPI 3.1 embeds it. `output` for what the server sends, `input` for what it accepts. */
22
22
  function schemaOf(schema, io = 'output') {
23
23
  const { $schema: _dialect, ...json } = z.toJSONSchema(schema, { target: 'draft-2020-12', unrepresentable: 'any', io });
@@ -38,6 +38,7 @@ const COMPONENTS = {
38
38
  Outline: [OutlineSchema, 'output'], EvidencePage: [EvidencePageSchema, 'output'], LayerRows: [LayerRowsSchema, 'output'], CheckDetail: [CheckDetailSchema, 'output'],
39
39
  ArtifactTagEdit: [ArtifactTagEditSchema, 'input'], ArtifactTagEntry: [ArtifactTagEntrySchema, 'output'], ArtifactTags: [ArtifactTagsSchema, 'output'],
40
40
  ReviewCollectionRequest: [ReviewCollectionRequestSchema, 'input'], ReviewCollection: [ReviewCollectionSchema, 'output'], ReviewCollectionList: [ReviewCollectionListSchema, 'output'], ReviewCollectionDetail: [ReviewCollectionDetailSchema, 'output'], ReviewNoteInput: [ReviewNoteInputSchema, 'input'], ReviewNote: [ReviewNoteSchema, 'output'], ReviewManifest: [ReviewManifestSchema, 'output'],
41
+ RecordingVerification: [RecordingVerificationSchema, 'output'], RecordingVerifyRequest: [RecordingVerifyRequestSchema, 'input'],
41
42
  TestCatalog: [TestCatalogSchema, 'output'], CatalogHealth: [CatalogHealthSchema, 'output'], TestHistory: [TestHistorySchema, 'output'], TestReliability: [TestReliabilitySchema, 'output'], RunList: [RunListSchema, 'output'], ArtifactList: [ArtifactListSchema, 'output'], UiCatalog: [UiCatalogSchema, 'output'], RunDetail: [RunDetailSchema, 'output'],
42
43
  SpecIndex: [SpecIndexSchema, 'output'], ReportSources: [ReportSourcesSchema, 'output'], RunsIndex: [RunsIndexSchema, 'output'],
43
44
  RunEvent: [LiveRunSchema, 'output'], HelloEvent: [HelloEventSchema, 'output'], ReportEvent: [ReportEventSchema, 'output'], AtlasChangeEvent: [AtlasChangeSchema, 'output'], PingEvent: [PingEventSchema, 'output']
@@ -367,6 +368,27 @@ const BASE_OPERATIONS = [
367
368
  body: { description: 'The attachment and the tags to add and remove. At most 20 of each, 64 KiB.', contentType: 'application/json', required: true, schema: ref('ArtifactTagEdit') },
368
369
  responses: { '201': json('The recorded entry.', 'ArtifactTagEntry'), '401': UNAUTHORIZED, '403': FORBIDDEN, '413': problem('Request too large.'), '422': problem('Not a tag edit: a bad target, an empty or over-long tag, or nothing to change.') }
369
370
  },
371
+ {
372
+ route: 'GET /api/reports/recordings/verification', path: '/api/reports/recordings/verification', operationId: 'getRecordingVerification', tag: 'Report', role: 'read',
373
+ summary: 'Which stored recordings the server last read back, and which were gone or damaged',
374
+ description: [
375
+ 'The newest read-back the server did of stored videos, per layer: how many it checked, how many passed and the `gaps` (a blob that is gone, bytes of another size or hash than were published, or a file that does not begin like its container), each naming the check that holds the video. `run` is the key of the run the layer belongs to (a `RunSummary.key`), so a viewer can mark that run\'s recordings as stored-then-unavailable beside its test results.',
376
+ '',
377
+ 'This route never reads a blob: it returns what a publish token\'s `POST` found earlier. A layer that was never read back is absent, which means "not verified", not "fine"; the result is kept in the server\'s memory only, so a restart empties it. Only layers a store still holds are listed (one removed by garbage collection drops out). No status, slot or winner depends on it. It checks stored bytes, not browser playback. Narrow with `?run=` (a run key) and `?layer=` (an id prefix of at least 7 hex digits, repeatable).'
378
+ ].join('\n'),
379
+ parameters: [
380
+ { name: 'run', in: 'query', description: 'Only layers of this run (a key from `GET /api/reports/runs`).', schema: str },
381
+ { name: 'layer', in: 'query', description: 'Only layers whose id starts with this prefix. Repeatable or comma-separated.', schema: str }
382
+ ],
383
+ responses: { '200': json('The held read-backs.', 'RecordingVerification'), '401': UNAUTHORIZED }
384
+ },
385
+ {
386
+ route: 'POST /api/reports/recordings/verification', path: '/api/reports/recordings/verification', operationId: 'verifyRecordings', tag: 'Report', role: 'publish',
387
+ summary: 'Read stored recordings back and check them (publish token)',
388
+ description: 'Reads the videos of the newest layers (`limit`, default 20, at most 200; or the layers `layers` names) back from storage and checks that each is still stored, still has the size and SHA-256 published, and still begins like its media type\'s container. This reads blobs, so it needs a publish (or admin) token in a Bearer header; a reader-only token, a browser sign-in box and an anonymous reader are refused. One read-back runs at a time (`409` otherwise). It writes nothing to storage and changes no status. The answer is the same document `GET` returns, with `verified`: how many layers this pass read back.',
389
+ body: { description: 'Optional `layers` (id prefixes) and `limit`. An empty body reads back the newest layers.', contentType: 'application/json', required: false, schema: ref('RecordingVerifyRequest') },
390
+ responses: { '200': json('The read-backs now held.', 'RecordingVerification'), '401': UNAUTHORIZED, '403': FORBIDDEN, '409': problem('A read-back is already running.'), '413': problem('Request too large.'), '422': problem('Not a verification request.'), '502': problem('A store could not be read.'), '503': NOT_READY }
391
+ },
370
392
  {
371
393
  route: 'GET /api/reports/review-collections', path: '/api/reports/review-collections', operationId: 'listReviewCollections', tag: 'Report', role: 'read',
372
394
  summary: 'Review collections: curated sets of recordings, newest first',
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @module Recording verification routes
3
+ *
4
+ * `/api/reports/recordings/verification`: whether the videos a run published can still be read back from storage as
5
+ * they were published, apart from whether the run's tests passed. A viewer reads the newest result with a read token
6
+ * (`GET`, which never touches a blob). A publish token starts a read-back (`POST`), which does read blobs and so is
7
+ * bounded: the newest 20 layers unless asked for more, at most 200, one pass at a time. The result is kept in this
8
+ * process only (it is derived, not state: a restart means "not verified yet", never "fine"), and only for layers the
9
+ * store still holds. It adds nothing to any layer and changes no status, slot or winner. This is the same check as
10
+ * `atlas report verify-recordings`, and as that command it checks stored bytes, not browser playback.
11
+ *
12
+ * @see docs/rich-evidence.md#recording-completeness
13
+ */
14
+ import type { Hono } from 'hono';
15
+ import type { ObjectStore } from '../../reports/src/object-store.js';
16
+ import type { ReportIndex } from '../../reports/src/report-index.js';
17
+ import type { Auth } from './auth.js';
18
+ /** What the verification routes need from the server around them. */
19
+ export type RecordingVerificationDeps = {
20
+ /** Every backend's store: a layer is read back from the store that holds it. */
21
+ readonly stores: readonly ObjectStore[];
22
+ readonly auth: Auth;
23
+ readonly index: ReportIndex;
24
+ readonly now: () => number;
25
+ readonly unauthorized: (verdict: Extract<ReturnType<Auth['check']>, {
26
+ ok: false;
27
+ }>) => Response;
28
+ };
29
+ /**
30
+ * Registers `GET` and `POST /api/reports/recordings/verification` on `app`. The result of the last read-back of each
31
+ * layer is held in memory, replaced by a newer one, and served only for layers a store still holds. A layer held by
32
+ * two backends keeps the reading with more gaps, so a copy that lost its blobs is never hidden by one that did not.
33
+ */
34
+ export declare function mountRecordingVerification(app: Hono, deps: RecordingVerificationDeps): void;