@crvouga/atlas 1.1.0 → 1.3.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 (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/reports/src/artifact-query.d.ts +123 -0
  3. package/dist/reports/src/artifact-query.js +234 -0
  4. package/dist/reports/src/evidence-detail.d.ts +14 -3
  5. package/dist/reports/src/evidence-detail.js +34 -19
  6. package/dist/reports/src/evidence-scope.d.ts +83 -0
  7. package/dist/reports/src/evidence-scope.js +129 -0
  8. package/dist/reports/src/evidence-types.d.ts +5 -1
  9. package/dist/reports/src/explorer-types.d.ts +1 -1
  10. package/dist/reports/src/layer-merge.js +3 -2
  11. package/dist/reports/src/layer-types.d.ts +1 -1
  12. package/dist/reports/src/publish-checks.js +1 -0
  13. package/dist/reports/src/reporter.d.ts +1 -1
  14. package/dist/reports/src/reports-explorer-route.d.ts +276 -0
  15. package/dist/reports/src/reports-route.d.ts +276 -0
  16. package/dist/reports/src/test-catalog.d.ts +108 -6
  17. package/dist/reports/src/test-catalog.js +155 -8
  18. package/dist/schema/src/api.d.ts +745 -0
  19. package/dist/schema/src/api.js +48 -6
  20. package/dist/schema/src/layer.d.ts +981 -0
  21. package/dist/schema/src/layer.js +43 -4
  22. package/dist/server/src/app.js +16 -5
  23. package/dist/server/src/openapi.js +42 -3
  24. package/docs/extending.md +1 -0
  25. package/docs/reference/http-api.md +86 -2
  26. package/docs/reference/reports.md +550 -14
  27. package/docs/reference/schema.md +55 -0
  28. package/docs/rich-evidence.md +78 -0
  29. package/docs/server.md +4 -1
  30. package/docs/ui.md +1 -1
  31. package/llms-full.txt +170 -4
  32. package/package.json +1 -1
  33. package/packages/schema/schemas/check-detail.schema.json +673 -1
  34. package/packages/schema/schemas/effective-delta.schema.json +206 -0
  35. package/packages/schema/schemas/effective-evidence.schema.json +206 -0
  36. package/packages/schema/schemas/evidence-page.schema.json +103 -0
  37. package/packages/schema/schemas/layer-rows.schema.json +103 -0
  38. package/packages/schema/schemas/layer.schema.json +1598 -62
  39. package/packages/schema/schemas/publish-checks.schema.json +768 -0
  40. package/packages/schema/schemas/report-run.schema.json +131 -0
  41. package/packages/schema/schemas/report-runs.schema.json +221 -0
  42. package/packages/schema/schemas/test-catalog.schema.json +135 -0
  43. package/packages/schema/schemas/test-history.schema.json +216 -0
  44. package/packages/server/openapi.json +4731 -735
  45. package/packages/visualizer/src/evidence/artifacts.tsx +24 -18
  46. package/packages/visualizer/src/evidence/evidence-scope.tsx +106 -0
  47. package/packages/visualizer/src/evidence/format.ts +11 -0
  48. package/packages/visualizer/src/explorer/ArtifactsView.tsx +162 -0
  49. package/packages/visualizer/src/explorer/Explorer.tsx +47 -3
  50. package/packages/visualizer/src/explorer/RunsView.tsx +43 -19
  51. package/packages/visualizer/src/explorer/catalog.tsx +97 -12
  52. package/packages/visualizer/src/explorer/filters.tsx +279 -0
  53. package/packages/visualizer/src/explorer/search.ts +85 -6
  54. package/packages/visualizer/src/explorer/views.ts +75 -0
@@ -4,6 +4,257 @@ Layered evidence for any kind of spec: publishing layers, stores (folder, S3, an
4
4
 
5
5
  > Generated from the doc comments of `packages/reports/src` by `bun run docs`. Do not edit: change the comment next to the code.
6
6
 
7
+ ### Run and artifact queries
8
+
9
+ Source: `packages/reports/src/artifact-query.ts`. Import: `@crvouga/atlas/reports/artifact-query`
10
+
11
+ One shareable query for the runs of a project and for the recorded results that carry evidence (recordings,
12
+ screenshots, traces): by artifact tag, label, media type, feature (domain, spec), environment or device, producer,
13
+ branch, revision, outcome and time. The same rules answer `GET /api/reports/runs`, `GET /api/reports/artifacts`
14
+ and the viewer's Runs and Artifacts views, so a link means the same thing everywhere.
15
+
16
+ The semantics are fixed, not inferred:
17
+
18
+ - Across dimensions the filters combine with AND: `artifactTag=demo&type=video&environment=ios` is a demo
19
+ recording on ios.
20
+ - Within a dimension the values combine with OR: `environment=ios,android` is either.
21
+ - When both `artifactTag` and `type` are asked for, one artifact must be both (a `demo`-tagged video, not a demo-tagged
22
+ screenshot beside an untagged video) wherever the report says which videos carry which tags; otherwise a result with
23
+ an artifact of that type and an artifact with that tag matches.
24
+ - Facet counts are disjunctive: the count beside a value is how many records the list would hold if that value were
25
+ the only choice in its dimension, with every other dimension's filter applied. With a value already chosen, the
26
+ other values show what choosing them in addition would add, and the chosen one's count is the list's.
27
+ - A filter never changes a status. Evidence that is not recorded is unknown, never a match.
28
+
29
+ Lists in the address are comma-separated; a comma or backslash inside a value is written `\,` or `\\`
30
+ ({@link splitList}, {@link joinList}), so a label such as `note=a,b` survives.
31
+
32
+ See docs/server.md#reading-the-report-as-an-agent
33
+
34
+ #### `QueryFacetValue`
35
+
36
+ ```ts
37
+ export type QueryFacetValue = { readonly value: string; readonly count: number };
38
+ ```
39
+
40
+ A value of a dimension and how many records have it.
41
+
42
+ #### `QUERY_DIMENSIONS`
43
+
44
+ ```ts
45
+ export const QUERY_DIMENSIONS = …
46
+ ```
47
+
48
+ The dimensions a query narrows by, with their address parameters.
49
+
50
+ #### `QueryDimension`
51
+
52
+ ```ts
53
+ export type QueryDimension = (typeof QUERY_DIMENSIONS)[number];
54
+ ```
55
+
56
+ One of {@link QUERY_DIMENSIONS}.
57
+
58
+ #### `ArtifactFilter`
59
+
60
+ ```ts
61
+ export type ArtifactFilter = {
62
+ /** Words (all of them) found in the title, test id, spec, domain, branch, labels, tags, environment or producer; or, for a run, its title. */
63
+ readonly q?: string;
64
+ /** Artifact tags (`demo` and `#demo` are the same). */
65
+ readonly artifactTag?: readonly string[];
66
+ /** Also match the tags an artifact inherits from its check and run. Default false: the exact artifact query. */
67
+ readonly inherited?: boolean;
68
+ /** Media types as roles: `video`, `image`, `audio`, `snapshot`, `text`, `file`, or a producer's own role. `video` means a playable recording. */
69
+ readonly type?: readonly string[];
70
+ /** `key=value`, or `key` for any value. */
71
+ readonly label?: readonly string[];
72
+ /** Environments or devices (`ios`, `desktop-web`). */
73
+ readonly environment?: readonly string[];
74
+ readonly producer?: readonly string[];
75
+ readonly branch?: readonly string[];
76
+ /** Revisions: a full commit id, or a prefix of at least seven characters. */
77
+ readonly sha?: readonly string[];
78
+ /** Statuses (`passed`, `failed`, `running`, `not-run`). A run's outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`. */
79
+ readonly outcome?: readonly string[];
80
+ /** The feature domain of a result's spec. Not applicable to runs. */
81
+ readonly domain?: readonly string[];
82
+ /** Spec paths. Not applicable to runs. */
83
+ readonly spec?: readonly string[];
84
+ /** Only records at or after this time (epoch ms): a result's recording time, a run's end. */
85
+ readonly since?: number;
86
+ /** Only records at or before this time (epoch ms). */
87
+ readonly until?: number;
88
+ /** Only records with a playable video recording (the same as `type: ['video']`). */
89
+ readonly video?: boolean;
90
+ };
91
+ ```
92
+
93
+ What a query asks for. Every field is optional; nothing asked is everything. See the module comment for the semantics.
94
+
95
+ #### `FilterName`
96
+
97
+ ```ts
98
+ export type FilterName = QueryDimension | 'q' | 'inherited' | 'since' | 'until' | 'video';
99
+ ```
100
+
101
+ The filters a query used, as they are named in the address and in `filters` of a response.
102
+
103
+ #### `FilterFields`
104
+
105
+ ```ts
106
+ export type FilterFields = {
107
+ /** Lower-cased text a search matches. */
108
+ readonly text: string;
109
+ /** The normalized tags of its artifacts. */
110
+ readonly artifactTags: readonly string[];
111
+ /** The tags its artifacts inherit from checks and runs, and its own. */
112
+ readonly tags: readonly string[];
113
+ /** Distinct media types; `video` only when a recording can be played. */
114
+ readonly types: readonly string[];
115
+ /** Artifacts by type and their own tags. Where the report does not say which artifact carries which tag, every artifact of the record is given the record's artifact tags. */
116
+ readonly media: readonly { readonly type: string; readonly tags: readonly string[] }[];
117
+ /** `key=value`, sorted and distinct. */
118
+ readonly labels: readonly string[];
119
+ readonly environments: readonly string[];
120
+ readonly producers: readonly string[];
121
+ readonly branches: readonly string[];
122
+ readonly shas: readonly string[];
123
+ readonly outcomes: readonly string[];
124
+ /** Absent when the dimension does not apply (runs have no domain). */
125
+ readonly domains?: readonly string[];
126
+ readonly specs?: readonly string[];
127
+ readonly at: number;
128
+ };
129
+ ```
130
+
131
+ The values of a record a query looks at: made by {@link fieldsOfRun} or by the artifact search.
132
+
133
+ #### `QueryFacets`
134
+
135
+ ```ts
136
+ export type QueryFacets = {
137
+ readonly artifactTags: readonly QueryFacetValue[];
138
+ readonly types: readonly QueryFacetValue[];
139
+ /** Values are `key=value`. */
140
+ readonly labels: readonly QueryFacetValue[];
141
+ readonly environments: readonly QueryFacetValue[];
142
+ readonly producers: readonly QueryFacetValue[];
143
+ readonly branches: readonly QueryFacetValue[];
144
+ readonly outcomes: readonly QueryFacetValue[];
145
+ readonly domains?: readonly QueryFacetValue[];
146
+ };
147
+ ```
148
+
149
+ The facets of a query: for each dimension, its values with disjunctive counts (see the module comment).
150
+
151
+ #### `splitList`
152
+
153
+ ```ts
154
+ export function splitList(raw: string): string[]
155
+ ```
156
+
157
+ Splits a comma-separated list from an address: `a,b` is two values; `\,` is a comma and `\\` a backslash inside a
158
+ value. Values are trimmed and empty ones dropped.
159
+
160
+ **Example**
161
+
162
+ ```ts
163
+ splitList('demo,release\\,2'); // ['demo', 'release,2']
164
+ ```
165
+
166
+ #### `joinList`
167
+
168
+ ```ts
169
+ export function joinList(list: readonly string[]): string | undefined
170
+ ```
171
+
172
+ Joins values into one address parameter that {@link splitList} reads back; `undefined` when there are none.
173
+
174
+ #### `parseTime`
175
+
176
+ ```ts
177
+ export function parseTime(raw: string | null | undefined): number | undefined
178
+ ```
179
+
180
+ A time from an address: epoch milliseconds, or a date or date-time Date can read (a date alone is UTC). `undefined` when unreadable.
181
+
182
+ #### `parseArtifactFilter`
183
+
184
+ ```ts
185
+ export function parseArtifactFilter(params: URLSearchParams): ArtifactFilter
186
+ ```
187
+
188
+ Reads a query from URL search parameters: `q`, `video`, `inherited`, `since`, `until` and the repeatable or
189
+ comma-separated {@link QUERY_DIMENSIONS}. Unreadable values are left out, never an error.
190
+
191
+ #### `filterParams`
192
+
193
+ ```ts
194
+ export function filterParams(filter: ArtifactFilter): URLSearchParams
195
+ ```
196
+
197
+ A query as URL search parameters, the form {@link parseArtifactFilter} reads back.
198
+
199
+ #### `usedFilters`
200
+
201
+ ```ts
202
+ export function usedFilters(filter: ArtifactFilter): FilterName[]
203
+ ```
204
+
205
+ Which filters a query uses, by the names an address gives them.
206
+
207
+ #### `isFiltered`
208
+
209
+ ```ts
210
+ export function isFiltered(filter: ArtifactFilter): boolean
211
+ ```
212
+
213
+ Whether a query narrows anything.
214
+
215
+ #### `forRuns`
216
+
217
+ ```ts
218
+ export function forRuns(filter: ArtifactFilter): ArtifactFilter
219
+ ```
220
+
221
+ Every filter in `filter` that may be applied to a record with these fields, as the runs query applies them (no domain or spec).
222
+
223
+ #### `matchesFilter`
224
+
225
+ ```ts
226
+ export function matchesFilter(fields: FilterFields, filter: ArtifactFilter, skip?: QueryDimension): boolean
227
+ ```
228
+
229
+ Whether a record satisfies a query. `skip` leaves one dimension out: what {@link facetsOf} uses to count the values of
230
+ that dimension, and nothing else should need.
231
+
232
+ #### `facetsOf`
233
+
234
+ ```ts
235
+ export function facetsOf(records: readonly FilterFields[], filter: ArtifactFilter): QueryFacets
236
+ ```
237
+
238
+ The facets of a query over every record: for each dimension, the values the records have and how many records
239
+ would match if that value were chosen in its dimension (see the module comment). A value chosen in the query is
240
+ listed even when nothing has it, with a count of 0, so it can be removed.
241
+
242
+ #### `outcomeOfCounts`
243
+
244
+ ```ts
245
+ export function outcomeOfCounts(counts: Readonly<Record<string, number>>): string
246
+ ```
247
+
248
+ The outcome of a run from its results by status: `failed`, else `running`, else `passed`, else `not-run`.
249
+
250
+ #### `labelList`
251
+
252
+ ```ts
253
+ export function labelList(labels: Readonly<Record<string, string>> | undefined): string[]
254
+ ```
255
+
256
+ The `key=value` labels of a record, sorted and distinct.
257
+
7
258
  ### Reviewer tags in a store
8
259
 
9
260
  Source: `packages/reports/src/artifact-tag-store.ts`. Import: `@crvouga/atlas/reports/artifact-tag-store`
@@ -1410,6 +1661,8 @@ export type EvidenceHolder = {
1410
1661
  readonly steps?: readonly Step[];
1411
1662
  readonly attempts?: readonly Attempt[];
1412
1663
  readonly extensions?: Readonly<Record<string, unknown>>;
1664
+ /** What its evidence shows, as its producer declared it (see `EvidenceScope`). */
1665
+ readonly scope?: EvidenceScope;
1413
1666
  };
1414
1667
  ```
1415
1668
 
@@ -1429,7 +1682,17 @@ Several holders (for example a check and its layer) are summed: `attachments` al
1429
1682
  attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
1430
1683
  label the later holder wins. `artifactTags` are the attachments' own normalized tags and are never inherited. `roles` are the attachments' roles (or, without one, what their media type or file
1431
1684
  name implies), sorted and distinct. `videos` lists every attachment whose effective media type is video, with the
1432
- check, retry attempt or step that holds it (see {@link isVideoAttachment}).
1685
+ check, retry attempt or step that holds it (see {@link isVideoAttachment}), each with the scope that applies to it.
1686
+
1687
+ #### `summarizeEvidenceIn`
1688
+
1689
+ ```ts
1690
+ export function summarizeEvidenceIn(inherited: EvidenceScope | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined
1691
+ ```
1692
+
1693
+ Like {@link summarizeEvidence} for the checks of a part: a video that declares no scope, and sits in a holder that
1694
+ declares none, takes the scope `inherited` from its part or layer (the nearest declaration of the two, see
1695
+ `effectiveScope`). Without `inherited` it is the same as `summarizeEvidence`.
1433
1696
 
1434
1697
  #### `summarizeLayerEvidence`
1435
1698
 
@@ -1479,6 +1742,8 @@ export type CheckDetail = {
1479
1742
  readonly check: Check;
1480
1743
  /** What the run as a whole carries: its logs, its links, its labels. */
1481
1744
  readonly run: EvidenceHolder;
1745
+ /** The scope the check inherits: its part's declaration, else its layer's. Absent when neither declared one, which is unknown, not live or physical. */
1746
+ readonly inheritedScope?: EvidenceScope;
1482
1747
  };
1483
1748
  ```
1484
1749
 
@@ -1487,7 +1752,7 @@ All of one result's evidence, read when someone opens it.
1487
1752
  #### `checkDetailOf`
1488
1753
 
1489
1754
  ```ts
1490
- export function checkDetailOf(layer: { readonly id: string; readonly parts: readonly { readonly kind: string; readonly checks: readonly Check[] }[] } & EvidenceHolder, checkId: string, kind?: string): CheckDetail | null
1755
+ export function checkDetailOf(layer: { readonly id: string; readonly parts: readonly { readonly kind: string; readonly checks: readonly Check[]; readonly scope?: EvidenceScope }[] } & EvidenceHolder, checkId: string, kind?: string): CheckDetail | null
1491
1756
  ```
1492
1757
 
1493
1758
  One result's evidence, from a layer: the first check with id `checkId` (in a part of kind `kind`, when given) plus the layer's own evidence. Null when the layer holds no such check.
@@ -1498,7 +1763,7 @@ One result's evidence, from a layer: the first check with id `checkId` (in a par
1498
1763
  export function runEvidence(layer: EvidenceHolder): EvidenceHolder
1499
1764
  ```
1500
1765
 
1501
- What a layer carries as a whole (its tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent.
1766
+ What a layer carries as a whole (its scope, tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent.
1502
1767
 
1503
1768
  #### `bundleKeysOf`
1504
1769
 
@@ -1593,6 +1858,156 @@ the next pass; `refresh` does not throw for those.
1593
1858
 
1594
1859
  - `args.backend`: The backend id stamped on live layers.
1595
1860
 
1861
+ ### Evidence scope
1862
+
1863
+ Source: `packages/reports/src/evidence-scope.ts`. Import: `@crvouga/atlas/reports/evidence-scope`
1864
+
1865
+ Reads what a producer declared a piece of evidence shows (`EvidenceScope`): which declaration applies to an
1866
+ attachment, the short badges a reviewer sees before playing a recording, the facts split into declared and
1867
+ independently verified, and the filter tokens (`device:physical`, `audio:recorded`) a query narrows by. Pure
1868
+ functions, safe in a browser.
1869
+
1870
+ The one rule: what is not declared is unknown. A missing scope, or a missing field, never reads as live, physical,
1871
+ audible or end to end, and a filter for a value never matches evidence that did not say it. A scope describes
1872
+ evidence; it never decides a status, a slot or a winner.
1873
+
1874
+ See docs/rich-evidence.md#evidence-scope
1875
+
1876
+ #### `SCOPE_DIMENSIONS`
1877
+
1878
+ ```ts
1879
+ export const SCOPE_DIMENSIONS = …
1880
+ ```
1881
+
1882
+ The fields of a scope that say something about the evidence and that `verified` may name, in the order they are shown.
1883
+
1884
+ #### `ScopeDimension`
1885
+
1886
+ ```ts
1887
+ export type ScopeDimension = (typeof SCOPE_DIMENSIONS)[number];
1888
+ ```
1889
+
1890
+ One of {@link SCOPE_DIMENSIONS}.
1891
+
1892
+ #### `UNKNOWN_SCOPE`
1893
+
1894
+ ```ts
1895
+ export const UNKNOWN_SCOPE = …
1896
+ ```
1897
+
1898
+ The filter token of a recording that declares no scope at all.
1899
+
1900
+ #### `effectiveScope`
1901
+
1902
+ ```ts
1903
+ export function effectiveScope(...candidates: readonly (EvidenceScope | undefined)[]): EvidenceScope | undefined
1904
+ ```
1905
+
1906
+ The declaration that applies to something: the nearest one wins as a whole, never field by field, so a recording
1907
+ never shows a limitation or a device that was declared for another source. Pass the candidates nearest first
1908
+ (attachment, attempt, check, part, layer); absent ones are skipped. Undefined when none declares a scope.
1909
+
1910
+ **Example**
1911
+
1912
+ ```ts
1913
+ effectiveScope(attachment.scope, attempt.scope, check.scope, layer.scope);
1914
+ ```
1915
+
1916
+ #### `scopeKey`
1917
+
1918
+ ```ts
1919
+ export function scopeKey(scope: EvidenceScope | undefined): string
1920
+ ```
1921
+
1922
+ A stable string that is equal for equal declarations (key order never matters), to tell one scope from another.
1923
+
1924
+ #### `scopeTokens`
1925
+
1926
+ ```ts
1927
+ export function scopeTokens(scope: EvidenceScope | undefined): string[]
1928
+ ```
1929
+
1930
+ The filter tokens one declaration states, normalized (letter case and spaces never matter): `surface:<value>`,
1931
+ `device:<value>`, `data:synthetic` or `data:real`, `dependencies:<value>`, `audio:recorded` or `audio:silent`,
1932
+ `pacing:<value>`. A field that is not declared adds no token. No scope at all is the single token
1933
+ {@link UNKNOWN_SCOPE}, so a filter can ask for the unknown on purpose; a scope that declares nothing readable is
1934
+ also unknown.
1935
+
1936
+ #### `matchesScope`
1937
+
1938
+ ```ts
1939
+ export function matchesScope(tokens: readonly string[], wanted: readonly string[]): boolean
1940
+ ```
1941
+
1942
+ Whether one recording's tokens satisfy a filter: every wanted token must be among them. Tokens compare normalized.
1943
+ An empty filter matches everything. The token {@link UNKNOWN_SCOPE} matches only a recording with no declared
1944
+ fields, so asking for `device:physical` never includes a recording that did not say what it ran on.
1945
+
1946
+ #### `ScopeBasis`
1947
+
1948
+ ```ts
1949
+ export type ScopeBasis = 'verified' | 'declared' | 'undeclared';
1950
+ ```
1951
+
1952
+ How a fact is known: `verified` (independently checked), `declared` (the producer's word) or `undeclared` (unknown).
1953
+
1954
+ #### `ScopeFact`
1955
+
1956
+ ```ts
1957
+ export type ScopeFact = {
1958
+ readonly dimension: ScopeDimension;
1959
+ /** A name for the dimension: `Device`, `Audio`. */
1960
+ readonly label: string;
1961
+ /** The value as words: `Physical device`, `Silent`, `Not declared`. */
1962
+ readonly value: string;
1963
+ readonly basis: ScopeBasis;
1964
+ };
1965
+ ```
1966
+
1967
+ One line of what a scope says: the dimension, a sentence for a person, and how well it is known.
1968
+
1969
+ #### `ScopeBadge`
1970
+
1971
+ ```ts
1972
+ export type ScopeBadge = {
1973
+ readonly dimension: ScopeDimension | 'unknown';
1974
+ readonly text: string;
1975
+ readonly basis: ScopeBasis;
1976
+ };
1977
+ ```
1978
+
1979
+ A short qualification to show beside a recording: `Device: Simulator`, `Scripted dependencies`, `Silent`.
1980
+
1981
+ #### `scopeFacts`
1982
+
1983
+ ```ts
1984
+ export function scopeFacts(scope: EvidenceScope | undefined): ScopeFact[]
1985
+ ```
1986
+
1987
+ Every dimension of a scope with its value and how it is known, in a fixed order, including the ones nobody declared
1988
+ (`Not declared`, basis `undeclared`). A dimension is `verified` only when the scope's `verified` names it and it has
1989
+ a value; everything else a producer wrote is `declared`. With no scope every dimension is undeclared.
1990
+
1991
+ #### `scopeBadges`
1992
+
1993
+ ```ts
1994
+ export function scopeBadges(scope: EvidenceScope | undefined): ScopeBadge[]
1995
+ ```
1996
+
1997
+ The concise badges for a scope: one per declared dimension, in a fixed order (`Surface: Browser`, `Device: Simulator`, `Synthetic
1998
+ data`, `Scripted dependencies`, `Silent`, `Paced`). With no declared dimension the one badge `Scope unknown`, so
1999
+ a reviewer is told nothing was claimed rather than shown nothing.
2000
+
2001
+ #### `scopeCaveats`
2002
+
2003
+ ```ts
2004
+ export function scopeCaveats(scope: EvidenceScope | undefined): string[]
2005
+ ```
2006
+
2007
+ What a reviewer must not read into evidence: a sentence per thing that is not shown. It names the assertions the
2008
+ result stands on when there are some (so activity visible beyond them is not verified), and every kind of reality
2009
+ that was not declared. Empty only for a scope that declares and verifies everything and lists no limitation.
2010
+
1596
2011
  ### Report evidence types
1597
2012
 
1598
2013
  Source: `packages/reports/src/evidence-types.ts`. Import: `@crvouga/atlas/reports/evidence-types`
@@ -1659,10 +2074,14 @@ export type VideoRef = {
1659
2074
  readonly attempt?: number;
1660
2075
  /** The name of the step that holds it, when `owner` is `step`. */
1661
2076
  readonly step?: string;
2077
+ /** The normalized tags of this attachment itself (`demo`), so a query can ask for a tagged video rather than a result that has a video and some tagged file. Absent when it has none (and in documents of older servers). */
2078
+ readonly tags?: readonly string[];
1662
2079
  /** What identifies the bytes (digest, bundle key or address); references with the same one are the same recording. Absent when `available` is false. */
1663
2080
  readonly recording?: string;
1664
2081
  /** False when the reference names no bytes at all (no bundle key, address or inline text), so it can never be played. */
1665
2082
  readonly available: boolean;
2083
+ /** What this recording shows, as declared for it: its own `scope`, else its attempt's, check's, part's or layer's. Absent when none declared one: its scope is unknown, not live, physical or audible. */
2084
+ readonly scope?: EvidenceScope;
1666
2085
  };
1667
2086
  ```
1668
2087
 
@@ -2166,7 +2585,7 @@ A number a check recorded. Exceeding `limit` is shown and never fails the check
2166
2585
  #### `EvidenceExtras`
2167
2586
 
2168
2587
  ```ts
2169
- export type EvidenceExtras = Partial<Pick<import('../../schema/src/layer').Check, 'tags' | 'labels' | 'links' | 'steps' | 'attachments' | 'annotations' | 'attempts' | 'extensions'>>;
2588
+ export type EvidenceExtras = Partial<Pick<import('../../schema/src/layer').Check, 'tags' | 'labels' | 'links' | 'steps' | 'attachments' | 'annotations' | 'attempts' | 'extensions' | 'scope'>>;
2170
2589
  ```
2171
2590
 
2172
2591
  What a check may carry beyond its outcome. All optional and open: see docs/extending.md.
@@ -4080,7 +4499,7 @@ String choices (such as `{ device: 'iphone-15' }`) that make a result its own sl
4080
4499
  #### `LayerExtras`
4081
4500
 
4082
4501
  ```ts
4083
- export type LayerExtras = Partial<Pick<import('../../schema/src/layer').LayerDocument, 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions' | 'recordings'>>;
4502
+ export type LayerExtras = Partial<Pick<import('../../schema/src/layer').LayerDocument, 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions' | 'recordings' | 'scope'>>;
4084
4503
  ```
4085
4504
 
4086
4505
  What a layer may carry as a whole. All optional and open: see docs/extending.md.
@@ -6383,7 +6802,7 @@ See docs/server.md#streaming-a-run-as-it-happens
6383
6802
  #### `ReporterIdentity`
6384
6803
 
6385
6804
  ```ts
6386
- export type ReporterIdentity = Pick<PublishChecksInput, 'producer' | 'kind' | 'title' | 'sha' | 'branch' | 'runUrl' | 'run' | 'environment' | 'parameters' | 'freshness' | 'specs' | 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>;
6805
+ export type ReporterIdentity = Pick<PublishChecksInput, 'producer' | 'kind' | 'title' | 'sha' | 'branch' | 'runUrl' | 'run' | 'environment' | 'parameters' | 'freshness' | 'specs' | 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'scope' | 'extensions'>;
6387
6806
  ```
6388
6807
 
6389
6808
  What every layer of one stream shares: who is reporting, from where. Whatever `publishChecks` takes, except the checks and the time.
@@ -7811,7 +8230,9 @@ runs that produced them and which tests each one ran.
7811
8230
  always the report's winner (`combineEvidence`), and history only describes how it got there.
7812
8231
  - `testCatalog` turns the report's winners (plus a history, when there is one) into a `TestCatalog`: a summary with
7813
8232
  insights (slowest, flaky, failing), facets to filter by, and the tests that match a `TestQuery`.
7814
- - `testHistory`, `runList` and `runDetail` answer for one test, every run, and one run.
8233
+ - `testHistory`, `runList` and `runDetail` answer for one test, every run, and one run. `runList` and `artifactList`
8234
+ narrow by tag, label, media type, environment, branch, outcome and time (see `artifact-query.ts`); `artifactList` finds
8235
+ the recorded results that carry evidence across the whole history, not only the latest winners.
7815
8236
  - `catalogMarkdown`, `historyMarkdown`, `runsMarkdown` and `runMarkdown` say the same in Markdown, for an agent or a
7816
8237
  terminal.
7817
8238
 
@@ -7962,6 +8383,8 @@ export type TestSummary = {
7962
8383
  readonly artifactTags?: readonly string[];
7963
8384
  /** The normalized tags its attachments inherit from their check and run (those of the results that have attachments), apart from `artifactTags`. What an inherited artifact query also matches. */
7964
8385
  readonly inheritedArtifactTags?: readonly string[];
8386
+ /** What each distinct playable recording of its current results declares it shows, as filter tokens (`device:physical`, `audio:silent`; see `scopeTokens`). A recording that declared nothing is `['unknown']`. Absent when it has no playable recording. A query's `scope` must hold for one recording, so evidence from two different devices never combines. */
8387
+ readonly scopeProfiles?: readonly (readonly string[])[];
7965
8388
  readonly labels: Readonly<Record<string, string>>;
7966
8389
  /** What its current results carry: attachments, steps, retries, and the kinds of attachment. */
7967
8390
  readonly evidence: { readonly attachments: number; readonly steps: number; readonly attempts: number; readonly roles: readonly string[] };
@@ -8054,6 +8477,8 @@ export type CatalogFacets = {
8054
8477
  readonly tags: readonly FacetValue[];
8055
8478
  /** The tags of attachments, normalized. Absent in documents of older servers. */
8056
8479
  readonly artifactTags?: readonly FacetValue[];
8480
+ /** The scope tokens of its recordings (`device:simulator`, `audio:recorded`, `unknown`), with how many tests have a recording that states each. Absent in documents of older servers. */
8481
+ readonly scopes?: readonly FacetValue[];
8057
8482
  readonly branches: readonly FacetValue[];
8058
8483
  readonly labels: Readonly<Record<string, readonly FacetValue[]>>;
8059
8484
  };
@@ -8095,6 +8520,8 @@ export type TestQuery = {
8095
8520
  readonly artifactTag?: readonly string[];
8096
8521
  /** With `artifactTag`: also match the tags an attachment inherits from its check and run (a recording without a tag of its own under a run tagged `demo`). Default false: the exact query. */
8097
8522
  readonly inherited?: boolean;
8523
+ /** Only tests with one playable recording that states every one of these (`device:physical`, `audio:recorded`, `data:synthetic`, `dependencies:live`, `surface:browser`, `pacing:paced`; see `scopeTokens`). Evidence that did not declare a value never matches it; `unknown` matches only a recording that declared no scope. */
8524
+ readonly scope?: readonly string[];
8098
8525
  /** Only tests recorded in this run (a key from `runList`). */
8099
8526
  readonly run?: string;
8100
8527
  readonly sort?: TestSort;
@@ -8174,7 +8601,15 @@ export type RunSummary = {
8174
8601
  * and references that cannot be played. Absent when it has none, and in a report that carries no video information.
8175
8602
  */
8176
8603
  readonly videos?: VideoCounts;
8177
- };
8604
+ /** The normalized tags of the attachments of its results and layers, sorted and distinct. Absent when none is tagged. */
8605
+ readonly artifactTags?: readonly string[];
8606
+ /** The tags of its checks and layers, which its attachments inherit (what `inherited=1` also matches), sorted and distinct. Absent when none. */
8607
+ readonly tags?: readonly string[];
8608
+ /** The media types (roles) of its attachments, sorted and distinct; `video` only for a playable recording. Absent when it has no attachment. */
8609
+ readonly types?: readonly string[];
8610
+ /** The `key=value` labels of its results and layers, sorted and distinct. Absent when none. */
8611
+ readonly labels?: readonly string[];
8612
+ …
8178
8613
  ```
8179
8614
 
8180
8615
  A run: the layers that name the same execution (`run.id`), else share a CI run URL, else a branch and commit. When,
@@ -8183,13 +8618,11 @@ where, and how its tests went.
8183
8618
  #### `RunQuery`
8184
8619
 
8185
8620
  ```ts
8186
- export type RunQuery = {
8187
- /** Only runs that have at least one playable video recording. `total` then counts only those. */
8188
- readonly video?: boolean;
8189
- };
8621
+ export type RunQuery = ArtifactFilter;
8190
8622
  ```
8191
8623
 
8192
- What `runList` can be asked.
8624
+ What `runList` can be asked: an {@link ArtifactFilter}, of which a run answers every dimension but `domain` and
8625
+ `spec`. `video` keeps only runs that have at least one playable video recording; `total` then counts only those.
8193
8626
 
8194
8627
  #### `RunList`
8195
8628
 
@@ -8202,6 +8635,10 @@ export type RunList = {
8202
8635
  readonly runs: readonly RunSummary[];
8203
8636
  /** How many of all runs (whatever was asked, before `limit`) have a playable video recording. */
8204
8637
  readonly withVideo?: number;
8638
+ /** The filters of the query that were applied. A viewer that asked for another knows the server ignored it. */
8639
+ readonly filters?: readonly FilterName[];
8640
+ /** The values each filter dimension offers with counts over every run (see `facetsOf`). */
8641
+ readonly facets?: QueryFacets;
8205
8642
  };
8206
8643
  ```
8207
8644
 
@@ -8242,6 +8679,70 @@ export type RunDetail = {
8242
8679
 
8243
8680
  `GET /api/reports/runs/detail`: one run and every result it recorded.
8244
8681
 
8682
+ #### `ArtifactResult`
8683
+
8684
+ ```ts
8685
+ export type ArtifactResult = {
8686
+ /** The test's key, as in `TestSummary.key`. */
8687
+ readonly key: string;
8688
+ readonly id: string;
8689
+ readonly title: string;
8690
+ readonly specPath: string | null;
8691
+ /** The domain of its spec, or null when it has none. */
8692
+ readonly domain: string | null;
8693
+ /** When it was recorded, epoch ms. */
8694
+ readonly at: number;
8695
+ readonly status: EvidenceStatus;
8696
+ readonly environment: string | null;
8697
+ readonly producer: string;
8698
+ /** The kind of the part that held it. */
8699
+ readonly kind: string;
8700
+ readonly parameters: Readonly<Record<string, string>>;
8701
+ readonly branch: string | null;
8702
+ readonly sha: string | null;
8703
+ /** The key of its run (see `runKey`). */
8704
+ readonly run: string;
8705
+ readonly runUrl: string | null;
8706
+ /** The id of the layer that recorded it: with `key`, the exact result to open. */
8707
+ readonly layer: string;
8708
+ readonly backend: string;
8709
+ /** Attachments on the result, including those on its steps and attempts. */
8710
+ readonly attachments: number;
8711
+ /** The normalized tags of its attachments. */
8712
+ readonly artifactTags: readonly string[];
8713
+ /** The tags of its check and run, which its attachments inherit. */
8714
+ readonly tags: readonly string[];
8715
+ /** The media types (roles) of its attachments; `video` only for a playable recording. */
8716
+ readonly types: readonly string[];
8717
+ readonly labels: Readonly<Record<string, string>>;
8718
+ readonly videos?: readonly VideoRef[];
8719
+ };
8720
+ ```
8721
+
8722
+ One recorded result that carries evidence (an attachment: a recording, a screenshot, a trace), with where and when it
8723
+ was recorded: the unit `artifactList` finds. It is a result in the history, not only the report's current winner, so
8724
+ a recording tagged `demo` is found with its original run, revision and status even when a later attempt is untagged.
8725
+
8726
+ #### `ArtifactList`
8727
+
8728
+ ```ts
8729
+ export type ArtifactList = {
8730
+ readonly schemaVersion: typeof TEST_CATALOG_VERSION;
8731
+ readonly version: string;
8732
+ /** Results that matched, before `offset` and `limit`. */
8733
+ readonly total: number;
8734
+ readonly offset: number;
8735
+ readonly limit: number;
8736
+ readonly results: readonly ArtifactResult[];
8737
+ /** The filters of the query that were applied. */
8738
+ readonly filters: readonly FilterName[];
8739
+ /** The values each dimension offers with disjunctive counts over every result that carries evidence. */
8740
+ readonly facets: QueryFacets;
8741
+ };
8742
+ ```
8743
+
8744
+ `GET /api/reports/artifacts`: the recorded results that carry evidence and match a query, newest first.
8745
+
8245
8746
  #### `runKey`
8246
8747
 
8247
8748
  ```ts
@@ -8279,6 +8780,7 @@ const catalog = testCatalog(catalogModel(layers, { history }));
8279
8780
  - `get runCount(): number { return this.#runs.size; }` — How many runs it knows of.
8280
8781
  - `runs(): RunSummary[]` — Every run, newest first.
8281
8782
  - `run(key: string): { readonly run: RunSummary; readonly results: RunResult[] } | null` — One run and its results (failed first, then by spec and title), or null.
8783
+ - `evidenceResults(): readonly { readonly result: ArtifactResult; readonly fields: FilterFields }[]` — Every recorded result that carries an attachment, newest first, with the values a query looks at. Built once per layer added. The domain is left to the caller (`artifactList`), which may know more specs than the layers recorded.
8282
8784
  - `testsIn(key: string): ReadonlySet<string>` — The keys of the tests one run recorded.
8283
8785
 
8284
8786
  #### `durationStats`
@@ -8392,6 +8894,32 @@ Every run the history knows of, newest first, `limit` at most (default 100). Wit
8392
8894
  have a playable video recording: the filter and `total` apply to every run before `limit`, and nothing is read from
8393
8895
  any recording's bytes. `withVideo` always counts every run that has one.
8394
8896
 
8897
+ #### `fieldsOfRun`
8898
+
8899
+ ```ts
8900
+ export function fieldsOfRun(run: RunSummary): FilterFields
8901
+ ```
8902
+
8903
+ What a query looks at in a run: see {@link ArtifactFilter}. A run's outcome is `failed` when any result failed.
8904
+
8905
+ #### `artifactList`
8906
+
8907
+ ```ts
8908
+ export function artifactList(history: TestHistory, version: string, query: ArtifactFilter = {}, page: { readonly limit?: number; readonly offset?: number; readonly domainOf?: (specPath: string) => string | undefined } = {}): ArtifactList
8909
+ ```
8910
+
8911
+ The recorded results that carry evidence and match a query, newest first, across the whole history (`limit` default
8912
+ 100). `total` and the facet counts apply to every match before `offset` and `limit`. `domainOf` tells the domain of a
8913
+ spec path when the caller knows more than the layers recorded (a server with a spec set); otherwise it is the
8914
+ recorded spec's domain, else the folder the path implies.
8915
+
8916
+ **Example**
8917
+
8918
+ ```ts
8919
+ const list = artifactList(history, 'v1', { artifactTag: ['demo'], type: ['video'], environment: ['ios'] });
8920
+ list.results.map(r => [r.title, r.run, r.sha, r.status]);
8921
+ ```
8922
+
8395
8923
  #### `runDetail`
8396
8924
 
8397
8925
  ```ts
@@ -8406,7 +8934,7 @@ One run with every result it recorded, or null when no run has that key.
8406
8934
  export function parseTestQuery(params: URLSearchParams): TestQuery
8407
8935
  ```
8408
8936
 
8409
- Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`, `artifactTag`, and `inherited=1`.
8937
+ Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`, `artifactTag`, `scope` (every token must hold for one recording), and `inherited=1`.
8410
8938
 
8411
8939
  #### `formatMs`
8412
8940
 
@@ -8440,6 +8968,14 @@ export function runsMarkdown(list: RunList): string
8440
8968
 
8441
8969
  The run list as Markdown.
8442
8970
 
8971
+ #### `artifactsMarkdown`
8972
+
8973
+ ```ts
8974
+ export function artifactsMarkdown(list: ArtifactList): string
8975
+ ```
8976
+
8977
+ The artifact results as Markdown, for an agent or a terminal.
8978
+
8443
8979
  #### `runMarkdown`
8444
8980
 
8445
8981
  ```ts