@crvouga/atlas 1.13.0 → 1.14.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 (43) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/dist/playwright/src/index.d.ts +4 -0
  3. package/dist/playwright/src/index.js +4 -0
  4. package/dist/playwright/src/ui-catalog.d.ts +51 -0
  5. package/dist/playwright/src/ui-catalog.js +44 -0
  6. package/dist/reports/src/evidence-types.d.ts +3 -0
  7. package/dist/reports/src/layer-merge.d.ts +5 -1
  8. package/dist/reports/src/layer-merge.js +9 -2
  9. package/dist/reports/src/report-index.js +9 -5
  10. package/dist/reports/src/ui-catalog-publish.d.ts +92 -0
  11. package/dist/reports/src/ui-catalog-publish.js +68 -0
  12. package/dist/reports/src/ui-catalog.d.ts +229 -0
  13. package/dist/reports/src/ui-catalog.js +273 -0
  14. package/dist/reports/src/ui-state.d.ts +70 -0
  15. package/dist/reports/src/ui-state.js +66 -0
  16. package/dist/schema/src/api.d.ts +350 -0
  17. package/dist/schema/src/api.js +71 -0
  18. package/dist/server/src/app.js +15 -0
  19. package/dist/server/src/openapi.js +22 -2
  20. package/docs/reference/http-api.md +46 -0
  21. package/docs/reference/playwright.md +63 -0
  22. package/docs/reference/reports.md +588 -1
  23. package/docs/reference/schema.md +33 -0
  24. package/docs/report-contract.md +2 -2
  25. package/docs/rich-evidence.md +5 -0
  26. package/docs/server.md +2 -0
  27. package/docs/ui-catalog.md +173 -0
  28. package/docs/ui.md +15 -0
  29. package/llms-full.txt +246 -2
  30. package/llms.txt +1 -0
  31. package/package.json +2 -1
  32. package/packages/schema/schemas/effective-delta.schema.json +73 -0
  33. package/packages/schema/schemas/effective-evidence.schema.json +73 -0
  34. package/packages/schema/schemas/layer-rows.schema.json +73 -0
  35. package/packages/server/openapi.json +1441 -79
  36. package/packages/visualizer/src/app/AppShell.tsx +36 -30
  37. package/packages/visualizer/src/app/router.tsx +10 -1
  38. package/packages/visualizer/src/catalog/UiCatalogFilters.tsx +65 -0
  39. package/packages/visualizer/src/catalog/UiCatalogPage.tsx +342 -0
  40. package/packages/visualizer/src/catalog/provider.tsx +25 -0
  41. package/packages/visualizer/src/catalog/search.ts +68 -0
  42. package/packages/visualizer/src/catalog/words.ts +26 -0
  43. package/packages/visualizer/src/components/Header.tsx +17 -4
@@ -325,6 +325,26 @@ export declare const LayerRowSchema: z.ZodObject<{
325
325
  }, z.core.$loose>>;
326
326
  }, z.core.$loose>>>;
327
327
  }, z.core.$loose>>;
328
+ state: z.ZodOptional<z.ZodObject<{
329
+ route: z.ZodOptional<z.ZodString>;
330
+ fixture: z.ZodOptional<z.ZodString>;
331
+ cohort: z.ZodOptional<z.ZodString>;
332
+ area: z.ZodOptional<z.ZodString>;
333
+ flags: z.ZodOptional<z.ZodArray<z.ZodString>>;
334
+ covers: z.ZodOptional<z.ZodArray<z.ZodString>>;
335
+ inventory: z.ZodOptional<z.ZodBoolean>;
336
+ screenshot: z.ZodOptional<z.ZodObject<{
337
+ name: z.ZodString;
338
+ bundleKey: z.ZodOptional<z.ZodString>;
339
+ uri: z.ZodOptional<z.ZodString>;
340
+ contentType: z.ZodOptional<z.ZodString>;
341
+ width: z.ZodOptional<z.ZodNumber>;
342
+ height: z.ZodOptional<z.ZodNumber>;
343
+ bytes: z.ZodOptional<z.ZodNumber>;
344
+ sha256: z.ZodOptional<z.ZodString>;
345
+ omitted: z.ZodOptional<z.ZodString>;
346
+ }, z.core.$loose>>;
347
+ }, z.core.$loose>>;
328
348
  recording: z.ZodOptional<z.ZodObject<{
329
349
  state: z.ZodString;
330
350
  reason: z.ZodOptional<z.ZodString>;
@@ -410,6 +430,26 @@ export declare const LayerRowsSchema: z.ZodObject<{
410
430
  }, z.core.$loose>>;
411
431
  }, z.core.$loose>>>;
412
432
  }, z.core.$loose>>;
