@crvouga/atlas 1.20.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/dist/core/src/cli.d.ts +2 -2
  3. package/dist/core/src/cli.js +7 -1
  4. package/dist/critics/src/catalog-client.d.ts +91 -0
  5. package/dist/critics/src/catalog-client.js +194 -0
  6. package/dist/critics/src/cli-calibrate.d.ts +4 -0
  7. package/dist/critics/src/cli-calibrate.js +211 -0
  8. package/dist/critics/src/cli-common.d.ts +108 -0
  9. package/dist/critics/src/cli-common.js +155 -0
  10. package/dist/critics/src/cli-review.d.ts +4 -0
  11. package/dist/critics/src/cli-review.js +174 -0
  12. package/dist/critics/src/cli-watch.d.ts +4 -0
  13. package/dist/critics/src/cli-watch.js +191 -0
  14. package/dist/critics/src/cli.d.ts +13 -0
  15. package/dist/critics/src/cli.js +490 -0
  16. package/dist/critics/src/index.d.ts +3 -0
  17. package/dist/critics/src/index.js +3 -0
  18. package/dist/critics/src/judging.d.ts +8 -0
  19. package/dist/critics/src/llm.d.ts +120 -0
  20. package/dist/critics/src/llm.js +266 -0
  21. package/dist/critics/src/marks.d.ts +74 -0
  22. package/dist/critics/src/marks.js +76 -0
  23. package/dist/critics/src/registry.d.ts +125 -0
  24. package/dist/critics/src/registry.js +203 -0
  25. package/dist/critics/src/spool.d.ts +32 -0
  26. package/dist/critics/src/spool.js +129 -0
  27. package/dist/critics/src/uiclip.d.ts +82 -0
  28. package/dist/critics/src/uiclip.js +167 -0
  29. package/dist/reports/src/compose.d.ts +23 -1
  30. package/dist/reports/src/compose.js +29 -19
  31. package/dist/reports/src/critique-review.d.ts +111 -0
  32. package/dist/reports/src/critique-review.js +123 -0
  33. package/dist/reports/src/critique-row.d.ts +24 -0
  34. package/dist/reports/src/critique-row.js +39 -0
  35. package/dist/reports/src/evidence-types.d.ts +5 -0
  36. package/dist/reports/src/layer-merge.js +5 -0
  37. package/dist/reports/src/report-cli.d.ts +13 -0
  38. package/dist/reports/src/report-cli.js +6 -4
  39. package/dist/reports/src/report-index.d.ts +6 -0
  40. package/dist/reports/src/report-index.js +12 -0
  41. package/dist/reports/src/ui-catalog-cli.d.ts +10 -0
  42. package/dist/reports/src/ui-catalog-cli.js +391 -0
  43. package/dist/reports/src/ui-catalog-diff.d.ts +104 -0
  44. package/dist/reports/src/ui-catalog-diff.js +130 -0
  45. package/dist/reports/src/ui-catalog-entries.d.ts +147 -0
  46. package/dist/reports/src/ui-catalog-entries.js +190 -0
  47. package/dist/reports/src/ui-catalog-export.d.ts +82 -0
  48. package/dist/reports/src/ui-catalog-export.js +221 -0
  49. package/dist/reports/src/ui-catalog-publish.js +2 -2
  50. package/dist/reports/src/ui-catalog.d.ts +2 -0
  51. package/dist/reports/src/ui-catalog.js +1 -1
  52. package/dist/reports/src/ui-state.d.ts +2 -0
  53. package/dist/reports/src/ui-state.js +2 -2
  54. package/dist/schema/src/api.d.ts +653 -1
  55. package/dist/schema/src/api.js +44 -0
  56. package/dist/schema/src/cli-results.d.ts +991 -1
  57. package/dist/schema/src/cli-results.js +265 -1
  58. package/dist/schema/src/critique.d.ts +31 -0
  59. package/dist/schema/src/critique.js +26 -0
  60. package/dist/schema/src/server-config.d.ts +3 -3
  61. package/dist/server/src/app.js +28 -1
  62. package/dist/server/src/openapi.js +19 -2
  63. package/docs/agents.md +16 -2
  64. package/docs/critics.md +273 -9
  65. package/docs/extending.md +1 -1
  66. package/docs/reference/cli.md +126 -1
  67. package/docs/reference/core.md +1 -1
  68. package/docs/reference/critics.md +1131 -29
  69. package/docs/reference/http-api.md +40 -0
  70. package/docs/reference/reports.md +855 -0
  71. package/docs/reference/schema.md +197 -1
  72. package/docs/report-contract.md +2 -1
  73. package/docs/server.md +2 -0
  74. package/docs/ui-catalog.md +110 -13
  75. package/docs/ui.md +69 -2
  76. package/llms-full.txt +636 -26
  77. package/llms.txt +1 -1
  78. package/package.json +9 -1
  79. package/packages/critics/README.md +1 -0
  80. package/packages/schema/schemas/critic-calibrate-result.schema.json +274 -0
  81. package/packages/schema/schemas/critic-doctor-result.schema.json +74 -0
  82. package/packages/schema/schemas/critic-list-result.schema.json +90 -0
  83. package/packages/schema/schemas/critic-review-result.schema.json +397 -0
  84. package/packages/schema/schemas/critic-run-result.schema.json +249 -0
  85. package/packages/schema/schemas/critic-setup-result.schema.json +34 -0
  86. package/packages/schema/schemas/critic-watch-result.schema.json +211 -0
  87. package/packages/schema/schemas/critique-mark.schema.json +81 -0
  88. package/packages/schema/schemas/effective-delta.schema.json +337 -0
  89. package/packages/schema/schemas/effective-evidence.schema.json +337 -0
  90. package/packages/schema/schemas/layer-rows.schema.json +337 -0
  91. package/packages/schema/schemas/ui-catalog-diff-result.schema.json +201 -0
  92. package/packages/schema/schemas/ui-catalog-export-result.schema.json +83 -0
  93. package/packages/schema/schemas/ui-catalog-publish-result.schema.json +116 -0
  94. package/packages/schema/schemas/ui-catalog-status.schema.json +197 -0
  95. package/packages/server/openapi.json +6755 -4738
  96. package/packages/visualizer/src/app/AppShell.tsx +3 -1
  97. package/packages/visualizer/src/app/router.tsx +20 -7
  98. package/packages/visualizer/src/catalog/Picture.tsx +32 -0
  99. package/packages/visualizer/src/catalog/UiCatalogCompare.tsx +289 -0
  100. package/packages/visualizer/src/catalog/UiCatalogPage.tsx +18 -28
  101. package/packages/visualizer/src/catalog/history.ts +61 -0
  102. package/packages/visualizer/src/catalog/search.ts +14 -3
  103. package/packages/visualizer/src/components/Header.tsx +16 -4
  104. package/packages/visualizer/src/critiques/CritiquesPage.tsx +402 -0
  105. package/packages/visualizer/src/critiques/FindingOverlay.tsx +50 -0
  106. package/packages/visualizer/src/critiques/ranking.ts +54 -0
  107. package/packages/visualizer/src/critiques/search.ts +41 -0
  108. package/packages/visualizer/src/evidence/applicability-view.tsx +11 -7
  109. package/packages/visualizer/src/evidence/behaviors-page.tsx +22 -15
  110. package/packages/visualizer/src/evidence/evidence-context.tsx +2 -1
  111. package/packages/visualizer/src/explorer/Explorer.tsx +5 -0
  112. package/packages/visualizer/src/explorer/TestDetail.tsx +4 -1
  113. package/packages/visualizer/src/explorer/parts.tsx +19 -1
  114. package/packages/visualizer/src/explorer/playlist.tsx +11 -7
