@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
@@ -104,7 +104,10 @@ recognise screens and tap or type (`screen`, `recognize`, `locate`, the signal c
104
104
  the media tools (`captureScreen`, `ScreencastRecorder`, `ffmpeg`, `PHONE` and the pacing presets). Needs the
105
105
  `playwright` package; clips and thumbnails need FFmpeg on the PATH.
106
106
 
107
+ Also `captureUiState`, the optional Playwright adapter of the UI catalog (it needs no Playwright import itself).
108
+
107
109
  See docs/statecharts.md
110
+ See docs/ui-catalog.md
108
111
 
109
112
  ### Playwright screenshots, clips and paced actions
110
113
 
@@ -301,3 +304,63 @@ const states = {
301
304
  'No todos': screen({ all: [text('Nothing to do yet')], none: [role('listitem')] })
302
305
  };
303
306
  ```
307
+
308
+ ### Playwright adapter for the UI catalog
309
+
310
+ Source: `packages/playwright/src/ui-catalog.ts`
311
+
312
+ The optional Playwright side of the UI catalog: take a page to a state, wait until it is ready, photograph it, and hand the
313
+ result to `uiStateLayer` (`@crvouga/atlas/reports/ui-catalog-publish`) as a capture. This is the only Playwright-specific code
314
+ of the feature; reading, composing and exploring the catalog know nothing about it, and any other program that writes the
315
+ same layer produces the same catalog. The page is typed by the three methods used, so no `playwright` import is needed here
316
+ and a Playwright Test `page`, a `Page` of the driver, or a stub all fit.
317
+
318
+ See docs/ui-catalog.md
319
+
320
+ #### `CapturePage`
321
+
322
+ ```ts
323
+ export type CapturePage = {
324
+ screenshot(options?: { readonly fullPage?: boolean; readonly animations?: 'disabled' | 'allow'; readonly type?: 'png' }): Promise<Uint8Array>;
325
+ viewportSize(): { readonly width: number; readonly height: number } | null;
326
+ };
327
+ ```
328
+
329
+ The part of a Playwright `Page` that capturing uses.
330
+
331
+ #### `CaptureUiStateOptions`
332
+
333
+ ```ts
334
+ export type CaptureUiStateOptions = UiStateFacts & {
335
+ /** The state's stable id: how a later capture replaces this one. */
336
+ readonly id: string;
337
+ readonly title?: string;
338
+ /** Resolves when the page shows the state (a locator is visible, the network is idle). A rejection or a timeout makes the capture `failed`, with the reason as its detail. */
339
+ readonly ready?: (page: CapturePage) => Promise<void>;
340
+ /** How long to wait for `ready`; default 15 seconds. */
341
+ readonly readyTimeoutMs?: number;
342
+ /** Photograph the whole scrollable page rather than the viewport. */
343
+ readonly fullPage?: boolean;
344
+ /** Extra labels for the capture (`browser`, `os`), which never split it. */
345
+ readonly labels?: Readonly<Record<string, string>>;
346
+ };
347
+ ```
348
+
349
+ What to capture and how to know the page is ready.
350
+
351
+ #### `captureUiState`
352
+
353
+ ```ts
354
+ export async function captureUiState(page: CapturePage, options: CaptureUiStateOptions): Promise<UiStateCapture>
355
+ ```
356
+
357
+ Wait for the page to be ready, photograph it, and return the capture to publish. Never throws for the page's sake: a page
358
+ that never became ready, or could not be photographed, is a `failed` capture that says why, so a broken state shows in the
359
+ catalog instead of ending the run. A readiness failure still attaches a picture of what the page showed, for diagnosis.
360
+
361
+ **Example**
362
+
363
+ ```ts
364
+ const shot = await captureUiState(page, { id: 'checkout/payment', route: '/checkout', ready: p => expectVisible(p, '#total') });
365
+ await publishChecks(store, uiStateLayer({ producer: 'playwright', recordedAtMs: Date.now(), environment: 'desktop-web', profile: { viewport: '1280x800' }, states: [shot] }));
366
+ ```
@@ -2252,6 +2252,8 @@ export type LayerRow = {
2252
2252
  readonly durationMs: number | null;
2253
2253
  /** What this result carries beyond its outcome. Absent when nothing. */
2254
2254
  readonly evidence?: EvidenceSummary;
2255
+ /** For a check of a `ui-state` or `ui-plan` part: its picture and declared facts, so a catalog is listed without another request (see `ui-state.ts`). Absent for every other kind. */
2256
+ readonly state?: UiStateFact;
2255
2257
  /** What became of this check's expected recording: `uploaded`, `referenced`, `skipped`, `unresolved` or `not-applicable`, with a reason when it is not uploaded. Absent when the check declared no expectation. Independent of `status`. */
2256
2258
  readonly recording?: { readonly state: string; readonly reason?: string };
2257
2259
  };
@@ -4581,7 +4583,9 @@ Serialises parameters as JSON key/value pairs sorted by key, so equal parameter
4581
4583
  export function isDiagnosticKind(kind: string): boolean
4582
4584
  ```
4583
4585
 
4584
- Whether a part `kind` only qualifies an outcome (today only `performance`) and so is not counted as a check when anything else is recorded.
4586
+ Whether a part `kind` only qualifies an outcome and so is not counted as a check when anything else is recorded: a
4587
+ `performance` timing, or a `ui-plan` entry (what a UI catalog expects, which a capture of the same state replaces in the
4588
+ tests' view). It never decides a slot: both still fill their own.
4585
4589
 
4586
4590
  #### `isOpinionKind`
4587
4591
 
@@ -9982,6 +9986,589 @@ export type ReportData = HtmlReportBundle & {
9982
9986
 
9983
9987
  A run's tests (`HtmlReportBundle`) plus the document title shown for them, for the standalone / Solid report viewer.
9984
9988
 
9989
+ ### Writing a UI catalog from any collector
9990
+
9991
+ Source: `packages/reports/src/ui-catalog-publish.ts`. Import: `@crvouga/atlas/reports/ui-catalog-publish`
9992
+
9993
+ Two small builders that turn what a collector captured, or what a planner expects, into the input of `publishChecks`
9994
+ (and of `atlas report publish --checks`), following the convention in {@link ./ui-state}. They are plain data in and plain
9995
+ data out: no browser, no test framework, no file system. A Playwright test, a Cypress run, a mobile device lab, a Storybook
9996
+ crawler or a shell script that writes the same JSON all produce the same catalog.
9997
+
9998
+ Shards need no coordination: each shard calls {@link uiStateLayer} for the states it captured (and optionally
9999
+ {@link uiPlanLayer} for the states it was given), with its own `shard` label, and publishes. A planner that knows the whole
10000
+ product publishes the plan once.
10001
+
10002
+ See docs/ui-catalog.md
10003
+
10004
+ #### `UiPicture`
10005
+
10006
+ ```ts
10007
+ export type UiPicture = {
10008
+ /** The file name; default `<id>.png`. */
10009
+ readonly name?: string;
10010
+ readonly data?: Uint8Array;
10011
+ readonly base64?: string;
10012
+ readonly uri?: string;
10013
+ /** Default `image/png`. */
10014
+ readonly contentType?: string;
10015
+ readonly width?: number;
10016
+ readonly height?: number;
10017
+ };
10018
+ ```
10019
+
10020
+ A picture to store with a capture: its bytes (`data`, `base64`), or where it already is (`uri`).
10021
+
10022
+ #### `UiStateCapture`
10023
+
10024
+ ```ts
10025
+ export type UiStateCapture = UiStateFacts & {
10026
+ readonly id: string;
10027
+ /** For a person; default the id. */
10028
+ readonly title?: string;
10029
+ /** `passed` (default): captured. `failed`: the capture was attempted and did not work (say why in `detail`). `running`: being taken. `not-run`: skipped. */
10030
+ readonly status?: 'passed' | 'failed' | 'running' | 'not-run';
10031
+ /** Why it failed or was skipped: a readiness check that never became true, a console error. */
10032
+ readonly detail?: string;
10033
+ readonly screenshot?: UiPicture;
10034
+ readonly durationMs?: number;
10035
+ /** More facts about the capture that never split it (`browser`, `os`). */
10036
+ readonly labels?: Readonly<Record<string, string>>;
10037
+ readonly tags?: readonly string[];
10038
+ };
10039
+ ```
10040
+
10041
+ One captured state. `id` must be stable across runs: it is how a new capture replaces an old one.
10042
+
10043
+ #### `UiStatePlan`
10044
+
10045
+ ```ts
10046
+ export type UiStatePlan = UiStateFacts & {
10047
+ readonly id: string;
10048
+ readonly title?: string;
10049
+ };
10050
+ ```
10051
+
10052
+ One expected state, or (with `inventory`) one surface of the product's inventory.
10053
+
10054
+ #### `UiLayerOrigin`
10055
+
10056
+ ```ts
10057
+ export type UiLayerOrigin = {
10058
+ /** The suite or tool that makes the layer. Part of the slot; a planner and a collector normally differ. */
10059
+ readonly producer: string;
10060
+ /** When the captures were taken (or the plan was made), epoch milliseconds. Take it from the run, not from the moment of publishing. */
10061
+ readonly recordedAtMs: number;
10062
+ /** The client: `desktop-web`, `mobile-web`, `ios`, `android`… Part of the slot. */
10063
+ readonly environment?: string;
10064
+ /** The rest of the profile, such as `{ viewport: '390x844', scheme: 'dark' }`. Each combination is its own result. Part of the slot. */
10065
+ readonly profile?: Readonly<Record<string, string>>;
10066
+ readonly sha?: string | null;
10067
+ readonly branch?: string | null;
10068
+ readonly runUrl?: string | null;
10069
+ /** Which shard this is (`2/4`, `checkout`, a machine name). Shown with each capture; never part of the slot. */
10070
+ readonly shard?: string;
10071
+ /** The execution these layers belong to, so shards of one run stay one run. */
10072
+ readonly run?: { readonly id: string; readonly title?: string };
10073
+ };
10074
+ ```
10075
+
10076
+ What every layer of a catalog says about where and when it was recorded.
10077
+
10078
+ #### `uiStateLayer`
10079
+
10080
+ ```ts
10081
+ export function uiStateLayer(input: UiLayerOrigin & { readonly states: readonly UiStateCapture[] }): PublishChecksInput
10082
+ ```
10083
+
10084
+ The input of `publishChecks` for the states one collector (or one shard) captured under one profile. Throws when a state id
10085
+ repeats, because the second would silently replace the first. A state without a `status` is `passed`: it was captured.
10086
+
10087
+ **Example**
10088
+
10089
+ ```ts
10090
+ await publishChecks(store, uiStateLayer({
10091
+ producer: 'web-capture', recordedAtMs: Date.now(), environment: 'mobile-web', profile: { viewport: '390x844', scheme: 'dark' }, shard: '1/4',
10092
+ states: [{ id: 'checkout/payment', title: 'Payment', route: '/checkout', covers: ['payment-form'], screenshot: { data: png } }]
10093
+ }));
10094
+ ```
10095
+
10096
+ #### `uiPlanLayer`
10097
+
10098
+ ```ts
10099
+ export function uiPlanLayer(input: UiLayerOrigin & { readonly states: readonly UiStatePlan[] }): PublishChecksInput
10100
+ ```
10101
+
10102
+ The input of `publishChecks` for what is expected: states that should be captured under this profile and, with `inventory`,
10103
+ surfaces the product is known to have. Every entry is `not-run`, so a plan never counts as a pass, and it fills a slot of
10104
+ its own (kind `ui-plan`), so a capture never erases it and it never hides a capture. Throws when an id repeats.
10105
+
10106
+ ### UI catalog
10107
+
10108
+ Source: `packages/reports/src/ui-catalog.ts`. Import: `@crvouga/atlas/reports/ui-catalog`
10109
+
10110
+ A browsable inventory of a product's user interface, computed from layers and nothing else: each **state** (a screen, or
10111
+ one state of a screen) captured once per **profile** (viewport, color scheme, device), with the facts needed to find and
10112
+ trust it, and what should exist but has not been captured yet. `uiCatalog` is a pure function of the winning results of
10113
+ the layers it is given, so it inherits the report contract's laws: the same layers in any order, split any way across
10114
+ backends, shards and machines, give the same catalog; a layer changes only the cells it fills; an unreadable layer is
10115
+ absent, never captured. It imports no collection framework and knows nothing about how a picture was taken.
10116
+
10117
+ How the convention maps onto the layer format is in {@link ./ui-state}: a state is a check of a `ui-state` part, a profile
10118
+ is the layer's `environment` and `parameters`, the expected set is a `ui-plan` part. The viewer computes the catalog from
10119
+ the report it already streams (so it fills in as results arrive), and the server answers the same document at
10120
+ `GET /api/reports/ui-catalog`.
10121
+
10122
+ See docs/ui-catalog.md
10123
+ See docs/report-contract.md#how-layers-compose
10124
+
10125
+ #### `UI_CATALOG_VERSION`
10126
+
10127
+ ```ts
10128
+ export const UI_CATALOG_VERSION = …
10129
+ ```
10130
+
10131
+ The `schemaVersion` of the catalog document.
10132
+
10133
+ #### `UiProfile`
10134
+
10135
+ ```ts
10136
+ export type UiProfile = {
10137
+ /** Stable identity: the environment and the sorted parameters. */
10138
+ readonly key: string;
10139
+ /** The client (`desktop-web`, `ios`…), or null when the layer named none. */
10140
+ readonly environment: string | null;
10141
+ /** The layer's other choices, such as `{ viewport: '390x844', scheme: 'dark' }`. */
10142
+ readonly parameters: Readonly<Record<string, string>>;
10143
+ /** For a person: the client and each parameter's value. */
10144
+ readonly label: string;
10145
+ };
10146
+ ```
10147
+
10148
+ One profile: the client and the choices a state was captured under. Its `key` identifies it across layers.
10149
+
10150
+ #### `UiCoverage`
10151
+
10152
+ ```ts
10153
+ export type UiCoverage = 'captured' | 'failed' | 'capturing' | 'skipped' | 'missing';
10154
+ ```
10155
+
10156
+ How one state stands in one profile. `captured`: a capture passed. `failed`: it was attempted and failed (its `detail`
10157
+ says why). `capturing`: it is being taken now. `skipped`: a producer recorded it as not run. `missing`: expected, and
10158
+ nothing has been recorded. Only `captured` is ever shown as captured; every other value means there is no picture to trust.
10159
+
10160
+ #### `UI_COVERAGE`
10161
+
10162
+ ```ts
10163
+ export const UI_COVERAGE = …
10164
+ ```
10165
+
10166
+ The values of {@link UiCoverage}, in the order a person reads them.
10167
+
10168
+ #### `UiImage`
10169
+
10170
+ ```ts
10171
+ export type UiImage = UiScreenshot & {
10172
+ /** Where the stored bytes are, relative to the reports API root (`/layers/<layer>/blobs/attachments/<sha256>`). Absent for an address-only or omitted picture. */
10173
+ readonly path?: string;
10174
+ };
10175
+ ```
10176
+
10177
+ A picture, with the address a browser loads it from.
10178
+
10179
+ #### `UiOrigin`
10180
+
10181
+ ```ts
10182
+ export type UiOrigin = {
10183
+ readonly layer: string;
10184
+ readonly backend: string;
10185
+ readonly producer: string;
10186
+ readonly recordedAtMs: number;
10187
+ readonly sha: string | null;
10188
+ readonly branch: string | null;
10189
+ readonly runUrl: string | null;
10190
+ readonly freshness?: 'unknown' | 'modified';
10191
+ };
10192
+ ```
10193
+
10194
+ Where a capture came from: the layer, the backend that holds it, and the provenance the layer recorded.
10195
+
10196
+ #### `UiCapture`
10197
+
10198
+ ```ts
10199
+ export type UiCapture = {
10200
+ readonly status: EvidenceStatus;
10201
+ /** Why it failed or was skipped; empty otherwise. */
10202
+ readonly detail: string;
10203
+ readonly durationMs: number | null;
10204
+ /** The picture, or null when the capture has none (or it was left out: see `omitted`). */
10205
+ readonly image: UiImage | null;
10206
+ /** The check's labels: what describes the capture without splitting it (a `shard`, a `browser`). */
10207
+ readonly labels: Readonly<Record<string, string>>;
10208
+ readonly origin: UiOrigin;
10209
+ /** How many other captures of this cell exist from other producers. The winner is the one the report contract picks. */
10210
+ readonly alternatives: number;
10211
+ };
10212
+ ```
10213
+
10214
+ The winning capture of one cell.
10215
+
10216
+ #### `UiCell`
10217
+
10218
+ ```ts
10219
+ export type UiCell = {
10220
+ readonly profile: UiProfile;
10221
+ readonly coverage: UiCoverage;
10222
+ /** Whether a plan expects this state in this profile: `declared`; `undeclared` when plans exist and none names it; `unknown` when no plan was published at all. */
10223
+ readonly expected: 'declared' | 'undeclared' | 'unknown';
10224
+ readonly capture: UiCapture | null;
10225
+ /** The layer of the plan that expects it, when one does. */
10226
+ readonly planned: { readonly layer: string; readonly backend: string } | null;
10227
+ };
10228
+ ```
10229
+
10230
+ One state in one profile.
10231
+
10232
+ #### `UiCatalogState`
10233
+
10234
+ ```ts
10235
+ export type UiCatalogState = {
10236
+ /** The check id the producers gave it. */
10237
+ readonly id: string;
10238
+ readonly title: string;
10239
+ readonly route: string | null;
10240
+ readonly fixture: string | null;
10241
+ readonly cohort: string | null;
10242
+ readonly area: string | null;
10243
+ readonly flags: readonly string[];
10244
+ /** The surfaces it covers. */
10245
+ readonly covers: readonly string[];
10246
+ /** The producers that captured or planned it. */
10247
+ readonly producers: readonly string[];
10248
+ /** One cell per profile, ordered by the profile's label. */
10249
+ readonly cells: readonly UiCell[];
10250
+ };
10251
+ ```
10252
+
10253
+ One state of the product's interface, across its profiles.
10254
+
10255
+ #### `UiSurface`
10256
+
10257
+ ```ts
10258
+ export type UiSurface = {
10259
+ readonly id: string;
10260
+ /** True when a `ui-plan` inventory entry names it: the product is known to have it. */
10261
+ readonly inventoried: boolean;
10262
+ /** States that cover it. */
10263
+ readonly states: number;
10264
+ /** Of those, states with at least one captured profile. */
10265
+ readonly captured: number;
10266
+ /** `covered`: some covering state is captured. `uncaptured`: states cover it and none is captured yet. `uncovered`: no state covers it. */
10267
+ readonly status: 'covered' | 'uncaptured' | 'uncovered';
10268
+ };
10269
+ ```
10270
+
10271
+ One surface of the product: an inventory entry, or something a state says it covers.
10272
+
10273
+ #### `UiFacetValue`
10274
+
10275
+ ```ts
10276
+ export type UiFacetValue = { readonly value: string; readonly count: number; readonly label?: string };
10277
+ ```
10278
+
10279
+ A value of a facet and how many states have it.
10280
+
10281
+ #### `UiCatalogFacets`
10282
+
10283
+ ```ts
10284
+ export type UiCatalogFacets = {
10285
+ readonly profiles: readonly UiFacetValue[];
10286
+ readonly coverage: readonly UiFacetValue[];
10287
+ readonly areas: readonly UiFacetValue[];
10288
+ readonly routes: readonly UiFacetValue[];
10289
+ readonly cohorts: readonly UiFacetValue[];
10290
+ readonly flags: readonly UiFacetValue[];
10291
+ readonly surfaces: readonly UiFacetValue[];
10292
+ readonly producers: readonly UiFacetValue[];
10293
+ };
10294
+ ```
10295
+
10296
+ The values each filter offers, over every state of the catalog.
10297
+
10298
+ #### `UiCatalogSummary`
10299
+
10300
+ ```ts
10301
+ export type UiCatalogSummary = {
10302
+ readonly states: number;
10303
+ readonly profiles: number;
10304
+ readonly cells: number;
10305
+ readonly coverage: Readonly<Record<UiCoverage, number>>;
10306
+ /** Captured or failed cells no plan expects. Zero when no plan was published, because then nothing is known to be unexpected. */
10307
+ readonly undeclared: number;
10308
+ /** Whether any plan (an expected set) was published. Without one, `missing` can only ever be zero. */
10309
+ readonly planned: boolean;
10310
+ readonly surfaces: number;
10311
+ readonly uncoveredSurfaces: number;
10312
+ };
10313
+ ```
10314
+
10315
+ The catalog in numbers. Cells count a state once per profile.
10316
+
10317
+ #### `UiCatalog`
10318
+
10319
+ ```ts
10320
+ export type UiCatalog = {
10321
+ readonly schemaVersion: typeof UI_CATALOG_VERSION;
10322
+ readonly version?: string;
10323
+ readonly summary: UiCatalogSummary;
10324
+ readonly profiles: readonly UiProfile[];
10325
+ readonly states: readonly UiCatalogState[];
10326
+ readonly surfaces: readonly UiSurface[];
10327
+ readonly facets: UiCatalogFacets;
10328
+ /** What could not be read. A state that depended on it is not in the catalog as captured. Filled by the caller who read the backends. */
10329
+ readonly sources: { readonly layers: number; readonly backends: readonly BackendInfo[]; readonly notices: readonly string[] };
10330
+ };
10331
+ ```
10332
+
10333
+ The catalog, as `GET /api/reports/ui-catalog` answers it (`version` is the report's, when the caller knows it).
10334
+
10335
+ #### `UiCatalogOptions`
10336
+
10337
+ ```ts
10338
+ export type UiCatalogOptions = {
10339
+ /** The report's version, repeated in the document. */
10340
+ readonly version?: string;
10341
+ /** The backends, with whether each could be read. */
10342
+ readonly backends?: readonly BackendInfo[];
10343
+ /** What could not be read. */
10344
+ readonly notices?: readonly string[];
10345
+ };
10346
+ ```
10347
+
10348
+ What a caller may say about how the layers were read.
10349
+
10350
+ #### `uiProfile`
10351
+
10352
+ ```ts
10353
+ export function uiProfile(layer: Pick<LayerHeader, 'environment' | 'parameters'>): UiProfile
10354
+ ```
10355
+
10356
+ The profile of a layer: its environment and its other parameters.
10357
+
10358
+ #### `uiCatalog`
10359
+
10360
+ ```ts
10361
+ export function uiCatalog(layers: readonly LayerEvidence[], options: UiCatalogOptions = {}): UiCatalog
10362
+ ```
10363
+
10364
+ The UI catalog of some layers (as the effective report holds them, or any set of {@link LayerEvidence}).
10365
+
10366
+ Only rows of `ui-state` and `ui-plan` parts take part. The winner of every slot is decided by the report contract
10367
+ (`combineEvidence`), so the result does not depend on the order of `layers` or on how they are divided among backends. A
10368
+ state is the set of cells that share a check `id`; a cell is one profile of it. A cell is `captured` only when the winning
10369
+ capture passed; a plan that nothing has captured is `missing`. When no plan exists nothing is `missing` and nothing is
10370
+ `undeclared`: what is expected is then unknown, and the catalog says so (`summary.planned`).
10371
+
10372
+ **Example**
10373
+
10374
+ ```ts
10375
+ const catalog = uiCatalog(effective.layers, { version: effective.version, backends: effective.backends });
10376
+ catalog.states.map(state => [state.id, state.cells.map(cell => cell.coverage)]);
10377
+ ```
10378
+
10379
+ #### `UiCatalogQuery`
10380
+
10381
+ ```ts
10382
+ export type UiCatalogQuery = {
10383
+ /** Words (all of them) found in the id, title, route, fixture, cohort, area, flags, surfaces or producers. */
10384
+ readonly q?: string;
10385
+ readonly coverage?: readonly UiCoverage[];
10386
+ /** Profile keys, as {@link UiProfile.key}. */
10387
+ readonly profile?: readonly string[];
10388
+ readonly area?: readonly string[];
10389
+ readonly route?: readonly string[];
10390
+ readonly cohort?: readonly string[];
10391
+ readonly flag?: readonly string[];
10392
+ /** Surfaces a state covers. */
10393
+ readonly surface?: readonly string[];
10394
+ readonly producer?: readonly string[];
10395
+ };
10396
+ ```
10397
+
10398
+ What a person narrows the catalog by. Every field is optional; nothing asked is everything. Values within a field are alternatives, fields combine with AND.
10399
+
10400
+ #### `parseUiCatalogQuery`
10401
+
10402
+ ```ts
10403
+ export function parseUiCatalogQuery(params: URLSearchParams): UiCatalogQuery
10404
+ ```
10405
+
10406
+ Reads a query from URL search parameters (`q`, and the comma-separated {@link UiCatalogQuery} lists). Unknown coverage values are left out, never an error.
10407
+
10408
+ #### `uiCatalogParams`
10409
+
10410
+ ```ts
10411
+ export function uiCatalogParams(query: UiCatalogQuery): URLSearchParams
10412
+ ```
10413
+
10414
+ A query as URL search parameters, the form {@link parseUiCatalogQuery} reads back.
10415
+
10416
+ #### `isUiCatalogFiltered`
10417
+
10418
+ ```ts
10419
+ export function isUiCatalogFiltered(query: UiCatalogQuery): boolean
10420
+ ```
10421
+
10422
+ Whether a query narrows anything.
10423
+
10424
+ #### `filterUiCatalog`
10425
+
10426
+ ```ts
10427
+ export function filterUiCatalog(catalog: UiCatalog, query: UiCatalogQuery): UiCatalog
10428
+ ```
10429
+
10430
+ The part of a catalog a query asks for. A state is kept when it matches the words and the fact filters; its cells are
10431
+ kept when they match the `profile` and `coverage` filters, and a state left with no cell is dropped. The summary is
10432
+ recomputed for what is kept; the profiles, surfaces, facets and sources stay those of the whole catalog, so the
10433
+ controls keep offering every choice.
10434
+
10435
+ #### `pageUiCatalog`
10436
+
10437
+ ```ts
10438
+ export function pageUiCatalog(catalog: UiCatalog, page: { readonly limit?: number; readonly offset?: number }): UiCatalog & { readonly total: number; readonly offset: number; readonly limit: number }
10439
+ ```
10440
+
10441
+ A page of a catalog's states, for a document that would otherwise be large. The summary, facets and sources are the whole catalog's.
10442
+
10443
+ #### `uiCatalogMarkdown`
10444
+
10445
+ ```ts
10446
+ export function uiCatalogMarkdown(catalog: UiCatalog): string
10447
+ ```
10448
+
10449
+ The catalog as Markdown for a person or an agent reading text: the summary in words, the surfaces that are not covered, and
10450
+ one line per state with its coverage in each profile. Only `captured` is called captured.
10451
+
10452
+ ### UI state conventions
10453
+
10454
+ Source: `packages/reports/src/ui-state.ts`. Import: `@crvouga/atlas/reports/ui-state`
10455
+
10456
+ How a captured UI state is written in a layer, using nothing but what the layer format already has. A **state** is a
10457
+ check in a part of kind `ui-state`: its `id` names the state, its `status` says how the capture went, a `screenshot`
10458
+ attachment is the picture, and the facts the producer declares about it (route, fixture, cohort, flags, the surfaces it
10459
+ covers) sit under the `atlas.ui-state` extension. A **profile** (viewport, color scheme, device) is the layer's
10460
+ `environment` and `parameters`, so a state captured at another viewport is another result, side by side. What is
10461
+ **expected** is a layer of kind `ui-plan` with the same ids and profiles, every check `not-run`.
10462
+
10463
+ Nothing here decides a result: a status, a slot and a winner are what the report contract says they are. This module
10464
+ only reads the convention into a small optional row field ({@link UiStateFact}) so a viewer needs no second request to
10465
+ list a catalog. A producer that writes something else is still read; what cannot be read is left out, never rejected.
10466
+
10467
+ See docs/ui-catalog.md
10468
+
10469
+ #### `UI_STATE_KIND`
10470
+
10471
+ ```ts
10472
+ export const UI_STATE_KIND = …
10473
+ ```
10474
+
10475
+ The part kind of captured states.
10476
+
10477
+ #### `UI_PLAN_KIND`
10478
+
10479
+ ```ts
10480
+ export const UI_PLAN_KIND = …
10481
+ ```
10482
+
10483
+ The part kind of the expected set: states (and surfaces) that should exist, whether or not they were captured.
10484
+
10485
+ #### `UI_STATE_EXTENSION`
10486
+
10487
+ ```ts
10488
+ export const UI_STATE_EXTENSION = …
10489
+ ```
10490
+
10491
+ The check extension that carries the facts a producer declares about a state.
10492
+
10493
+ #### `isUiKind`
10494
+
10495
+ ```ts
10496
+ export function isUiKind(kind: string): boolean
10497
+ ```
10498
+
10499
+ Whether a part kind belongs to the UI catalog (`ui-state` or `ui-plan`).
10500
+
10501
+ #### `UiScreenshot`
10502
+
10503
+ ```ts
10504
+ export type UiScreenshot = {
10505
+ readonly name: string;
10506
+ readonly bundleKey?: string;
10507
+ readonly uri?: string;
10508
+ readonly contentType?: string;
10509
+ readonly width?: number;
10510
+ readonly height?: number;
10511
+ readonly bytes?: number;
10512
+ readonly sha256?: string;
10513
+ /** Why the producer left the bytes out (`too-large`, `policy`…): a picture that was never taken is told apart from one left out. */
10514
+ readonly omitted?: string;
10515
+ };
10516
+ ```
10517
+
10518
+ The picture of a captured state: a stored file (`bundleKey`), or an address (`uri`). Everything but `name` is optional.
10519
+
10520
+ #### `UiStateFacts`
10521
+
10522
+ ```ts
10523
+ export type UiStateFacts = {
10524
+ /** The address or screen the state is of (`/checkout/payment`). */
10525
+ readonly route?: string;
10526
+ /** The test data it was captured with. */
10527
+ readonly fixture?: string;
10528
+ /** The audience or account kind it shows (`new-customer`). */
10529
+ readonly cohort?: string;
10530
+ /** Feature flags that were on. */
10531
+ readonly flags?: readonly string[];
10532
+ /** The product surfaces (screens, components) this state covers, for coverage against an inventory. */
10533
+ readonly covers?: readonly string[];
10534
+ /** A grouping a person navigates by (`checkout`). */
10535
+ readonly area?: string;
10536
+ /** On a `ui-plan` check only: this entry is a surface of the product's inventory, not a state. Nothing is captured for it. */
10537
+ readonly inventory?: boolean;
10538
+ };
10539
+ ```
10540
+
10541
+ The facts a producer declares about a state, under the `atlas.ui-state` extension. All optional, all strings or lists of strings.
10542
+
10543
+ #### `UiStateFact`
10544
+
10545
+ ```ts
10546
+ export type UiStateFact = UiStateFacts & {
10547
+ /** The check's picture: its `screenshot` attachment, else its first image. */
10548
+ readonly screenshot?: UiScreenshot;
10549
+ };
10550
+ ```
10551
+
10552
+ What a row of a `ui-state` or `ui-plan` part carries for the catalog. Rides in the report row, only for those kinds.
10553
+
10554
+ #### `uiStateFacts`
10555
+
10556
+ ```ts
10557
+ export function uiStateFacts(check: Pick<Check, 'extensions'>): UiStateFacts
10558
+ ```
10559
+
10560
+ The declared facts of a check, read from its `atlas.ui-state` extension. Strings are trimmed and bounded, lists are
10561
+ distinct and bounded, and anything of another shape is dropped; a check without the extension has no facts.
10562
+
10563
+ #### `uiStateOf`
10564
+
10565
+ ```ts
10566
+ export function uiStateOf(kind: string, check: Pick<Check, 'extensions' | 'attachments'>): UiStateFact | undefined
10567
+ ```
10568
+
10569
+ What a check of a part of `kind` carries for the catalog: its declared facts and, in a `ui-state` part, its picture.
10570
+ `undefined` for any other kind, and for a UI check that carries neither, so ordinary rows stay as small as they were.
10571
+
9985
10572
  ### Report viewer navigation state
9986
10573
 
9987
10574
  Source: `packages/reports/src/use-report-nav.ts`. Import: `@crvouga/atlas/reports/use-report-nav`