433
+ state: z.ZodOptional<z.ZodObject<{
434
+ route: z.ZodOptional<z.ZodString>;
435
+ fixture: z.ZodOptional<z.ZodString>;
436
+ cohort: z.ZodOptional<z.ZodString>;
437
+ area: z.ZodOptional<z.ZodString>;
438
+ flags: z.ZodOptional<z.ZodArray<z.ZodString>>;
439
+ covers: z.ZodOptional<z.ZodArray<z.ZodString>>;
440
+ inventory: z.ZodOptional<z.ZodBoolean>;
441
+ screenshot: z.ZodOptional<z.ZodObject<{
442
+ name: z.ZodString;
443
+ bundleKey: z.ZodOptional<z.ZodString>;
444
+ uri: z.ZodOptional<z.ZodString>;
445
+ contentType: z.ZodOptional<z.ZodString>;
446
+ width: z.ZodOptional<z.ZodNumber>;
447
+ height: z.ZodOptional<z.ZodNumber>;
448
+ bytes: z.ZodOptional<z.ZodNumber>;
449
+ sha256: z.ZodOptional<z.ZodString>;
450
+ omitted: z.ZodOptional<z.ZodString>;
451
+ }, z.core.$loose>>;
452
+ }, z.core.$loose>>;
413
453
  recording: z.ZodOptional<z.ZodObject<{
414
454
  state: z.ZodString;
415
455
  reason: z.ZodOptional<z.ZodString>;
@@ -497,6 +537,26 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
497
537
  }, z.core.$loose>>;
498
538
  }, z.core.$loose>>>;
499
539
  }, z.core.$loose>>;
540
+ state: z.ZodOptional<z.ZodObject<{
541
+ route: z.ZodOptional<z.ZodString>;
542
+ fixture: z.ZodOptional<z.ZodString>;
543
+ cohort: z.ZodOptional<z.ZodString>;
544
+ area: z.ZodOptional<z.ZodString>;
545
+ flags: z.ZodOptional<z.ZodArray<z.ZodString>>;
546
+ covers: z.ZodOptional<z.ZodArray<z.ZodString>>;
547
+ inventory: z.ZodOptional<z.ZodBoolean>;
548
+ screenshot: z.ZodOptional<z.ZodObject<{
549
+ name: z.ZodString;
550
+ bundleKey: z.ZodOptional<z.ZodString>;
551
+ uri: z.ZodOptional<z.ZodString>;
552
+ contentType: z.ZodOptional<z.ZodString>;
553
+ width: z.ZodOptional<z.ZodNumber>;
554
+ height: z.ZodOptional<z.ZodNumber>;
555
+ bytes: z.ZodOptional<z.ZodNumber>;
556
+ sha256: z.ZodOptional<z.ZodString>;
557
+ omitted: z.ZodOptional<z.ZodString>;
558
+ }, z.core.$loose>>;
559
+ }, z.core.$loose>>;
500
560
  recording: z.ZodOptional<z.ZodObject<{
501
561
  state: z.ZodString;
502
562
  reason: z.ZodOptional<z.ZodString>;
@@ -674,6 +734,26 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
674
734
  }, z.core.$loose>>;
675
735
  }, z.core.$loose>>>;
676
736
  }, z.core.$loose>>;
737
+ state: z.ZodOptional<z.ZodObject<{
738
+ route: z.ZodOptional<z.ZodString>;
739
+ fixture: z.ZodOptional<z.ZodString>;
740
+ cohort: z.ZodOptional<z.ZodString>;
741
+ area: z.ZodOptional<z.ZodString>;
742
+ flags: z.ZodOptional<z.ZodArray<z.ZodString>>;
743
+ covers: z.ZodOptional<z.ZodArray<z.ZodString>>;
744
+ inventory: z.ZodOptional<z.ZodBoolean>;
745
+ screenshot: z.ZodOptional<z.ZodObject<{
746
+ name: z.ZodString;
747
+ bundleKey: z.ZodOptional<z.ZodString>;
748
+ uri: z.ZodOptional<z.ZodString>;
749
+ contentType: z.ZodOptional<z.ZodString>;
750
+ width: z.ZodOptional<z.ZodNumber>;
751
+ height: z.ZodOptional<z.ZodNumber>;
752
+ bytes: z.ZodOptional<z.ZodNumber>;
753
+ sha256: z.ZodOptional<z.ZodString>;
754
+ omitted: z.ZodOptional<z.ZodString>;
755
+ }, z.core.$loose>>;
756
+ }, z.core.$loose>>;
677
757
  recording: z.ZodOptional<z.ZodObject<{
678
758
  state: z.ZodString;
679
759
  reason: z.ZodOptional<z.ZodString>;
@@ -872,6 +952,26 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
872
952
  }, z.core.$loose>>;
873
953
  }, z.core.$loose>>>;
874
954
  }, z.core.$loose>>;
955
+ state: z.ZodOptional<z.ZodObject<{
956
+ route: z.ZodOptional<z.ZodString>;
957
+ fixture: z.ZodOptional<z.ZodString>;
958
+ cohort: z.ZodOptional<z.ZodString>;
959
+ area: z.ZodOptional<z.ZodString>;
960
+ flags: z.ZodOptional<z.ZodArray<z.ZodString>>;
961
+ covers: z.ZodOptional<z.ZodArray<z.ZodString>>;
962
+ inventory: z.ZodOptional<z.ZodBoolean>;
963
+ screenshot: z.ZodOptional<z.ZodObject<{
964
+ name: z.ZodString;
965
+ bundleKey: z.ZodOptional<z.ZodString>;
966
+ uri: z.ZodOptional<z.ZodString>;
967
+ contentType: z.ZodOptional<z.ZodString>;
968
+ width: z.ZodOptional<z.ZodNumber>;
969
+ height: z.ZodOptional<z.ZodNumber>;
970
+ bytes: z.ZodOptional<z.ZodNumber>;
971
+ sha256: z.ZodOptional<z.ZodString>;
972
+ omitted: z.ZodOptional<z.ZodString>;
973
+ }, z.core.$loose>>;
974
+ }, z.core.$loose>>;
875
975
  recording: z.ZodOptional<z.ZodObject<{
876
976
  state: z.ZodString;
877
977
  reason: z.ZodOptional<z.ZodString>;
@@ -3540,3 +3640,253 @@ export declare const RunDetailSchema: z.ZodObject<{
3540
3640
  }, z.core.$loose>>;
3541
3641
  }, z.core.$loose>>>;