@@ -1420,6 +1420,32 @@ export function summarize(combined: CombinedEvidence): ReportSummary
1420
1420
 
1421
1421
  Counts a combined report's winning results in total and per environment, and lists the specs that have no result at all.
1422
1422
 
1423
+ #### `BackendLayers`
1424
+
1425
+ ```ts
1426
+ export type BackendLayers = {
1427
+ /** Each layer that could be read, with the specs it recorded. A layer several backends hold appears once per backend. */
1428
+ readonly loaded: readonly { readonly evidence: LayerEvidence; readonly specs: readonly SpecEntry[] }[];
1429
+ /** Every backend given, sorted by id, with whether it could be listed. */
1430
+ readonly backends: readonly BackendInfo[];
1431
+ /** What could not be read, sorted and without repeats. */
1432
+ readonly notices: readonly string[];
1433
+ };
1434
+ ```
1435
+
1436
+ Every layer of some backends, read but not yet composed.
1437
+
1438
+ #### `readBackendLayers`
1439
+
1440
+ ```ts
1441
+ export async function readBackendLayers(backends: readonly BackendInput[], options: { readonly concurrency?: number } = {}): Promise<BackendLayers>
1442
+ ```
1443
+
1444
+ Reads every layer of every backend, superseded ones included, without composing them. This is what {@link combineBackends}
1445
+ starts from; a caller that needs layers a newer layer has replaced (to compare two commits, for example) reads them here.
1446
+ A backend that cannot be listed, or a layer that cannot be understood, is a notice, never an omission that looks like a pass.
1447
+ Throws for an invalid or repeated backend `id`, before anything is read.
1448
+
1423
1449
  #### `combineBackends`
1424
1450
 