3542
3642
  }, z.core.$loose>;
3643
+ /** A state's profile: the client and the choices it was captured under. */
3644
+ export declare const UiProfileSchema: z.ZodObject<{
3645
+ key: z.ZodString;
3646
+ environment: z.ZodNullable<z.ZodString>;
3647
+ parameters: z.ZodRecord<z.ZodString, z.ZodString>;
3648
+ label: z.ZodString;
3649
+ }, z.core.$loose>;
3650
+ /** The picture of a capture. */
3651
+ export declare const UiImageSchema: z.ZodObject<{
3652
+ name: z.ZodString;
3653
+ contentType: z.ZodOptional<z.ZodString>;
3654
+ width: z.ZodOptional<z.ZodNumber>;
3655
+ height: z.ZodOptional<z.ZodNumber>;
3656
+ bytes: z.ZodOptional<z.ZodNumber>;
3657
+ sha256: z.ZodOptional<z.ZodString>;
3658
+ bundleKey: z.ZodOptional<z.ZodString>;
3659
+ uri: z.ZodOptional<z.ZodString>;
3660
+ path: z.ZodOptional<z.ZodString>;
3661
+ omitted: z.ZodOptional<z.ZodString>;
3662
+ }, z.core.$loose>;
3663
+ /** One state in one profile. */
3664
+ export declare const UiCellSchema: z.ZodObject<{
3665
+ profile: z.ZodObject<{
3666
+ key: z.ZodString;
3667
+ environment: z.ZodNullable<z.ZodString>;
3668
+ parameters: z.ZodRecord<z.ZodString, z.ZodString>;
3669
+ label: z.ZodString;
3670
+ }, z.core.$loose>;
3671
+ coverage: z.ZodEnum<{
3672
+ failed: "failed";
3673
+ skipped: "skipped";
3674
+ missing: "missing";
3675
+ captured: "captured";
3676
+ capturing: "capturing";
3677
+ }>;
3678
+ expected: z.ZodEnum<{
3679
+ unknown: "unknown";
3680
+ declared: "declared";
3681
+ undeclared: "undeclared";
3682
+ }>;
3683
+ capture: z.ZodNullable<z.ZodObject<{
3684
+ status: z.ZodEnum<{
3685
+ passed: "passed";
3686
+ failed: "failed";
3687
+ running: "running";
3688
+ "not-run": "not-run";
3689
+ }>;
3690
+ detail: z.ZodString;
3691
+ durationMs: z.ZodNullable<z.ZodNumber>;
3692
+ image: z.ZodNullable<z.ZodObject<{
3693
+ name: z.ZodString;
3694
+ contentType: z.ZodOptional<z.ZodString>;
3695
+ width: z.ZodOptional<z.ZodNumber>;
3696
+ height: z.ZodOptional<z.ZodNumber>;
3697
+ bytes: z.ZodOptional<z.ZodNumber>;
3698
+ sha256: z.ZodOptional<z.ZodString>;
3699
+ bundleKey: z.ZodOptional<z.ZodString>;
3700
+ uri: z.ZodOptional<z.ZodString>;
3701
+ path: z.ZodOptional<z.ZodString>;
3702
+ omitted: z.ZodOptional<z.ZodString>;
3703
+ }, z.core.$loose>>;
3704
+ labels: z.ZodRecord<z.ZodString, z.ZodString>;
3705
+ origin: z.ZodObject<{
3706
+ layer: z.ZodString;
3707
+ backend: z.ZodString;
3708
+ producer: z.ZodString;
3709
+ recordedAtMs: z.ZodNumber;
3710
+ sha: z.ZodNullable<z.ZodString>;
3711
+ branch: z.ZodNullable<z.ZodString>;
3712
+ runUrl: z.ZodNullable<z.ZodString>;
3713
+ freshness: z.ZodOptional<z.ZodEnum<{
3714
+ unknown: "unknown";
3715
+ modified: "modified";
3716
+ }>>;
3717
+ }, z.core.$loose>;
3718
+ alternatives: z.ZodNumber;
3719
+ }, z.core.$loose>>;
3720
+ planned: z.ZodNullable<z.ZodObject<{
3721
+ layer: z.ZodString;
3722
+ backend: z.ZodString;
3723
+ }, z.core.$loose>>;
3724
+ }, z.core.$loose>;
3725
+ /**
3726
+ * `GET /api/reports/ui-catalog`: the UI catalog, computed from the winning results of the `ui-state` and `ui-plan` parts
3727
+ * of every layer. States are in id order and cells in profile order, so the same layers always give the same document.
3728
+ */
3729
+ export declare const UiCatalogSchema: z.ZodObject<{
3730
+ schemaVersion: z.ZodLiteral<1>;
3731
+ version: z.ZodOptional<z.ZodString>;
3732
+ summary: z.ZodObject<{
3733
+ states: z.ZodNumber;
3734
+ profiles: z.ZodNumber;
3735
+ cells: z.ZodNumber;
3736
+ coverage: z.ZodObject<{
3737
+ captured: z.ZodNumber;
3738
+ failed: z.ZodNumber;
3739
+ capturing: z.ZodNumber;
3740
+ skipped: z.ZodNumber;
3741
+ missing: z.ZodNumber;
3742
+ }, z.core.$loose>;
3743
+ undeclared: z.ZodNumber;
3744
+ planned: z.ZodBoolean;
3745
+ surfaces: z.ZodNumber;
3746
+ uncoveredSurfaces: z.ZodNumber;
3747
+ }, z.core.$loose>;
3748
+ profiles: z.ZodArray<z.ZodObject<{
3749
+ key: z.ZodString;
3750
+ environment: z.ZodNullable<z.ZodString>;
3751
+ parameters: z.ZodRecord<z.ZodString, z.ZodString>;
3752
+ label: z.ZodString;
3753
+ }, z.core.$loose>>;
3754
+ states: z.ZodArray<z.ZodObject<{
3755
+ id: z.ZodString;
3756
+ title: z.ZodString;
3757
+ route: z.ZodNullable<z.ZodString>;
3758
+ fixture: z.ZodNullable<z.ZodString>;
3759
+ cohort: z.ZodNullable<z.ZodString>;
3760
+ area: z.ZodNullable<z.ZodString>;
3761
+ flags: z.ZodArray<z.ZodString>;
3762
+ covers: z.ZodArray<z.ZodString>;
3763
+ producers: z.ZodArray<z.ZodString>;
3764
+ cells: z.ZodArray<z.ZodObject<{
3765
+ profile: z.ZodObject<{
3766
+ key: z.ZodString;
3767
+ environment: z.ZodNullable<z.ZodString>;
3768
+ parameters: z.ZodRecord<z.ZodString, z.ZodString>;
3769
+ label: z.ZodString;
3770
+ }, z.core.$loose>;
3771
+ coverage: z.ZodEnum<{
3772
+ failed: "failed";
3773
+ skipped: "skipped";
3774
+ missing: "missing";
3775
+ captured: "captured";
3776
+ capturing: "capturing";
3777
+ }>;
3778
+ expected: z.ZodEnum<{
3779
+ unknown: "unknown";
3780
+ declared: "declared";
3781
+ undeclared: "undeclared";
3782
+ }>;
3783
+ capture: z.ZodNullable<z.ZodObject<{
3784
+ status: z.ZodEnum<{
3785
+ passed: "passed";
3786
+ failed: "failed";
3787
+ running: "running";
3788
+ "not-run": "not-run";
3789
+ }>;
3790
+ detail: z.ZodString;
3791
+ durationMs: z.ZodNullable<z.ZodNumber>;
3792
+ image: z.ZodNullable<z.ZodObject<{
3793
+ name: z.ZodString;
3794
+ contentType: z.ZodOptional<z.ZodString>;
3795
+ width: z.ZodOptional<z.ZodNumber>;
3796
+ height: z.ZodOptional<z.ZodNumber>;
3797
+ bytes: z.ZodOptional<z.ZodNumber>;
3798
+ sha256: z.ZodOptional<z.ZodString>;
3799
+ bundleKey: z.ZodOptional<z.ZodString>;
3800
+ uri: z.ZodOptional<z.ZodString>;
3801
+ path: z.ZodOptional<z.ZodString>;
3802
+ omitted: z.ZodOptional<z.ZodString>;
3803
+ }, z.core.$loose>>;
3804
+ labels: z.ZodRecord<z.ZodString, z.ZodString>;
3805
+ origin: z.ZodObject<{
3806
+ layer: z.ZodString;
3807
+ backend: z.ZodString;
3808
+ producer: z.ZodString;
3809
+ recordedAtMs: z.ZodNumber;
3810
+ sha: z.ZodNullable<z.ZodString>;
3811
+ branch: z.ZodNullable<z.ZodString>;
3812
+ runUrl: z.ZodNullable<z.ZodString>;
3813
+ freshness: z.ZodOptional<z.ZodEnum<{
3814
+ unknown: "unknown";
3815
+ modified: "modified";
3816
+ }>>;
3817
+ }, z.core.$loose>;
3818
+ alternatives: z.ZodNumber;
3819
+ }, z.core.$loose>>;
3820
+ planned: z.ZodNullable<z.ZodObject<{
3821
+ layer: z.ZodString;
3822
+ backend: z.ZodString;
3823
+ }, z.core.$loose>>;
3824
+ }, z.core.$loose>>;
3825
+ }, z.core.$loose>>;
3826
+ surfaces: z.ZodArray<z.ZodObject<{
3827
+ id: z.ZodString;
3828
+ inventoried: z.ZodBoolean;
3829
+ states: z.ZodNumber;
3830
+ captured: z.ZodNumber;
3831
+ status: z.ZodEnum<{
3832
+ covered: "covered";
3833
+ uncaptured: "uncaptured";
3834
+ uncovered: "uncovered";
3835
+ }>;
3836
+ }, z.core.$loose>>;
3837
+ facets: z.ZodObject<{
3838
+ profiles: z.ZodArray<z.ZodObject<{
3839
+ value: z.ZodString;
3840
+ count: z.ZodNumber;
3841
+ label: z.ZodOptional<z.ZodString>;
3842
+ }, z.core.$loose>>;
3843
+ coverage: z.ZodArray<z.ZodObject<{
3844
+ value: z.ZodString;
3845
+ count: z.ZodNumber;
3846
+ label: z.ZodOptional<z.ZodString>;
3847
+ }, z.core.$loose>>;
3848
+ areas: z.ZodArray<z.ZodObject<{
3849
+ value: z.ZodString;
3850
+ count: z.ZodNumber;
3851
+ label: z.ZodOptional<z.ZodString>;
3852
+ }, z.core.$loose>>;
3853
+ routes: z.ZodArray<z.ZodObject<{
3854
+ value: z.ZodString;
3855
+ count: z.ZodNumber;
3856
+ label: z.ZodOptional<z.ZodString>;
3857
+ }, z.core.$loose>>;
3858
+ cohorts: z.ZodArray<z.ZodObject<{
3859
+ value: z.ZodString;
3860
+ count: z.ZodNumber;
3861
+ label: z.ZodOptional<z.ZodString>;
3862
+ }, z.core.$loose>>;
3863
+ flags: z.ZodArray<z.ZodObject<{
3864
+ value: z.ZodString;
3865
+ count: z.ZodNumber;
3866
+ label: z.ZodOptional<z.ZodString>;
3867
+ }, z.core.$loose>>;
3868
+ surfaces: z.ZodArray<z.ZodObject<{
3869
+ value: z.ZodString;
3870
+ count: z.ZodNumber;
3871
+ label: z.ZodOptional<z.ZodString>;
3872
+ }, z.core.$loose>>;
3873
+ producers: z.ZodArray<z.ZodObject<{
3874
+ value: z.ZodString;
3875
+ count: z.ZodNumber;
3876
+ label: z.ZodOptional<z.ZodString>;
3877
+ }, z.core.$loose>>;
3878
+ }, z.core.$loose>;
3879
+ sources: z.ZodObject<{
3880
+ layers: z.ZodNumber;
3881
+ backends: z.ZodArray<z.ZodObject<{
3882
+ id: z.ZodString;
3883
+ label: z.ZodString;
3884
+ live: z.ZodBoolean;
3885
+ available: z.ZodBoolean;
3886
+ }, z.core.$loose>>;
3887
+ notices: z.ZodArray<z.ZodString>;
3888
+ }, z.core.$loose>;
3889
+ total: z.ZodOptional<z.ZodNumber>;
3890
+ offset: z.ZodOptional<z.ZodNumber>;
3891
+ limit: z.ZodOptional<z.ZodNumber>;
3892
+ }, z.core.$loose>;
@@ -130,6 +130,16 @@ export const LayerRowSchema = z.looseObject({
130
130
  variant: z.record(z.string(), z.string()),
131
131
  durationMs: z.number().nullable(),
132
132
  evidence: EvidenceSummarySchema.optional(),
133
+ state: z.looseObject({
134
+ route: z.string().optional(), fixture: z.string().optional(), cohort: z.string().optional(), area: z.string().optional(),
135
+ flags: z.array(z.string()).optional(), covers: z.array(z.string()).optional(),
136
+ inventory: z.boolean().optional().describe('A `ui-plan` entry that names a surface of the product\'s inventory, not a state'),
137
+ screenshot: z.looseObject({
138
+ name: z.string(), bundleKey: z.string().optional(), uri: z.string().optional(), contentType: z.string().optional(),
139
+ width: z.number().optional(), height: z.number().optional(), bytes: z.number().optional(), sha256: z.string().optional(),
140
+ omitted: z.string().optional().describe('Why the producer left the bytes out')
141
+ }).optional().describe('The check\'s picture: its `screenshot` attachment, else its first image')
142
+ }).optional().describe('Only for a check of a `ui-state` or `ui-plan` part: its picture and the facts its producer declared under the `atlas.ui-state` extension. See the UI catalog guide. Never decides a result'),
133
143
  recording: z.looseObject({ state: z.string(), reason: z.string().optional() }).optional().describe('What became of the check\'s expected recording: uploaded, referenced, skipped, unresolved or not-applicable. Independent of `status`')
134
144
  });
135
145
  /** A layer with its checks: the rows of one layer, read apart from its header. */
@@ -671,3 +681,64 @@ export const RunDetailSchema = z.looseObject({
671
681
  videos: VideoCountsSchema.optional()
672
682
  })).optional().describe('Every layer of the run, oldest first. Absent from an older server')
673
683
  });
684
+ /** A state's profile: the client and the choices it was captured under. */
685
+ export const UiProfileSchema = z.looseObject({
686
+ key: z.string().describe('Identifies the profile across layers: the environment and the sorted parameters'),
687
+ environment: z.string().nullable(), parameters: z.record(z.string(), z.string()),
688
+ label: z.string().describe('For a person: the client and each parameter\'s value')
689
+ });
690
+ /** The picture of a capture. */
691
+ export const UiImageSchema = z.looseObject({
692
+ name: z.string(), contentType: z.string().optional(), width: z.number().optional(), height: z.number().optional(), bytes: z.number().optional(), sha256: z.string().optional(),
693
+ bundleKey: z.string().optional(), uri: z.string().optional().describe('An http(s) address, when the picture lives elsewhere'),
694
+ path: z.string().optional().describe('Where the stored bytes are, relative to the reports API root (`/api/reports` on a server): `/layers/<layer>/blobs/attachments/<sha256>`'),
695
+ omitted: z.string().optional().describe('Why the producer left the bytes out. A picture that is left out is not a picture that was never taken')
696
+ });
697
+ /** One state in one profile. */
698
+ export const UiCellSchema = z.looseObject({
699
+ profile: UiProfileSchema,
700
+ coverage: z.enum(['captured', 'failed', 'capturing', 'skipped', 'missing']).describe('`captured` only when the winning capture passed. Every other value means there is no picture to trust'),
701
+ expected: z.enum(['declared', 'undeclared', 'unknown']).describe('`declared`: a plan expects it. `undeclared`: plans exist and none names it. `unknown`: no plan was published, so nothing is known to be unexpected'),
702
+ capture: z.looseObject({
703
+ status: EvidenceStatusSchema, detail: z.string(), durationMs: z.number().nullable(), image: UiImageSchema.nullable(),
704
+ labels: z.record(z.string(), z.string()).describe('What describes the capture without splitting it: a `shard`, a `browser`'),
705
+ origin: z.looseObject({
706
+ layer: z.string(), backend: z.string(), producer: z.string(), recordedAtMs: z.number(), sha: z.string().nullable(), branch: z.string().nullable(), runUrl: z.string().nullable(),
707
+ freshness: z.enum(['unknown', 'modified']).optional()
708
+ }),
709
+ alternatives: z.number().int().nonnegative().describe('Other captures of this cell from other producers; the winner is the one the report contract picks')
710
+ }).nullable(),
711
+ planned: z.looseObject({ layer: z.string(), backend: z.string() }).nullable().describe('The plan layer that expects this cell')
712
+ });
713
+ const UiFacetSchema = z.array(z.looseObject({ value: z.string(), count: z.number().int().nonnegative(), label: z.string().optional() }));
714
+ /**
715
+ * `GET /api/reports/ui-catalog`: the UI catalog, computed from the winning results of the `ui-state` and `ui-plan` parts
716
+ * of every layer. States are in id order and cells in profile order, so the same layers always give the same document.
717
+ */
718
+ export const UiCatalogSchema = z.looseObject({
719
+ schemaVersion: z.literal(1),
720
+ version: z.string().optional().describe('The report version this was computed from; equal exactly when the report is'),
721
+ summary: z.looseObject({
722
+ states: z.number().int().nonnegative(), profiles: z.number().int().nonnegative(), cells: z.number().int().nonnegative(),
723
+ coverage: z.looseObject({ captured: z.number(), failed: z.number(), capturing: z.number(), skipped: z.number(), missing: z.number() }),
724
+ undeclared: z.number().int().nonnegative().describe('Cells captured or failed that no plan expects. Zero when no plan was published'),
725
+ planned: z.boolean().describe('Whether any plan was published. Without one `missing` is always zero: what should exist is unknown'),
726
+ surfaces: z.number().int().nonnegative(), uncoveredSurfaces: z.number().int().nonnegative()
727
+ }),
728
+ profiles: z.array(UiProfileSchema),
729
+ states: z.array(z.looseObject({
730
+ id: z.string(), title: z.string(),
731
+ route: z.string().nullable(), fixture: z.string().nullable(), cohort: z.string().nullable(), area: z.string().nullable(),
732
+ flags: z.array(z.string()), covers: z.array(z.string()).describe('The surfaces it covers'), producers: z.array(z.string()),
733
+ cells: z.array(UiCellSchema)
734
+ })),
735
+ surfaces: z.array(z.looseObject({
736
+ id: z.string(), inventoried: z.boolean().describe('A `ui-plan` inventory entry names it: the product is known to have it'),
737
+ states: z.number().int().nonnegative(), captured: z.number().int().nonnegative(),
738
+ status: z.enum(['covered', 'uncaptured', 'uncovered'])
739
+ })),
740
+ facets: z.looseObject({ profiles: UiFacetSchema, coverage: UiFacetSchema, areas: UiFacetSchema, routes: UiFacetSchema, cohorts: UiFacetSchema, flags: UiFacetSchema, surfaces: UiFacetSchema, producers: UiFacetSchema }),
741
+ sources: z.looseObject({ layers: z.number().int().nonnegative(), backends: z.array(BackendInfoSchema), notices: z.array(z.string()) }).describe('What was read. A backend that is unavailable, or a layer that could not be read, is named here; what depended on it is not shown as captured'),
742
+ total: z.number().int().nonnegative().optional().describe('States that matched, before `offset` and `limit`'),
743
+ offset: z.number().int().nonnegative().optional(), limit: z.number().int().nonnegative().optional()
744
+ });
@@ -28,6 +28,7 @@ import { listRunSummaries, readRunFile } from '../../reports/src/run-bundle.js';
28
28
  import { latestSpecSet, readSpecBody } from '../../reports/src/spec-set.js';