1425
1451
  ```ts
@@ -1474,6 +1500,220 @@ commutative and safe to repeat after a failure.
1474
1500
  A layer's catalog entry is copied last, and its manifest after its files, so a reader of `to` never sees an
1475
1501
  entry for a layer whose content has not arrived.
1476
1502
 
1503
+ ### Reading critiques together
1504
+
1505
+ Source: `packages/reports/src/critique-review.ts`. Import: `@crvouga/atlas/reports/critique-review`
1506
+
1507
+ What a person or a command wants to know after critics have spoken: which states need polish first (`rankCritiques`), do the
1508
+ critics agree (`critiqueAgreement`), and what did people decide about the opinions (`markStanding`). These are readings of
1509
+ evidence that is already in the report. They count nothing toward a test total and decide no status, slot or gate: an
1510
+ opinion is an opinion, and so is a person's mark of it. Everything here is pure, so the viewer and `atlas critic review`
1511
+ read the same way.
1512
+
1513
+ See docs/critics.md
1514
+
1515
+ #### `RankedEntry`
1516
+
1517
+ ```ts
1518
+ export type RankedEntry<E extends Pick<CatalogEntry, 'key' | 'state' | 'critiques'> = CatalogEntry> = { readonly entry: E; readonly found: EntryCritique };
1519
+ ```
1520
+
1521
+ One entry and the critic's newest critique of it.
1522
+
1523
+ #### `CritiqueRanking`
1524
+
1525
+ ```ts
1526
+ export type CritiqueRanking<E extends Pick<CatalogEntry, 'key' | 'state' | 'critiques'> = CatalogEntry> = {
1527
+ /** Entries judged at their current picture with a score, lowest first. */
1528
+ readonly ranked: readonly RankedEntry<E>[];
1529
+ /** How the list was ranked: by standing within the state kind (when every score has one), or by raw score. */
1530
+ readonly rankedBy: 'standing-within-kind' | 'score';
1531
+ /** Entries whose newest critique judged an earlier picture, or that the critic could not judge: listed, never ranked. */
1532
+ readonly unranked: readonly RankedEntry<E>[];
1533
+ /** Entries in scope the critic has not judged at all. */
1534
+ readonly unjudged: number;
1535
+ };
1536
+ ```
1537
+
1538
+ The ranking of the entries one critic judged.
1539
+
1540
+ #### `rankCritiques`
1541
+
1542
+ ```ts
1543
+ export function rankCritiques<E extends Pick<CatalogEntry, 'key' | 'state' | 'critiques'>>(entries: readonly E[], critic: string | null, kind?: string): CritiqueRanking<E>
1544
+ ```
1545
+
1546
+ Ranks the entries a critic judged, lowest first, so the likeliest problems are reviewed first. A score is read as
1547
+ higher-is-better (the convention of the critique format). When every ranked score also carries its standing among the scores
1548
+ of the same state kind (`verdict.relative.percentile`) that standing ranks them instead, because a loading overlay is not
1549
+ compared with a full screen. An entry whose picture changed since it was judged, or that the critic could not judge, is not
1550
+ ranked but is returned; an entry the critic never saw is only counted.
1551
+
1552
+ - `entries`: the entries in view.
1553
+
1554
+ - `critic`: the critic whose critiques rank them.
1555
+
1556
+ - `kind`: when given, only entries of this state kind.
1557
+
1558
+ #### `Stance`
1559
+
1560
+ ```ts
1561
+ export type Stance = 'flags' | 'clear' | 'no-view';
1562
+ ```
1563
+
1564
+ What a critic's opinion amounts to for a reader who wants to know whether it flags the UI: it does, it does not, or it gives no view.
1565
+
1566
+ #### `FLAGGING_PERCENTILE`
1567
+
1568
+ ```ts
1569
+ export const FLAGGING_PERCENTILE = …
1570
+ ```
1571
+
1572
+ A score at or below this standing within its state kind (the lowest quarter) counts as flagging, when the critic gave no label.
1573
+
1574
+ #### `critiqueStance`
1575
+
1576
+ ```ts
1577
+ export function critiqueStance(critique: Critique): Stance
1578
+ ```
1579
+
1580
+ Whether a critique flags the UI. A failed critique, or one with nothing to go on, gives no view and is never read as fine.
1581
+ Otherwise the critic's own label decides (`needs-polish` flags, `ok` clears); with no label, findings flag; with neither, a score
1582
+ in the lowest quarter of its state kind (`relative.percentile` at most {@link FLAGGING_PERCENTILE}) flags and a higher one clears. A bare
1583
+ score with no standing gives no view, because a raw score is not absolute.
1584
+
1585
+ #### `AgreementOutcome`
1586
+
1587
+ ```ts
1588
+ export type AgreementOutcome = 'agree-flag' | 'agree-clear' | 'disagree' | 'unclear';
1589
+ ```
1590
+
1591
+ How the critics who judged an entry at its current picture stand together.
1592
+
1593
+ #### `CriticView`
1594
+
1595
+ ```ts
1596
+ export type CriticView = {
1597
+ readonly critic: string;
1598
+ readonly version: string;
1599
+ readonly stance: Stance;
1600
+ readonly score?: number;
1601
+ readonly percentile?: number;
1602
+ readonly label?: string;
1603
+ };
1604
+ ```
1605
+
1606
+ One critic's view of an entry.
1607
+
1608
+ #### `AgreementRow`
1609
+
1610
+ ```ts
1611
+ export type AgreementRow<E extends Pick<CatalogEntry, 'key' | 'state' | 'critiques'> = CatalogEntry> = {
1612
+ readonly entry: E;
1613
+ readonly views: readonly CriticView[];
1614
+ readonly outcome: AgreementOutcome;
1615
+ };
1616
+ ```
1617
+
1618
+ An entry judged at its current picture by two or more critics, and whether they agree.
1619
+
1620
+ #### `CritiqueAgreement`
1621
+
1622
+ ```ts
1623
+ export type CritiqueAgreement<E extends Pick<CatalogEntry, 'key' | 'state' | 'critiques'> = CatalogEntry> = {
1624
+ /** Entries judged by two or more critics at their current picture, those every critic flags first, then those they disagree on. */
1625
+ readonly rows: readonly AgreementRow<E>[];
1626
+ /** Entries only one critic has judged at the current picture: there is nothing to compare. */
1627
+ readonly single: number;
1628
+ /** The critics that have judged at least one entry at its current picture. */
1629
+ readonly critics: readonly string[];
1630
+ };
1631
+ ```
1632
+
1633
+ The agreement of critics over some entries.
1634
+
1635
+ #### `critiqueAgreement`
1636
+
1637
+ ```ts
1638
+ export function critiqueAgreement<E extends Pick<CatalogEntry, 'key' | 'state' | 'critiques'>>(entries: readonly E[], kind?: string): CritiqueAgreement<E>
1639
+ ```
1640
+
1641
+ Compares what the critics think of the same pictures. Only critiques of an entry's current picture count, and only each critic's
1642
+ newest. Critics that give no view ({@link critiqueStance}) are shown but do not make an entry agree or disagree: with fewer than
1643
+ two views the outcome is `unclear`, never agreement. An agreement is a reading for people; it is not a verdict, a status or a gate.
1644
+
1645
+ #### `MarkStanding`
1646
+
1647
+ ```ts
1648
+ export type MarkStanding = {
1649
+ /** `accepted`: only accepts. `dismissed`: only dismissals. `disputed`: reviewers disagree. `unmarked`: nobody has marked it at the current picture. */
1650
+ readonly state: 'accepted' | 'dismissed' | 'disputed' | 'unmarked';
1651
+ /** Reviewers who accepted it (their name, or the producer when they gave none). */
1652
+ readonly accepted: readonly string[];
1653
+ /** Reviewers who dismissed it. */
1654
+ readonly dismissed: readonly string[];
1655
+ /** Marks made about an earlier picture, which count for nothing now. */
1656
+ readonly earlier: number;
1657
+ /** The notes reviewers left, newest first. */
1658
+ readonly notes: readonly string[];
1659
+ };
1660
+ ```
1661
+
1662
+ What people decided about one opinion (a critic's critique of an entry, or one of its findings).
1663
+
1664
+ #### `markDecision`
1665
+
1666
+ ```ts
1667
+ export function markDecision(mark: Pick<EntryMark['mark'], 'decision'>): 'accept' | 'dismiss' | null
1668
+ ```
1669
+
1670
+ The decision a mark records: `accept`, `dismiss`, or `null` for a word this reader does not know (shown as written, counted as neither).
1671
+
1672
+ #### `markStanding`
1673
+
1674
+ ```ts
1675
+ export function markStanding(marks: readonly EntryMark[], target: { readonly critic: string; readonly finding?: string | undefined }): MarkStanding
1676
+ ```
1677
+
1678
+ What reviewers decided about a critic's opinion of an entry. Only marks made about the picture the entry has now count; those
1679
+ about an earlier picture are tallied apart. Each reviewer's newest mark is the one that is held (the slot rule); a reviewer who
1680
+ accepted and later dismissed has dismissed. Marks with a decision this reader does not know count as neither.
1681
+
1682
+ - `marks`: the entry's marks (`CatalogEntry.marks`).
1683
+
1684
+ - `target`: the critic whose opinion is marked; `finding` names one finding, and without it the critique as a whole.
1685
+
1686
+ ### Critiques in report rows
1687
+
1688
+ Source: `packages/reports/src/critique-row.ts`. Import: `@crvouga/atlas/reports/critique-row`
1689
+
1690
+ A check of a part of kind `critique` carries an automated reviewer's opinion in `extensions["atlas.critique"]`
1691
+ (see `@crvouga/atlas/critics`). This module reads that extension into the optional `critique` field of a report row,
1692
+ so a viewer can list critiques without a second request, exactly as `state` carries a UI state. It decides nothing:
1693
+ the row's `status` says only whether the critic ran, a critique is never counted, and an extension this reader cannot
1694
+ read (a missing subject, a score that is not a number, a version it does not know) leaves the row as it was, an
1695
+ unknown opinion and never a pass.
1696
+
1697
+ See docs/critics.md
1698
+
1699
+ #### `critiqueOf`
1700
+
1701
+ ```ts
1702
+ export function critiqueOf(kind: string, check: Pick<Check, 'extensions'>): Critique | undefined
1703
+ ```
1704
+
1705
+ The critique a check of a part of `kind` carries, or `undefined` for any other kind and for an extension that cannot be
1706
+ read. Unknown fields and values inside a readable critique are kept.
1707
+
1708
+ #### `critiqueMarkOf`
1709
+
1710
+ ```ts
1711
+ export function critiqueMarkOf(kind: string, check: Pick<Check, 'extensions'>): CritiqueMark | undefined
1712
+ ```
1713
+
1714
+ The mark a check of a part of `kind` carries (a reviewer's accept or dismiss of a critic's opinion, `atlas.critique-mark`), or
1715
+ `undefined` for any other kind and for an extension that cannot be read. Like a critique it is evidence: it decides nothing.
1716
+
1477
1717
  ### Spec outline from a directory
1478
1718
 
1479
1719
  Source: `packages/reports/src/directory-outline.ts`. Import: `@crvouga/atlas/reports/directory-outline`
@@ -2330,6 +2570,10 @@ export type LayerRow = {
2330
2570
  readonly evidence?: EvidenceSummary;
2331
2571
  /** 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. */
2332
2572
  readonly state?: UiStateFact;
2573
+ /** For a check of a `critique` part whose `atlas.critique` extension can be read: the opinion of an automated reviewer (see `critique-row.ts`). An opinion, never a result: it does not decide `status`, a slot or the gate. Absent for every other kind, and for an extension that cannot be read. */
2574
+ readonly critique?: Critique;
2575
+ /** For a check of a `critique` part whose `atlas.critique-mark` extension can be read: a person's accept or dismiss of a critic's opinion (see `critique-row.ts`). Evidence, never a result: it does not decide `status`, a slot or the gate. Absent for every other kind, and for an extension that cannot be read. */
2576
+ readonly critiqueMark?: CritiqueMark;
2333
2577
  /** 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`. */