29
29
  import { catalogHealth, healthMarkdown, parseRootMappings } from '../../reports/src/catalog-health.js';
30
30
  import { parseArtifactFilter } from '../../reports/src/artifact-query.js';
31
+ import { filterUiCatalog, pageUiCatalog, parseUiCatalogQuery, uiCatalog, uiCatalogMarkdown } from '../../reports/src/ui-catalog.js';
31
32
  import { parseReliabilityScope, reliability, reliabilityMarkdown } from '../../reports/src/reliability.js';
32
33
  import { artifactList, artifactsMarkdown, catalogMarkdown, catalogModel, historyMarkdown, parseTestQuery, runDetail, runList, runMarkdown, runsMarkdown, testCatalog, testHistory } from '../../reports/src/test-catalog.js';
33
34
  import { llmsTxt } from './llms.js';
@@ -418,6 +419,7 @@ function createProjectApplication(options) {
418
419
  }
419
420
  return model.value;
420
421
  };
422
+ let uiModel;
421
423
  const wantsMarkdown = (c) => {
422
424
  const format = c.req.query('format');
423
425
  return format === 'markdown' || format === 'md' || (format === undefined && /\btext\/markdown\b/.test(c.req.header('accept') ?? ''));
@@ -502,6 +504,19 @@ function createProjectApplication(options) {
502
504
  const document = () => artifactList(index.history, found.version, parseArtifactFilter(search), { limit: number('limit', 100), offset: number('offset', 0), domainOf: path => domains.get(path) });
503
505
  return answer(c, found.version, document, () => artifactsMarkdown(document()), `artifacts|${search.toString()}|${wantsMarkdown(c)}`);
504
506
  });