2334
2578
  readonly recording?: { readonly state: string; readonly reason?: string };
2335
2579
  };
@@ -6992,6 +7236,32 @@ Whether the checks ran on exactly the commit they name. A CI checkout does. A de
6992
7236
  with edits to tracked files does not: its layer is `modified`, shown with a warning and never mistaken
6993
7237
  for the commit's own result. Outside git the origin is `unknown`.
6994
7238
 
7239
+ #### `keyValue`
7240
+
7241
+ ```ts
7242
+ export function keyValue(pair: string, flag: string): [string, string]
7243
+ ```
7244
+
7245
+ Splits `key=value` (the form of `--param` and `--label`), or throws a message naming `flag`.
7246
+
7247
+ #### `parseTime`
7248
+
7249
+ ```ts
7250
+ export function parseTime(text: string | undefined, flag = '--recorded-at'): number | null
7251
+ ```
7252
+
7253
+ An ISO date or epoch milliseconds from a flag, `null` when the flag was not given. Throws a message naming `flag` for anything else.
7254
+
7255
+ #### `executionOf`
7256
+
7257
+ ```ts
7258
+ export function executionOf(rest: readonly string[], env: Env, given?: unknown): LayerRun | undefined
7259
+ ```
7260
+
7261
+ The execution the producer named, from `--run-id`, `--run-title`, `--run-started-at`, `--run-ended-at` and then
7262
+ `ATLAS_RUN_ID` / `ATLAS_RUN_TITLE`, laid over a `run` the checks file gave. `undefined` when nothing names one, so the
7263
+ documented grouping (CI run URL, else branch@sha) applies. A title or time without an id is refused: it would have nothing to belong to.
7264
+
6995
7265
  #### `runReportCommand`
6996
7266
 
6997
7267
  ```ts
@@ -7001,6 +7271,14 @@ export async function runReportCommand(argv: readonly string[]): Promise<number>
7001
7271
  `atlas report …` for a terminal: a mistake in the command or a path is a message on stderr and exit
7002
7272
  code 2, so a CI job can tell a broken invocation (2) from a report that does not pass (1).
7003
7273
 
7274
+ #### `wantsJson`
7275
+
7276
+ ```ts
7277
+ export function wantsJson(argv: readonly string[]): boolean
7278
+ ```
7279
+
7280
+ Does the command ask for a JSON document (`--format json` or `--json`) instead of prose?
7281
+
7004
7282
  #### `reportMain`
7005
7283
 
7006
7284
  ```ts
@@ -7076,6 +7354,7 @@ It reads only immutable layers (`layers/index/…`), which is what makes "read o
7076
7354
  - `snapshot(): EffectiveEvidence` — The current report, ready to send. Built once per change, and only the layers that changed are rebuilt.
7077
7355
  - `json(): { readonly version: string; readonly text: string }` — The report as JSON text: exactly what `JSON.stringify({ ...snapshot(), active: [] })` writes, put together from the text of each layer, which is written once. `active` is what a store without live runs always sends: none.
7078
7356
  - `delta(since: string): { readonly text: string; readonly version: string } | null` — What differs between the report a viewer holds (named by its version) and the current one: the layers that changed in full, and the ids of those that no longer hold a winner. Null when that version is not remembered, and the viewer should ask for the whole report. See `applyEffectiveDelta`.
7357
+ - `changedSince(since: string): ReadonlySet<string> | null` — The ids of the layers that were added or changed (took a slot, or lost one) since the report a reader holds, named by its version: what `delta` sends, without the text. `null` when that version is not remembered, and the reader should take everything. Used to answer "what changed" about a view of the report, such as the entries of the UI catalog.
7079
7358
  - `get history(): TestHistory { return this.#history; }` — Every recorded result of every test and every run, as layers were folded in: what the test catalog, a test's history and the run list are built from. Read-only for callers; `history.rev` changes whenever a layer is taken in.
7080
7359
  - `superseded(): ReadonlySet<string>` — The ids of layers this index has read and that no longer decide any result. Removing one from its store cannot change the report (every slot it filled is held by a newer layer), which is what makes it safe for garbage collection to delete. A layer that failed to load, is hidden by `include`, or still holds a result is not here.
7081
7360
  - `stats()` — For `/readyz` and logs.
@@ -10556,6 +10835,578 @@ export type ReportData = HtmlReportBundle & {
10556
10835
 
10557
10836
  A run's tests (`HtmlReportBundle`) plus the document title shown for them, for the standalone / Solid report viewer.
10558
10837
 
10838
+ ### The `atlas ui-catalog` command
10839
+
10840
+ Source: `packages/reports/src/ui-catalog-cli.ts`. Import: `@crvouga/atlas/reports/ui-catalog-cli`
10841
+
10842
+ `uiCatalogMain` is `atlas ui-catalog plan | publish | status | diff | export`: the UI catalog ({@link ./ui-catalog}) without
10843
+ a script. `plan` and `publish` turn a small JSON file (and the pictures beside it) into the layers of the expected set and of
10844
+ the captures; `status` reads every store and says whether the catalog is complete and free of failures; `diff` sets two
10845
+ commits against each other; `export` writes the catalog as one HTML file that needs no server. Like `atlas report`, it is
10846
+ written so it can be called from code and tests: it takes its arguments, environment, working directory, clock and git as
10847
+ parameters and returns the exit code, every command prints a JSON document with `--format json`, and a command that is wrong
10848
+ throws, which `runUiCatalogCommand` turns into a message (or a JSON error) and exit code 2.
10849
+
10850
+ Nothing here knows how a picture was taken: the files it reads are the same data any collector can write. Publishing a
10851
+ catalog to a hosting platform is not a command: it would need that platform's credentials and API, which Atlas does not
10852
+ hold. The file `export` writes, or a folder of them, is what to hand to whatever hosts static files.
10853
+
10854
+ Exit codes: 0 it worked, 1 it ran and the answer is no (an incomplete catalog, a failed capture, an unreadable store, a
10855
+ difference under `--fail-on-change`), 2 the command itself was wrong.
10856
+
10857
+ See docs/ui-catalog.md
10858
+ See docs/agents.md
10859
+
10860
+ #### `UI_CATALOG_USAGE`
10861
+
10862
+ ```ts
10863
+ export const UI_CATALOG_USAGE = …
10864
+ ```
10865
+
10866
+ The text `atlas ui-catalog --help` prints: every subcommand, its flags, what each does and the exit codes. Also the source of its entry in `docs/reference/cli.md`.
10867
+
10868
+ #### `uiCatalogMain`
10869
+
10870
+ ```ts
10871
+ export async function uiCatalogMain(argv: readonly string[], io: ReportIo = { out: text => process.stdout.write(text) }): Promise<number>
10872
+ ```
10873
+
10874
+ `atlas ui-catalog …`. Returns the exit code. Throws on a mistake in the command (see {@link runUiCatalogCommand}).
10875
+
10876
+ #### `runUiCatalogCommand`
10877
+
10878
+ ```ts
10879
+ export async function runUiCatalogCommand(argv: readonly string[]): Promise<number>
10880
+ ```
10881
+
10882
+ `atlas ui-catalog …` for a terminal: a mistake in the command or a path is a message on stderr and exit code 2, so a CI job can
10883
+ tell a broken invocation (2) from a catalog that needs attention (1). With `--format json` the mistake is a JSON document.
10884
+
10885
+ ### UI catalog comparison
10886
+
10887
+ Source: `packages/reports/src/ui-catalog-diff.ts`. Import: `@crvouga/atlas/reports/ui-catalog-diff`
10888
+
10889
+ What changed in the UI catalog between two commits, or between two profiles, without claiming more than the evidence
10890
+ supports. Pure functions of {@link UiCatalog} documents (and of the layers they came from), safe in a browser; nothing here
10891
+ changes a status, a slot or a winner.
10892
+
10893
+ - **Two commits.** The catalog of a commit is the catalog of the layers recorded at that commit ({@link uiCatalogAtCommit}).
10894
+ A state is paired with itself in the same profile. A layer replaces the older one of its slot, so the effective report no
10895
+ longer holds an earlier commit's captures: compare from the layers themselves ({@link readBackendLayers}), which keep them.
10896
+ - **Two profiles.** Within one catalog, each state's cell in one profile is set beside its cell in another
10897
+ ({@link diffUiProfiles}): desktop against mobile, light against dark.
10898
+ - **Pictures are compared by content.** Two captures have the same picture when their stored bytes have the same SHA-256.
10899
+ A different hash means different bytes, which is not a measure of how different the pixels are. When a side has no
10900
+ content hash (a picture given only by address, or left out), whether it changed is `unverified`, never "unchanged".
10901
+ - **A state only one side has** is `added` or `removed`. It may be new or renamed, or the other side's shard may not have
10902
+ run, and the comparison cannot tell which: it is not called a regression.
10903
+ - **A capture that did not pass is not a picture.** A cell that failed, is being taken or was skipped on a side is a change of
10904
+ `coverage`, never compared by picture.
10905
+
10906
+ See docs/ui-catalog.md
10907
+
10908
+ #### `UI_DIFF_VERSION`
10909
+
10910
+ ```ts
10911
+ export const UI_DIFF_VERSION = …
10912
+ ```
10913
+
10914
+ The `schemaVersion` of the comparison document.
10915
+
10916
+ #### `UiCellChange`
10917
+
10918
+ ```ts
10919
+ export type UiCellChange = 'added' | 'removed' | 'coverage' | 'picture' | 'unverified' | 'unchanged';
10920
+ ```
10921
+
10922
+ How one pair of cells stands. `added`: only the after side has the cell. `removed`: only the before side has it.
10923
+ `coverage`: both have it and it stands differently (captured against failed, for example). `picture`: both captured and
10924
+ the bytes differ. `unverified`: both captured, but a side has no content hash, so whether the picture changed is unknown.
10925
+ `unchanged`: the same coverage and, when captured, the same bytes.
10926
+
10927
+ #### `UI_CELL_CHANGES`
10928
+
10929
+ ```ts
10930
+ export const UI_CELL_CHANGES = …
10931
+ ```
10932
+
10933
+ The values of {@link UiCellChange}, in the order a review reads them.
10934
+
10935
+ #### `UiDiffEntry`
10936
+
10937
+ ```ts
10938
+ export type UiDiffEntry = {
10939
+ /** The state's id. */
10940
+ readonly id: string;
10941
+ /** Its title, from the after side when it has the state. */
10942
+ readonly title: string;
10943
+ /** The profile both sides are in (a comparison of commits), or null when the two sides are different profiles. */
10944
+ readonly profile: UiProfile | null;
10945
+ readonly before: UiCell | null;
10946
+ readonly after: UiCell | null;
10947
+ readonly change: UiCellChange;
10948
+ };
10949
+ ```
10950
+
10951
+ One state in one profile on each side.
10952
+
10953
+ #### `UiCatalogDiff`
10954
+
10955
+ ```ts
10956
+ export type UiCatalogDiff = {
10957
+ readonly schemaVersion: typeof UI_DIFF_VERSION;
10958
+ /** How many entries are in each {@link UiCellChange}. */
10959
+ readonly summary: Readonly<Record<UiCellChange, number>> & { readonly total: number };
10960
+ /** Every pair, ordered by state id and then profile label. */
10961
+ readonly entries: readonly UiDiffEntry[];
10962
+ };
10963
+ ```
10964
+
10965
+ A comparison, as `atlas ui-catalog diff --format json` prints it and the viewer shows it.
10966
+
10967
+ #### `pictureIdentity`
10968
+
10969
+ ```ts
10970
+ export function pictureIdentity(image: UiImage | null | undefined): string | null
10971
+ ```
10972
+
10973
+ What identifies a picture's content: its recorded SHA-256, else the hash in a content-addressed bundle key
10974
+ (`attachments/<sha256>`), else `null`. A picture given only by address, or left out, has no identity here: its content is
10975
+ not Atlas's to vouch for.
10976
+
10977
+ #### `diffUiCatalogs`
10978
+
10979
+ ```ts
10980
+ export function diffUiCatalogs(before: UiCatalog, after: UiCatalog): UiCatalogDiff
10981
+ ```
10982
+
10983
+ Sets the catalog of an earlier commit (`before`) against a later one (`after`): each state in each profile is paired with
10984
+ itself. A cell that is only a plan (`missing`) on one side is a change of coverage like any other. The order of the
10985
+ states in either catalog does not matter.
10986
+
10987
+ #### `diffUiProfiles`
10988
+
10989
+ ```ts
10990
+ export function diffUiProfiles(catalog: UiCatalog, from: string, to: string): UiCatalogDiff
10991
+ ```
10992
+
10993
+ Sets one profile of a catalog (`from`, a {@link UiProfile.key}) against another (`to`): each state's cell in `from` beside its
10994
+ cell in `to`. A state that has a cell in only one of them is `added` or `removed`. Throws when the two are the same profile,
10995
+ because that compares a catalog with itself.
10996
+
10997
+ #### `changedUiEntries`
10998
+
10999
+ ```ts
11000
+ export function changedUiEntries(diff: UiCatalogDiff): readonly UiDiffEntry[]
11001
+ ```
11002
+
11003
+ The entries of a comparison that changed, or all of them. A change the comparison cannot verify is a change worth looking at, so it is kept.
11004
+
11005
+ #### `UiCommit`
11006
+
11007
+ ```ts
11008
+ export type UiCommit = {
11009
+ readonly sha: string;
11010
+ /** The branch of the newest layer recorded at the commit, or null. */
11011
+ readonly branch: string | null;
11012
+ /** When the newest layer at the commit was recorded, epoch milliseconds. */
11013
+ readonly recordedAtMs: number;
11014
+ /** How many layers were recorded at the commit. */
11015
+ readonly layers: number;
11016
+ };
11017
+ ```
11018
+
11019
+ A commit that has layers of the catalog's producers.
11020
+
11021
+ #### `uiCatalogCommits`
11022
+
11023
+ ```ts
11024
+ export function uiCatalogCommits(layers: readonly Pick<LayerEvidence['layer'], 'sha' | 'branch' | 'recordedAtMs' | 'freshness'>[]): UiCommit[]
11025
+ ```
11026
+
11027
+ The commits layers were recorded at, newest first. A layer with no commit is left out: it cannot be compared by commit.
11028
+ Layers recorded from a checkout with uncommitted changes (`freshness` `modified`) are not the commit's own result and are
11029
+ left out too, as the gate does by default for comparisons.
11030
+
11031
+ #### `layersAtCommit`
11032
+
11033
+ ```ts
11034
+ export function layersAtCommit(layers: readonly LayerEvidence[], sha: string): LayerEvidence[]
11035
+ ```
11036
+
11037
+ The layers recorded at one commit (`sha` is the whole commit id, or a prefix of at least 7 hexadecimal digits that names
11038
+ exactly one commit among `layers`). Throws when a prefix names none, or more than one, so a comparison never silently uses
11039
+ the wrong commit.
11040
+
11041
+ #### `uiCatalogAtCommit`
11042
+
11043
+ ```ts
11044
+ export function uiCatalogAtCommit(layers: readonly LayerEvidence[], sha: string, options: UiCatalogOptions = {}): UiCatalog
11045
+ ```
11046
+
11047
+ The UI catalog as it was recorded at one commit: only the layers recorded there take part. A state the commit's layers do
11048
+ not hold is absent, not failed. See {@link layersAtCommit} for how `sha` is matched.
11049
+
11050
+ ### UI catalog entries for critics
11051
+
11052
+ Source: `packages/reports/src/ui-catalog-entries.ts`. Import: `@crvouga/atlas/reports/ui-catalog-entries`
11053
+
11054
+ The UI catalog seen as a flat, orderable list that a separate process can iterate: one **entry** per captured state in
11055
+ one profile that has a stored picture, with the hash of that picture and the critiques already made of it. A critic
11056
+ lists entries a page at a time with a stable cursor, asks only for those that changed since a report version it holds,
11057
+ asks only for those a given critic has not judged at the picture they have now, and fetches the picture itself by the
11058
+ `image.path` (served by the same safe attachment path as every other file). Nothing here is stored: entries are a function of the
11059
+ report, so the same layers in any order give the same list, and a missed event is covered by the next read.
11060
+
11061
+ Critiques are opinions, never results: they ride along as evidence of an entry and change nothing about whether a
11062
+ report passes.
11063
+
11064
+ See docs/critics.md#reading-the-catalog
11065
+ See docs/ui-catalog.md
11066
+
11067
+ #### `CATALOG_ENTRIES_VERSION`
11068
+
11069
+ ```ts
11070
+ export const CATALOG_ENTRIES_VERSION = …
11071
+ ```
11072
+
11073
+ The `schemaVersion` of the entries document.
11074
+
11075
+ #### `UNKNOWN_STATE_KIND`
11076
+
11077
+ ```ts
11078
+ export const UNKNOWN_STATE_KIND = …
11079
+ ```
11080
+
11081
+ The state kind of an entry whose producers declared none. It is unknown, not `screen`.
11082
+
11083
+ #### `EntryImage`
11084
+
11085
+ ```ts
11086
+ export type EntryImage = {
11087
+ /** SHA-256 of the exact stored bytes, lowercase hex. */
11088
+ readonly sha256: string;
11089
+ /** Where the bytes are, relative to the reports API root: `/layers/<layer>/blobs/attachments/<sha256>`. */
11090
+ readonly path: string;
11091
+ readonly contentType?: string;
11092
+ readonly width?: number;
11093
+ readonly height?: number;
11094
+ readonly bytes?: number;
11095
+ };
11096
+ ```
11097
+
11098
+ The picture an entry was captured as: the hash critiques are anchored to, and where its stored bytes are.
11099
+
11100
+ #### `EntryCritique`
11101
+
11102
+ ```ts
11103
+ export type EntryCritique = {
11104
+ readonly layer: string;
11105
+ readonly producer: string;
11106
+ readonly recordedAtMs: number;
11107
+ /** The check's own status: whether the critic ran, never what it thought. */
11108
+ readonly status: EvidenceStatus;
11109
+ /** `current`: it judged this picture. `earlier-image`: the picture changed since, so the entry counts as not yet judged by that critic. */
11110
+ readonly currency: 'current' | 'earlier-image';
11111
+ readonly critique: Critique;
11112
+ };
11113
+ ```
11114
+
11115
+ A critique already made of an entry (any critic), and whether it judged the picture the entry has now.
11116
+
11117
+ #### `EntryMark`
11118
+
11119
+ ```ts
11120
+ export type EntryMark = {
11121
+ readonly layer: string;
11122
+ /** `critic.review.<reviewer>`. */
11123
+ readonly producer: string;
11124
+ readonly recordedAtMs: number;
11125
+ /** `current`: made about this picture. `earlier-image`: the picture changed since, so the mark is shown as about an earlier picture and counts for nothing now. */
11126
+ readonly currency: 'current' | 'earlier-image';
11127
+ readonly mark: CritiqueMark;
11128
+ };
11129
+ ```
11130
+
11131
+ A person's accept or dismiss of a critic's opinion about an entry that wins its slot, and whether it was made about the picture the entry has now.
11132
+
11133
+ #### `CatalogEntry`
11134
+
11135
+ ```ts
11136
+ export type CatalogEntry = {
11137
+ /** Stable identity, unique in the catalog: the state id, then `@` and the profile when it has one. */
11138
+ readonly key: string;
11139
+ readonly stateId: string;
11140
+ readonly title: string;
11141
+ readonly route: string | null;
11142
+ readonly area: string | null;
11143
+ /** The kind of UI state (`screen`, `overlay`, `loading`, `error`, `empty`…), or `unknown`. */
11144
+ readonly state: string;
11145
+ readonly profile: UiProfile;
11146
+ readonly image: EntryImage;
11147
+ /** The layer, backend and provenance of the capture. */
11148
+ readonly origin: UiOrigin;
11149
+ /** Critiques of this entry that win their slot, newest first. */
11150
+ readonly critiques: readonly EntryCritique[];
11151
+ /** Reviewers' marks (accept or dismiss) of those critiques that win their slot, newest first. Evidence: they never decide a result. */
11152
+ readonly marks: readonly EntryMark[];
11153
+ };
11154
+ ```
11155
+
11156
+ One captured state in one profile, ready to be judged.
11157
+
11158
+ #### `CatalogEntrySet`
11159
+
11160
+ ```ts
11161
+ export type CatalogEntrySet = {
11162
+ readonly entries: readonly CatalogEntry[];
11163
+ /** Captured cells without a stored picture of a known hash (given only by address, left out, or not yet archived). They are not entries. */
11164
+ readonly unjudgeable: number;
11165
+ };
11166
+ ```
11167
+
11168
+ The entries of a report, and how many captured cells could not be offered for judging.
11169
+
11170
+ #### `catalogEntryKey`
11171
+
11172
+ ```ts
11173
+ export function catalogEntryKey(stateId: string, profile: Pick<UiProfile, 'environment' | 'parameters'>): string
11174
+ ```
11175
+
11176
+ The key of the entry for a state in a profile. The state id alone when the profile says nothing; else
11177
+ `id@environment;name=value;…` with the parameters sorted and every part percent-encoded, so two profiles never share a key.
11178
+
11179
+ #### `catalogEntries`
11180
+
11181
+ ```ts
11182
+ export function catalogEntries(layers: readonly LayerEvidence[]): CatalogEntrySet
11183
+ ```
11184
+
11185
+ The entries of some layers, in key order. Only a `captured` cell with a picture stored in its layer (`image.path`) and a
11186
+ known hash is an entry. Critiques are matched to entries by `subject.entryKey`, take the winner of each of their slots by
11187
+ the report contract, and are marked `earlier-image` when they judged other bytes.
11188
+
11189
+ #### `isJudgedBy`
11190
+
11191
+ ```ts
11192
+ export function isJudgedBy(entry: Pick<CatalogEntry, 'critiques'>, critic: { readonly name: string; readonly version?: string }): boolean
11193
+ ```
11194
+
11195
+ Whether an entry has a current critique with a verdict from the critic (and, when given, its version). A failed critique is not a judgement: the next run retries it.
11196
+
11197
+ #### `CatalogEntriesQuery`
11198
+
11199
+ ```ts
11200
+ export type CatalogEntriesQuery = {
11201
+ /** Words (all of them) found in the key, title, route or area. */
11202
+ readonly q?: string;
11203
+ readonly area?: readonly string[];
11204
+ /** Profile keys, as `UiProfile.key`. */
11205
+ readonly profile?: readonly string[];
11206
+ /** State kinds: `screen`, `overlay`, `loading`… */
11207
+ readonly state?: readonly string[];
11208
+ readonly route?: readonly string[];
11209
+ /** Only entries with no current critique with a verdict from this critic (`name`, or `name@version`). */
11210
+ readonly unjudgedBy?: { readonly name: string; readonly version?: string };
11211
+ /** Only entries added or changed since this report version. A version the server does not remember is answered with everything. */
11212
+ readonly since?: string;
11213
+ /** Resume after the entry this cursor names. Opaque: take it from the previous page. */
11214
+ readonly cursor?: string;
11215
+ /** Entries per page. Default 100, at most 1000. */
11216
+ readonly limit?: number;
11217
+ };
11218
+ ```
11219
+
11220
+ What a critic narrows the entries by. Every field is optional; nothing asked is everything. Values within a field are alternatives, fields combine with AND.
11221
+
11222
+ #### `MAX_ENTRIES_LIMIT`
11223
+
11224
+ ```ts
11225
+ export const MAX_ENTRIES_LIMIT = …
11226
+ ```
11227
+
11228
+ The most entries one page holds.
11229
+
11230
+ #### `parseCatalogEntriesQuery`
11231
+
11232
+ ```ts
11233
+ export function parseCatalogEntriesQuery(params: URLSearchParams): CatalogEntriesQuery
11234
+ ```
11235
+
11236
+ Reads a query from URL search parameters; the comma-separated lists, `q`, `unjudgedBy=name@version`, `since`, `cursor` and `limit`. A `limit` that is not a whole number is left out.
11237
+
11238
+ #### `catalogEntriesParams`
11239
+
11240
+ ```ts
11241
+ export function catalogEntriesParams(query: CatalogEntriesQuery): URLSearchParams
11242
+ ```
11243
+
11244
+ A query as URL search parameters, the form {@link parseCatalogEntriesQuery} reads back.
11245
+
11246
+ #### `encodeEntryCursor`
11247
+
11248
+ ```ts
11249
+ export function encodeEntryCursor(key: string): string
11250
+ ```
11251
+
11252
+ The opaque cursor after an entry.
11253
+
11254
+ #### `decodeEntryCursor`
11255
+
11256
+ ```ts
11257
+ export function decodeEntryCursor(cursor: string): string | null
11258
+ ```
11259
+
11260
+ The key a cursor names, or `null` when it is not a cursor this reader made.
11261
+
11262
+ #### `CatalogEntryPage`
11263
+
11264
+ ```ts
11265
+ export type CatalogEntryPage = {
11266
+ /** The page, in key order. */
11267
+ readonly entries: readonly CatalogEntry[];
11268
+ /** Entries that match the query (all pages). */
11269
+ readonly total: number;
11270
+ /** The `cursor` of the next page, absent on the last. */
11271
+ readonly nextCursor?: string;
11272
+ };
11273
+ ```
11274
+
11275
+ One page of entries and where to resume.
11276
+
11277
+ #### `pageCatalogEntries`
11278
+
11279
+ ```ts
11280
+ export function pageCatalogEntries(all: readonly CatalogEntry[], query: CatalogEntriesQuery, changed: ReadonlySet<string> | null = null): CatalogEntryPage
11281
+ ```
11282
+
11283
+ One page of the entries that match a query, in key order. The order is by key and the cursor names the last key of a page, so
11284
+ a critic that iterates sees each entry exactly once however the catalog grows meanwhile (a new entry that sorts before the
11285
+ cursor is found by the next `since` read). `changed` is the set of layer ids that changed since `query.since`
11286
+ (`ReportIndex.changedSince`): `null` means the version was not remembered, and then everything matches.
11287
+
11288
+ **Throws** Error when `query.cursor` is not a cursor.
11289
+
11290
+ ### UI catalog single-file export
11291
+
11292
+ Source: `packages/reports/src/ui-catalog-export.ts`. Import: `@crvouga/atlas/reports/ui-catalog-export`
11293
+
11294
+ The UI catalog as one HTML file that needs no server, no network and no build: open it from a disk, attach it to a ticket,
11295
+ host it anywhere that serves a file. Pictures are embedded as `data:` addresses, the manifest is embedded as JSON
11296
+ (`<script type="application/json" id="ui-catalog">`), and a few lines of script filter the states; with scripts off, every
11297
+ state is simply shown. It shows what the viewer's catalog shows, but it does not stream: it is the catalog at the moment it
11298
+ was made, and says when.
11299
+
11300
+ Hosting platforms cap the size of one file, which is the reason a catalog is also a store and a viewer. The export keeps to a
11301
+ budget ({@link gatherUiPictures}): pictures past it are listed as left out with the reason, never silently dropped, and the
11302
+ manifest stays complete.
11303
+
11304
+ Everything in the page is data from a test run. Text is escaped; only PNG, JPEG, GIF and WebP bytes whose first bytes say so
11305
+ are embedded (never SVG, never what a layer merely claims); a page address is linked only when it is `http` or `https`; the
11306
+ page carries a Content-Security-Policy that allows no network request at all.
11307
+
11308
+ See docs/ui-catalog.md
11309
+
11310
+ #### `UiExportPicture`
11311
+
11312
+ ```ts
11313
+ export type UiExportPicture = { readonly contentType: string; readonly data: Uint8Array };
11314
+ ```
11315
+
11316
+ A picture ready to embed.
11317
+
11318
+ #### `UiExportOptions`
11319
+
11320
+ ```ts
11321
+ export type UiExportOptions = {
11322
+ /** The page's title. Default `UI catalog`. */
11323
+ readonly title?: string;
11324
+ /** The pictures to embed, by {@link uiPictureKey}. A cell whose key is not here shows why it has no picture. */
11325
+ readonly pictures?: ReadonlyMap<string, UiExportPicture>;
11326
+ /** Why a picture that could not be embedded is not here, by {@link uiPictureKey}: shown in its place. */
11327
+ readonly left?: ReadonlyMap<string, string>;
11328
+ /** When the export was made, epoch milliseconds. It is stated on the page so nobody mistakes it for a live view. Default: now. */
11329
+ readonly generatedAtMs?: number;
11330
+ };
11331
+ ```
11332
+
11333
+ What {@link uiCatalogHtml} may be given besides the catalog.
11334
+
11335
+ #### `uiPictureKey`
11336
+
11337
+ ```ts
11338
+ export function uiPictureKey(capture: Pick<UiCapture, 'origin' | 'image'> | null | undefined): string | null
11339
+ ```
11340
+
11341
+ The key of a capture's picture in {@link UiExportOptions.pictures}: its layer and its bundle key, or `null` when the picture is not stored in a layer.
11342
+
11343
+ #### `UiPictureWant`
11344
+
11345
+ ```ts
11346
+ export type UiPictureWant = { readonly key: string; readonly layer: string; readonly backend: string; readonly bundleKey: string; readonly bytes?: number };
11347
+ ```
11348
+
11349
+ One picture an export may embed.
11350
+
11351
+ #### `uiPicturesWanted`
11352
+
11353
+ ```ts
11354
+ export function uiPicturesWanted(catalog: UiCatalog): UiPictureWant[]
11355
+ ```
11356
+
11357
+ The pictures of a catalog an export would embed, once each, in the order the page shows them. Only a cell that was captured, or
11358
+ that failed (a picture of what the page showed), and whose picture is stored in a layer, has one.
11359
+
11360
+ #### `sniffImage`
11361
+
11362
+ ```ts
11363
+ export function sniffImage(data: Uint8Array): string | null
11364
+ ```
11365
+
11366
+ The media type a picture's first bytes say it has (`image/png`, `image/jpeg`, `image/gif`, `image/webp`), or `null` for anything else, SVG included.
11367
+
11368
+ #### `UiPicturesGathered`
11369
+
11370
+ ```ts
11371
+ export type UiPicturesGathered = {
11372
+ readonly pictures: ReadonlyMap<string, UiExportPicture>;
11373
+ /** Why each picture that is not in `pictures` is not. */
11374
+ readonly left: ReadonlyMap<string, string>;
11375
+ /** Bytes embedded. */
11376
+ readonly bytes: number;
11377
+ };
11378
+ ```
11379
+
11380
+ What {@link gatherUiPictures} found.
11381
+
11382
+ #### `gatherUiPictures`
11383
+
11384
+ ```ts
11385
+ export async function gatherUiPictures(wanted: readonly UiPictureWant[], read: (want: UiPictureWant) => Promise<Uint8Array | null>, options: { readonly maxBytes: number }): Promise<UiPicturesGathered>
11386
+ ```
11387
+
11388
+ Reads the pictures an export wants through `read` (which returns the stored bytes of a picture, or `null` when the store
11389
+ no longer holds it) and keeps those that fit in `maxBytes` in total. A picture that is not stored, is not a PNG, JPEG, GIF or
11390
+ WebP, cannot be read, or would pass the budget is recorded in `left` with the reason; the pictures are taken in the page's
11391
+ order, so which are left out is deterministic. A `read` that throws is a picture left out, never a failed export.
11392
+
11393
+ #### `uiCatalogHtml`
11394
+
11395
+ ```ts
11396
+ export function uiCatalogHtml(catalog: UiCatalog, options: UiExportOptions = {}): string
11397
+ ```
11398
+
11399
+ The whole catalog as one self-contained HTML document. See the module description for what it contains and how it treats
11400
+ the data in it. Deterministic for the same catalog, pictures and `generatedAtMs`.
11401
+
11402
+ **Example**
11403
+
11404
+ ```ts
11405
+ const wanted = uiPicturesWanted(catalog);
11406
+ const { pictures, left } = await gatherUiPictures(wanted, want => readPicture(want), { maxBytes: 32 * 1024 * 1024 });
11407
+ await writeFile('catalog.html', uiCatalogHtml(catalog, { pictures, left }));
11408
+ ```
11409
+
10559
11410
  ### Writing a UI catalog from any collector
10560
11411
 
10561
11412
  Source: `packages/reports/src/ui-catalog-publish.ts`. Import: `@crvouga/atlas/reports/ui-catalog-publish`
@@ -10810,6 +11661,8 @@ export type UiCatalogState = {
10810
11661
  readonly fixture: string | null;
10811
11662
  readonly cohort: string | null;
10812
11663
  readonly area: string | null;
11664
+ /** The kind of UI state the producers declared (`screen`, `overlay`, `loading`…), or null when none did. */
11665
+ readonly kind: string | null;
10813
11666
  readonly flags: readonly string[];
10814
11667
  /** The surfaces it covers. */
10815
11668
  readonly covers: readonly string[];
@@ -11103,6 +11956,8 @@ export type UiStateFacts = {
11103
11956
  readonly covers?: readonly string[];
11104
11957
  /** A grouping a person navigates by (`checkout`). */
11105
11958
  readonly area?: string;
11959
+ /** The kind of UI state: `screen`, `overlay`, `loading`, `error`, `empty`, or any other word. Open vocabulary. Critics judge a state against others of its kind, so declare it; when it is absent the kind is unknown. */
11960
+ readonly kind?: string;
11106
11961
  /** On a `ui-plan` check only: this entry is a surface of the product's inventory, not a state. Nothing is captured for it. */
11107
11962
  readonly inventory?: boolean;
11108
11963
  };