507
+ app.get('/api/reports/ui-catalog', async (c) => {
508
+ if (!index.ready)
509
+ return notReady(c);
510
+ const snapshot = index.snapshot();
511
+ const search = new URL(c.req.url).searchParams;
512
+ const number = (name, fallback) => (/^\d+$/.test(search.get(name) ?? '') ? Number(search.get(name)) : fallback);
513
+ // The catalog is a function of the report, so it is built once per version and narrowed per question.
514
+ if (uiModel?.version !== snapshot.version)
515
+ uiModel = { version: snapshot.version, value: uiCatalog(snapshot.layers, { version: snapshot.version, backends: snapshot.backends, notices: snapshot.notices }) };
516
+ const whole = uiModel.value;
517
+ const document = () => pageUiCatalog(filterUiCatalog(whole, parseUiCatalogQuery(search)), { limit: number('limit', 200), offset: number('offset', 0) });
518
+ return answer(c, snapshot.version, document, () => uiCatalogMarkdown(document()), `ui-catalog|${search.toString()}|${wantsMarkdown(c)}`);
519
+ });
505
520
  app.get('/api/reports/runs/detail', async (c) => {
506
521
  if (!index.ready)
507
522
  return notReady(c);
@@ -14,7 +14,7 @@
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, 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 } 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
20
  export const API_VERSION = '1.6.0';
@@ -38,7 +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
- TestCatalog: [TestCatalogSchema, 'output'], CatalogHealth: [CatalogHealthSchema, 'output'], TestHistory: [TestHistorySchema, 'output'], TestReliability: [TestReliabilitySchema, 'output'], RunList: [RunListSchema, 'output'], ArtifactList: [ArtifactListSchema, 'output'], RunDetail: [RunDetailSchema, 'output'],
41
+ 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
42
  SpecIndex: [SpecIndexSchema, 'output'], ReportSources: [ReportSourcesSchema, 'output'], RunsIndex: [RunsIndexSchema, 'output'],
43
43
  RunEvent: [LiveRunSchema, 'output'], HelloEvent: [HelloEventSchema, 'output'], ReportEvent: [ReportEventSchema, 'output'], AtlasChangeEvent: [AtlasChangeSchema, 'output'], PingEvent: [PingEventSchema, 'output']
44
44
  };
@@ -460,6 +460,26 @@ const BASE_OPERATIONS = [
460
460
  ],
461
461
  responses: { '200': json('The matching results and the facets.', 'ArtifactList'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '401': UNAUTHORIZED, '503': NOT_READY }
462
462
  },
463
+ {
464
+ route: 'GET /api/reports/ui-catalog', path: '/api/reports/ui-catalog', operationId: 'getUiCatalog', tag: 'Report', role: 'read',
465
+ summary: 'The UI catalog: captured UI states per profile, and what is still expected',
466
+ description: 'A browsable inventory of the product\'s user interface, computed from the winning results of the `ui-state` and `ui-plan` parts of every layer (see the UI catalog guide). Each state is a check id; each cell is one profile of it (the layer\'s environment and parameters) with its coverage: `captured` only when the winning capture passed, `failed`, `capturing`, `skipped`, or `missing` when a plan expects it and nothing has been captured. A picture is read from the cell\'s `capture.image.path` under the reports API root, or `uri`. What is expected is itself a layer, so a shard that never published leaves its states `missing`, never captured. The document equals what the viewer computes from the same report. Narrow it by `q` (words), `coverage`, `profile` (a profile `key`), `area`, `route`, `cohort`, `flag`, `surface` and `producer`: AND across parameters, OR within one; a state keeps only the cells that match `profile` and `coverage`. Page the states with `limit` and `offset`. Viewers need not poll: they hold the report over `/api/events` and read changes with `/api/reports/evidence/effective?since=`.',
467
+ parameters: [
468
+ { name: 'limit', in: 'query', description: 'At most this many states (default 200).', schema: { type: 'integer', minimum: 0 } },
469
+ { name: 'offset', in: 'query', description: 'Skip this many states (default 0).', schema: { type: 'integer', minimum: 0 } },
470
+ { name: 'q', in: 'query', description: 'Words that must all appear in a state\'s id, title, route, fixture, cohort, area, flags, surfaces or producers.', schema: str },
471
+ { name: 'coverage', in: 'query', description: 'Coverage values: `captured`, `failed`, `capturing`, `skipped`, `missing`. Comma-separated.', schema: str },
472
+ { name: 'profile', in: 'query', description: 'Profile keys, as listed in `profiles[].key`.', schema: str },
473
+ { name: 'area', in: 'query', description: 'Areas.', schema: str },
474
+ { name: 'route', in: 'query', description: 'Routes.', schema: str },
475
+ { name: 'cohort', in: 'query', description: 'Cohorts.', schema: str },
476
+ { name: 'flag', in: 'query', description: 'Feature flags a state was captured with.', schema: str },
477
+ { name: 'surface', in: 'query', description: 'Surfaces a state covers.', schema: str },
478
+ { name: 'producer', in: 'query', description: 'Producers that captured or planned a state.', schema: str },
479
+ { name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
480
+ ],
481
+ responses: { '200': json('The catalog.', 'UiCatalog'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '401': UNAUTHORIZED, '503': NOT_READY }
482
+ },
463
483
  {
464
484
  route: 'GET /api/reports/runs/detail', path: '/api/reports/runs/detail', operationId: 'getReportRun', tag: 'Report', role: 'read',
465
485
  summary: 'Every test one run recorded',
@@ -926,6 +926,52 @@ Scoped to one project. The same project may be selected on the unprefixed API us
926
926
 
927
927
  Responses: `200` The matching results and the facets.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
928
928
 
929
+ ### The UI catalog: captured UI states per profile, and what is still expected
930
+
931
+ `GET /api/reports/ui-catalog` · role: read
932
+
933
+ A browsable inventory of the product's user interface, computed from the winning results of the `ui-state` and `ui-plan` parts of every layer (see the UI catalog guide). Each state is a check id; each cell is one profile of it (the layer's environment and parameters) with its coverage: `captured` only when the winning capture passed, `failed`, `capturing`, `skipped`, or `missing` when a plan expects it and nothing has been captured. A picture is read from the cell's `capture.image.path` under the reports API root, or `uri`. What is expected is itself a layer, so a shard that never published leaves its states `missing`, never captured. The document equals what the viewer computes from the same report. Narrow it by `q` (words), `coverage`, `profile` (a profile `key`), `area`, `route`, `cohort`, `flag`, `surface` and `producer`: AND across parameters, OR within one; a state keeps only the cells that match `profile` and `coverage`. Page the states with `limit` and `offset`. Viewers need not poll: they hold the report over `/api/events` and read changes with `/api/reports/evidence/effective?since=`.
934
+
935
+ - `limit` (query): At most this many states (default 200).
936
+ - `offset` (query): Skip this many states (default 0).
937
+ - `q` (query): Words that must all appear in a state's id, title, route, fixture, cohort, area, flags, surfaces or producers.
938
+ - `coverage` (query): Coverage values: `captured`, `failed`, `capturing`, `skipped`, `missing`. Comma-separated.
939
+ - `profile` (query): Profile keys, as listed in `profiles[].key`.
940
+ - `area` (query): Areas.
941
+ - `route` (query): Routes.
942
+ - `cohort` (query): Cohorts.
943
+ - `flag` (query): Feature flags a state was captured with.
944
+ - `surface` (query): Surfaces a state covers.
945
+ - `producer` (query): Producers that captured or planned a state.
946
+ - `format` (query): `markdown` for Markdown instead of JSON.
947
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
948
+
949
+ Responses: `200` The catalog.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
950
+
951
+ ### The UI catalog: captured UI states per profile, and what is still expected
952
+
953
+ `GET /api/projects/{project}/reports/ui-catalog` · role: read
954
+
955
+ A browsable inventory of the product's user interface, computed from the winning results of the `ui-state` and `ui-plan` parts of every layer (see the UI catalog guide). Each state is a check id; each cell is one profile of it (the layer's environment and parameters) with its coverage: `captured` only when the winning capture passed, `failed`, `capturing`, `skipped`, or `missing` when a plan expects it and nothing has been captured. A picture is read from the cell's `capture.image.path` under the reports API root, or `uri`. What is expected is itself a layer, so a shard that never published leaves its states `missing`, never captured. The document equals what the viewer computes from the same report. Narrow it by `q` (words), `coverage`, `profile` (a profile `key`), `area`, `route`, `cohort`, `flag`, `surface` and `producer`: AND across parameters, OR within one; a state keeps only the cells that match `profile` and `coverage`. Page the states with `limit` and `offset`. Viewers need not poll: they hold the report over `/api/events` and read changes with `/api/reports/evidence/effective?since=`.
956
+
957
+ 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.
958
+
959
+ - `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.
960
+ - `limit` (query): At most this many states (default 200).
961
+ - `offset` (query): Skip this many states (default 0).
962
+ - `q` (query): Words that must all appear in a state's id, title, route, fixture, cohort, area, flags, surfaces or producers.
963
+ - `coverage` (query): Coverage values: `captured`, `failed`, `capturing`, `skipped`, `missing`. Comma-separated.
964
+ - `profile` (query): Profile keys, as listed in `profiles[].key`.
965
+ - `area` (query): Areas.
966
+ - `route` (query): Routes.
967
+ - `cohort` (query): Cohorts.
968
+ - `flag` (query): Feature flags a state was captured with.
969
+ - `surface` (query): Surfaces a state covers.
970
+ - `producer` (query): Producers that captured or planned a state.
971
+ - `format` (query): `markdown` for Markdown instead of JSON.
972
+
973
+ Responses: `200` The catalog.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
974
+
929
975
  ### Every test one run recorded
930
976
 
931
977
  `GET /api/reports/runs/detail` · role: read