@crvouga/atlas 1.2.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 (43) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/reports/src/evidence-detail.d.ts +14 -3
  3. package/dist/reports/src/evidence-detail.js +33 -19
  4. package/dist/reports/src/evidence-scope.d.ts +83 -0
  5. package/dist/reports/src/evidence-scope.js +129 -0
  6. package/dist/reports/src/evidence-types.d.ts +3 -1
  7. package/dist/reports/src/explorer-types.d.ts +1 -1
  8. package/dist/reports/src/layer-merge.js +3 -2
  9. package/dist/reports/src/layer-types.d.ts +1 -1
  10. package/dist/reports/src/publish-checks.js +1 -0
  11. package/dist/reports/src/reporter.d.ts +1 -1
  12. package/dist/reports/src/reports-explorer-route.d.ts +276 -0
  13. package/dist/reports/src/reports-route.d.ts +276 -0
  14. package/dist/reports/src/test-catalog.d.ts +7 -1
  15. package/dist/reports/src/test-catalog.js +15 -3
  16. package/dist/schema/src/api.d.ts +520 -0
  17. package/dist/schema/src/api.js +7 -4
  18. package/dist/schema/src/layer.d.ts +981 -0
  19. package/dist/schema/src/layer.js +43 -4
  20. package/dist/server/src/openapi.js +1 -0
  21. package/docs/extending.md +1 -0
  22. package/docs/reference/http-api.md +2 -0
  23. package/docs/reference/reports.md +179 -7
  24. package/docs/reference/schema.md +23 -0
  25. package/docs/rich-evidence.md +47 -0
  26. package/llms-full.txt +50 -0
  27. package/package.json +1 -1
  28. package/packages/schema/schemas/check-detail.schema.json +673 -1
  29. package/packages/schema/schemas/effective-delta.schema.json +192 -0
  30. package/packages/schema/schemas/effective-evidence.schema.json +192 -0
  31. package/packages/schema/schemas/evidence-page.schema.json +96 -0
  32. package/packages/schema/schemas/layer-rows.schema.json +96 -0
  33. package/packages/schema/schemas/layer.schema.json +1598 -62
  34. package/packages/schema/schemas/publish-checks.schema.json +768 -0
  35. package/packages/schema/schemas/report-run.schema.json +96 -0
  36. package/packages/schema/schemas/test-catalog.schema.json +128 -0
  37. package/packages/schema/schemas/test-history.schema.json +202 -0
  38. package/packages/server/openapi.json +2581 -23
  39. package/packages/visualizer/src/evidence/artifacts.tsx +24 -18
  40. package/packages/visualizer/src/evidence/evidence-scope.tsx +106 -0
  41. package/packages/visualizer/src/evidence/format.ts +11 -0
  42. package/packages/visualizer/src/explorer/Explorer.tsx +19 -0
  43. package/packages/visualizer/src/explorer/search.ts +5 -1
@@ -88,6 +88,31 @@ export const MomentSchema = z.looseObject({
88
88
  step: z.string().optional().describe('The `id` of a step'),
89
89
  offsetMs: z.number().nonnegative().optional()
90
90
  });
91
+ /**
92
+ * What a piece of evidence does and does not show, declared by the producer that made it: where it ran, whether its
93
+ * data and dependencies were real, whether audio was captured, how it was paced and which assertions the result
94
+ * stands on. Every field is optional and a field that is absent is unknown, never assumed live, physical or end to end.
95
+ *
96
+ * It is a declaration, not a finding: Atlas shows it as the producer's word, apart from the dimensions the producer
97
+ * names in `verified` (facts something independent checked). It describes evidence and never decides a status, a slot
98
+ * or a winner. The nearest declaration wins as a whole, never field by field: an attachment's `scope`, else its
99
+ * attempt's, else its check's, else its part's, else its layer's.
100
+ */
101
+ export const EvidenceScopeSchema = z.looseObject({
102
+ surface: z.string().min(1).optional().describe('Where the product ran, in an open vocabulary: `browser`, `native-app`, `api`, `cli`…'),
103
+ deviceKind: z.string().min(1).optional().describe('What it ran on, in an open vocabulary: `simulator`, `emulator`, `physical`, `virtual-machine`, `desktop`… A simulator and a physical phone stay distinct values; they are filtered separately'),
104
+ syntheticData: z.boolean().optional().describe('True: the data was made up for the run. False: real data. Absent: unknown'),
105
+ dependencies: z.string().min(1).optional().describe('Whether what the product talks to was `scripted` (replies fixed in advance), `live` or `mixed`. An open vocabulary; absent is unknown'),
106
+ audioCaptured: z.boolean().optional().describe('True: the recording has real audio. False: it is silent. Absent: unknown'),
107
+ pacing: z.string().min(1).optional().describe('How time ran, in an open vocabulary: `paced` (slowed or scripted for a viewer), `real-time`. Absent: unknown'),
108
+ asserted: z.array(z.string()).optional().describe('The boundaries the result actually asserts. Activity visible in a recording beyond them is not verified behavior'),
109
+ limitations: z.array(z.string()).optional().describe('Known limits of what this evidence shows, in the producer\'s words'),
110
+ declaredBy: z.string().optional().describe('Who or what made this declaration: a tool, a team, a person'),
111
+ verified: z.array(z.string()).optional().describe('The names of the fields above (`surface`, `deviceKind`, `syntheticData`, `dependencies`, `audioCaptured`, `pacing`) that something independent of the producer checked. Every other field is only declared'),
112
+ verifiedBy: z.string().optional().describe('What checked the `verified` fields'),
113
+ provenance: z.array(LinkSchema).optional().describe('Where the declaration comes from and how to check it: a document, a CI step, a device log'),
114
+ extensions: ExtensionsSchema.optional()
115
+ });
91
116
  /**
92
117
  * A file or a value that goes with a result: a screenshot, a video, a trace, a log, a DOM or accessibility
93
118
  * snapshot, a HAR, a coverage report, a diff, anything. `contentType` says how to read it; `role` says what
@@ -110,6 +135,7 @@ export const AttachmentSchema = z.looseObject({
110
135
  durationMs: z.number().nonnegative().optional().describe('For video and audio'),
111
136
  description: z.string().optional(),
112
137
  omitted: z.string().optional().describe('Why the producer did not include its bytes, in an open vocabulary: too-large, not-found, not-allowed, over-budget, policy… Present on a metadata-only attachment, so a reader can tell "left out" from "never recorded"'),
138
+ scope: EvidenceScopeSchema.optional().describe('What this one attachment (usually a recording) does and does not show. Overrides the scope of its attempt, check, part and layer as a whole'),
113
139
  tags: z.array(z.string()).optional().describe('Free labels of this one attachment, such as `demo`: shown as `#demo`, searchable, and normalized by `normalizeTag` (a leading `#` and letter case never matter). Unlike the tags of a check they say nothing about its result'),
114
140
  at: MomentSchema.optional(),
115
141
  poster: z.string().optional().describe('The `id` of an attachment that stands for this one before it is opened'),
@@ -190,6 +216,7 @@ export const AttemptSchema = z.looseObject({
190
216
  durationMs: z.number().nonnegative().optional(),
191
217
  startedAtMs: z.number().nonnegative().optional(),
192
218
  detail: z.string().optional(),
219
+ scope: EvidenceScopeSchema.optional().describe('What this try\'s evidence shows, when it differs from the other tries (a retry on another device). Overrides the check\'s scope for this try\'s attachments'),
193
220
  steps: z.array(StepSchema).optional(),
194
221
  attachments: z.array(AttachmentSchema).optional(),
195
222
  annotations: z.array(AnnotationSchema).optional(),
@@ -202,6 +229,7 @@ const evidenceShape = {
202
229
  links: z.array(LinkSchema).optional(),
203
230
  attachments: z.array(AttachmentSchema).optional(),
204
231
  annotations: z.array(AnnotationSchema).optional(),
232
+ scope: EvidenceScopeSchema.optional().describe('What the evidence below this level does and does not show: where it ran, how real its data and dependencies were, whether audio was captured, and what is asserted. Absent: unknown, never assumed live, physical or end to end'),
205
233
  extensions: ExtensionsSchema.optional()
206
234
  };
207
235
  /**
@@ -329,17 +357,27 @@ function lenientList(value, schema, prepare = (item) => item) {
329
357
  });
330
358
  }
331
359
  const object = (value) => (typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null);
332
- /** An attachment whose `tags` cannot be read keeps its file: only the tags that are strings are kept. */
360
+ /** A scope that cannot be read is no scope: the evidence is kept and its scope is unknown. */
361
+ function lenientScope(holder) {
362
+ if (!('scope' in holder))
363
+ return holder;
364
+ const { scope, ...rest } = holder;
365
+ const parsed = EvidenceScopeSchema.safeParse(scope);
366
+ return parsed.success ? { ...rest, scope: parsed.data } : rest;
367
+ }
368
+ /** An attachment whose `tags` or `scope` cannot be read keeps its file: only the tags that are strings are kept, and an unreadable scope is dropped. */
333
369
  function lenientAttachment(item) {
334
370
  const attachment = object(item);
335
- if (!attachment || !('tags' in attachment))
371
+ if (!attachment)
336
372
  return item;
337
- const { tags, ...rest } = attachment;
373
+ const { tags, ...rest } = lenientScope(attachment);
374
+ if (!('tags' in attachment))
375
+ return rest;
338
376
  const kept = lenientList(tags, z.string());
339
377
  return kept ? { ...rest, tags: kept } : rest;
340
378
  }
341
379
  function lenientEvidence(holder, nested = []) {
342
- const out = { ...holder };
380
+ const out = lenientScope(holder);
343
381
  const set = (key, value) => { if (value === undefined)
344
382
  delete out[key];
345
383
  else
@@ -468,6 +506,7 @@ export const PublishChecksSchema = z.looseObject({
468
506
  links: evidenceShape.links,
469
507
  attachments: z.array(AttachmentInputSchema).optional().describe('Attached to the whole layer: a CI log, a coverage report, a trace archive'),
470
508
  annotations: evidenceShape.annotations,
509
+ scope: evidenceShape.scope,
471
510
  extensions: evidenceShape.extensions
472
511
  });
473
512
  const CountsSchema = z.object({
@@ -293,6 +293,7 @@ const BASE_OPERATIONS = [
293
293
  { name: 'tag', in: 'query', description: 'Only tests with this tag. Repeatable or comma-separated.', schema: str },
294
294
  { name: 'artifactTag', in: 'query', description: 'Only tests with an attachment tagged this (`demo` and `#demo` are the same tag), not counting tags inherited from a check or run: the exact artifact query. `inherited=1` widens it to the tags inherited from the check and run. Repeatable or comma-separated.', schema: str, example: 'demo' },
295
295
  { name: 'inherited', in: 'query', description: 'With `artifactTag`: `1` also matches the tags an attachment inherits from its check and run, so a recording without a tag of its own under a run tagged `demo` is found. Without it the query is exact.', schema: { type: 'string', enum: ['1', 'true'] } },
296
+ { name: 'scope', in: 'query', description: 'Only tests with one playable recording that states every one of these producer-declared qualifications: `surface:<value>`, `device:<value>` (`device:physical` and `device:simulator` stay apart), `data:synthetic` or `data:real`, `dependencies:<value>`, `audio:recorded` or `audio:silent`, `pacing:<value>`. All must hold for the same recording. A recording that did not declare a value never matches it, and `unknown` matches only a recording that declared no scope. Repeatable or comma-separated.', schema: str, example: 'device:physical,audio:recorded' },
296
297
  { name: 'run', in: 'query', description: 'Only tests recorded in this run (a `key` from `/api/reports/runs`).', schema: str },
297
298
  { name: 'sort', in: 'query', description: 'The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.', schema: { type: 'string', enum: ['spec', 'title', 'recent', 'slowest', 'failures', 'flaky', 'runs'] } },
298
299
  { name: 'offset', in: 'query', description: 'Skip this many matching tests.', schema: { type: 'integer', minimum: 0 } },
package/docs/extending.md CHANGED
@@ -31,6 +31,7 @@ carry any of these. Every one is optional.
31
31
  | `attempts[]` | Every try, when the check was retried. |
32
32
  | `tags[]` | Free labels to filter by: `smoke`, `payments`. |
33
33
  | `labels{}` | Facts about the result that are not its identity: `owner`, `severity`, `browser`, `build`. |
34
+ | `scope{}` | What the evidence does and does not show: surface, device kind, synthetic data, scripted dependencies, audio, pacing, what was asserted and its limits. Unknown when absent. See [Evidence scope](rich-evidence.md#evidence-scope). |
34
35
  | `links[]` | Where to read more: the CI run, the issue, the trace client. |
35
36
  | `extensions{}` | Structured data of your own or a tool's, under a name you own. |
36
37
  | `measurements[]` | Numbers, with units and an allowance (already part of the contract). |
@@ -497,6 +497,7 @@ Send `format=markdown` (or `Accept: text/markdown`) for the same as a Markdown r
497
497
  - `tag` (query): Only tests with this tag. Repeatable or comma-separated.
498
498
  - `artifactTag` (query): Only tests with an attachment tagged this (`demo` and `#demo` are the same tag), not counting tags inherited from a check or run: the exact artifact query. `inherited=1` widens it to the tags inherited from the check and run. Repeatable or comma-separated.
499
499
  - `inherited` (query): With `artifactTag`: `1` also matches the tags an attachment inherits from its check and run, so a recording without a tag of its own under a run tagged `demo` is found. Without it the query is exact.
500
+ - `scope` (query): Only tests with one playable recording that states every one of these producer-declared qualifications: `surface:<value>`, `device:<value>` (`device:physical` and `device:simulator` stay apart), `data:synthetic` or `data:real`, `dependencies:<value>`, `audio:recorded` or `audio:silent`, `pacing:<value>`. All must hold for the same recording. A recording that did not declare a value never matches it, and `unknown` matches only a recording that declared no scope. Repeatable or comma-separated.
500
501
  - `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
501
502
  - `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
502
503
  - `offset` (query): Skip this many matching tests.
@@ -528,6 +529,7 @@ Scoped to one project. The same project may be selected on the unprefixed API us
528
529
  - `tag` (query): Only tests with this tag. Repeatable or comma-separated.
529
530
  - `artifactTag` (query): Only tests with an attachment tagged this (`demo` and `#demo` are the same tag), not counting tags inherited from a check or run: the exact artifact query. `inherited=1` widens it to the tags inherited from the check and run. Repeatable or comma-separated.
530
531
  - `inherited` (query): With `artifactTag`: `1` also matches the tags an attachment inherits from its check and run, so a recording without a tag of its own under a run tagged `demo` is found. Without it the query is exact.
532
+ - `scope` (query): Only tests with one playable recording that states every one of these producer-declared qualifications: `surface:<value>`, `device:<value>` (`device:physical` and `device:simulator` stay apart), `data:synthetic` or `data:real`, `dependencies:<value>`, `audio:recorded` or `audio:silent`, `pacing:<value>`. All must hold for the same recording. A recording that did not declare a value never matches it, and `unknown` matches only a recording that declared no scope. Repeatable or comma-separated.
531
533
  - `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
532
534
  - `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
533
535
  - `offset` (query): Skip this many matching tests.
@@ -1661,6 +1661,8 @@ export type EvidenceHolder = {
1661
1661
  readonly steps?: readonly Step[];
1662
1662
  readonly attempts?: readonly Attempt[];
1663
1663
  readonly extensions?: Readonly<Record<string, unknown>>;
1664
+ /** What its evidence shows, as its producer declared it (see `EvidenceScope`). */
1665
+ readonly scope?: EvidenceScope;
1664
1666
  };
1665
1667
  ```
1666
1668
 
@@ -1680,7 +1682,17 @@ Several holders (for example a check and its layer) are summed: `attachments` al
1680
1682
  attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
1681
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
1682
1684
  name implies), sorted and distinct. `videos` lists every attachment whose effective media type is video, with the
1683
- 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`.
1684
1696
 
1685
1697
  #### `summarizeLayerEvidence`
1686
1698
 
@@ -1730,6 +1742,8 @@ export type CheckDetail = {
1730
1742
  readonly check: Check;
1731
1743
  /** What the run as a whole carries: its logs, its links, its labels. */
1732
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;
1733
1747
  };
1734
1748
  ```
1735
1749
 
@@ -1738,7 +1752,7 @@ All of one result's evidence, read when someone opens it.
1738
1752
  #### `checkDetailOf`
1739
1753
 
1740
1754
  ```ts
1741
- 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
1742
1756
  ```
1743
1757
 
1744
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.
@@ -1749,7 +1763,7 @@ One result's evidence, from a layer: the first check with id `checkId` (in a par
1749
1763
  export function runEvidence(layer: EvidenceHolder): EvidenceHolder
1750
1764
  ```
1751
1765
 
1752
- 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.
1753
1767
 
1754
1768
  #### `bundleKeysOf`
1755
1769
 
@@ -1844,6 +1858,156 @@ the next pass; `refresh` does not throw for those.
1844
1858
 
1845
1859
  - `args.backend`: The backend id stamped on live layers.
1846
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
+
1847
2011
  ### Report evidence types
1848
2012
 
1849
2013
  Source: `packages/reports/src/evidence-types.ts`. Import: `@crvouga/atlas/reports/evidence-types`
@@ -1916,6 +2080,8 @@ export type VideoRef = {
1916
2080
  readonly recording?: string;
1917
2081
  /** False when the reference names no bytes at all (no bundle key, address or inline text), so it can never be played. */
1918
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;
1919
2085
  };
1920
2086
  ```
1921
2087
 
@@ -2419,7 +2585,7 @@ A number a check recorded. Exceeding `limit` is shown and never fails the check
2419
2585
  #### `EvidenceExtras`
2420
2586
 
2421
2587
  ```ts
2422
- 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'>>;
2423
2589
  ```
2424
2590
 
2425
2591
  What a check may carry beyond its outcome. All optional and open: see docs/extending.md.
@@ -4333,7 +4499,7 @@ String choices (such as `{ device: 'iphone-15' }`) that make a result its own sl
4333
4499
  #### `LayerExtras`
4334
4500
 
4335
4501
  ```ts
4336
- 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'>>;
4337
4503
  ```
4338
4504
 
4339
4505
  What a layer may carry as a whole. All optional and open: see docs/extending.md.
@@ -6636,7 +6802,7 @@ See docs/server.md#streaming-a-run-as-it-happens
6636
6802
  #### `ReporterIdentity`
6637
6803
 
6638
6804
  ```ts
6639
- 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'>;
6640
6806
  ```
6641
6807
 
6642
6808
  What every layer of one stream shares: who is reporting, from where. Whatever `publishChecks` takes, except the checks and the time.
@@ -8217,6 +8383,8 @@ export type TestSummary = {
8217
8383
  readonly artifactTags?: readonly string[];
8218
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. */
8219
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[])[];
8220
8388
  readonly labels: Readonly<Record<string, string>>;
8221
8389
  /** What its current results carry: attachments, steps, retries, and the kinds of attachment. */
8222
8390
  readonly evidence: { readonly attachments: number; readonly steps: number; readonly attempts: number; readonly roles: readonly string[] };
@@ -8309,6 +8477,8 @@ export type CatalogFacets = {
8309
8477
  readonly tags: readonly FacetValue[];
8310
8478
  /** The tags of attachments, normalized. Absent in documents of older servers. */
8311
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[];
8312
8482
  readonly branches: readonly FacetValue[];
8313
8483
  readonly labels: Readonly<Record<string, readonly FacetValue[]>>;
8314
8484
  };
@@ -8350,6 +8520,8 @@ export type TestQuery = {
8350
8520
  readonly artifactTag?: readonly string[];
8351
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. */
8352
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[];
8353
8525
  /** Only tests recorded in this run (a key from `runList`). */
8354
8526
  readonly run?: string;
8355
8527
  readonly sort?: TestSort;
@@ -8762,7 +8934,7 @@ One run with every result it recorded, or null when no run has that key.
8762
8934
  export function parseTestQuery(params: URLSearchParams): TestQuery
8763
8935
  ```
8764
8936
 
8765
- 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`.
8766
8938
 
8767
8939
  #### `formatMs`
8768
8940
 
@@ -776,6 +776,29 @@ export const MomentSchema = …
776
776
 
777
777
  Where in a check something happened: at a step, and/or this long after the check started.
778
778
 
779
+ #### `EvidenceScopeSchema`
780
+
781
+ ```ts
782
+ export const EvidenceScopeSchema = …
783
+ ```
784
+
785
+ What a piece of evidence does and does not show, declared by the producer that made it: where it ran, whether its
786
+ data and dependencies were real, whether audio was captured, how it was paced and which assertions the result
787
+ stands on. Every field is optional and a field that is absent is unknown, never assumed live, physical or end to end.
788
+
789
+ It is a declaration, not a finding: Atlas shows it as the producer's word, apart from the dimensions the producer
790
+ names in `verified` (facts something independent checked). It describes evidence and never decides a status, a slot
791
+ or a winner. The nearest declaration wins as a whole, never field by field: an attachment's `scope`, else its
792
+ attempt's, else its check's, else its part's, else its layer's.
793
+
794
+ #### `EvidenceScope`
795
+
796
+ ```ts
797
+ export type EvidenceScope = z.output<typeof EvidenceScopeSchema>;
798
+ ```
799
+
800
+ A producer-declared evidence scope as stored in a layer, parsed from `EvidenceScopeSchema`.
801
+
779
802
  #### `AttachmentSchema`
780
803
 
781
804
  ```ts
@@ -27,6 +27,7 @@ decided exactly as before ([the report contract](report-contract.md)). Evidence
27
27
  | `attempts` | check | Every try of a retried check, with its own status, detail, steps and attachments. A check that passed after failing is flaky. |
28
28
  | `tags`, `labels` | check, layer | Filters and groups (`smoke`, `flaky`, an owner, a browser). |
29
29
  | `tags` | attachment | Marks one recording or screenshot, for example `demo`: shown as a `#demo` chip, and searchable. See [Tagging artifacts and demos](#tagging-artifacts-and-demos). |
30
+ | `scope` | attachment, attempt, check, part, layer | What a recording does and does not show: where it ran, whether data and dependencies were real, whether audio was captured. See [Evidence scope](#evidence-scope). |
30
31
  | `annotations`, `links` | check, step, attempt, layer | Notes, warnings and issue links next to the result. |
31
32
  | `specPath` | check | The spec a check belongs to (a `.feature`, a test file), relative to the repository. Checks are grouped by it. |
32
33
  | `specs[]` (`path`, `domain`, `kind`, `title`) | layer | Names each spec and its group (`domain`), so a server can group checks by spec with a title even when no [spec set](integrating.md#tell-atlas-which-spec-a-check-belongs-to) has been published. |
@@ -62,6 +63,52 @@ attachments with that tag, and says how many match. Tags describe evidence; they
62
63
  - **Older layers.** A layer without attachment tags reads exactly as before. Tags on an attachment that cannot be read
63
64
  (not a list of strings) are dropped, and the attachment, check and layer stay.
64
65
 
66
+ ## Evidence scope
67
+
68
+ A green result and a video do not say whether the speech was real, the replies scripted, the audio recorded, the device
69
+ physical or a vendor substituted. Declare it where the evidence is, in an optional `scope`, and the viewer shows it as
70
+ short badges above the recording (before it is played), with the exact declaration on demand:
71
+
72
+ ```ts
73
+ {
74
+ name: 'checkout.webm', role: 'video',
75
+ scope: {
76
+ surface: 'browser', // where the product ran: browser, native-app, api…
77
+ deviceKind: 'simulator', // simulator, emulator, physical, virtual-machine, desktop…
78
+ syntheticData: true, // made-up data (false: real, absent: unknown)
79
+ dependencies: 'scripted', // scripted, live or mixed
80
+ audioCaptured: false, // false: silent, absent: unknown
81
+ pacing: 'paced', // paced, real-time
82
+ asserted: ['the receipt is shown'], // what the result stands on
83
+ limitations: ['Replies are scripted'],
84
+ declaredBy: 'checkout e2e suite',
85
+ verified: ['deviceKind'], // fields something independent checked
86
+ verifiedBy: 'device farm session log',
87
+ provenance: [{ url: 'https://farm.example/session/1', name: 'Device session' }]
88
+ }
89
+ }
90
+ ```
91
+
92
+ Every field is optional, and every value except the booleans is an open string.
93
+
94
+ - **Unknown is not live.** A recording with no scope, or a scope that does not say a thing, shows `Scope unknown` or
95
+ `Not declared`. It is never assumed live, physical, audible or end to end.
96
+ - **Declared is not verified.** Atlas shows `scope` as the producer's word. Only the fields named in `verified` are
97
+ marked verified (with `verifiedBy`), and `provenance` links where to check them. Atlas does not check any of it.
98
+ - **Only what was asserted.** `asserted` lists the boundaries the result stands on. The viewer says that other activity
99
+ visible in the clip is not verified behavior; with no `asserted` it says that no boundary was declared.
100
+ - **The nearest declaration, whole.** A recording takes the `scope` of the attachment, else its attempt, its check, its
101
+ part, its layer, in that order. It is never merged field by field, so a retry on a physical phone does not borrow
102
+ the limitations declared for the simulator try, and the badges change with the exact recording opened.
103
+ - **Filtering.** `GET /api/reports/tests?scope=device:physical,audio:recorded` (and the explorer's *Recording scope*
104
+ filter) lists tests with one playable recording that states every token: `surface:<v>`, `device:<v>`,
105
+ `data:synthetic` or `data:real`, `dependencies:<v>`, `audio:recorded` or `audio:silent`, `pacing:<v>`. A simulator
106
+ and a physical phone are separate values. Evidence that did not declare a value is never included, and `unknown`
107
+ asks for recordings that declared nothing. Each test lists its recordings' tokens as `scopeProfiles`, the facets as
108
+ `scopes`, and the search box finds the words in them.
109
+ - **It decides nothing.** A scope never changes a status, a slot or a winner. A scope an older Atlas cannot read is
110
+ dropped, and the attachment, check and layer stay.
111
+
65
112
  ## Finding runs and artifacts
66
113
 
67
114
  The viewer's **Runs** and **Artifacts** tabs and the API answer one query, so a link means the same thing everywhere.
package/llms-full.txt CHANGED
@@ -1014,6 +1014,7 @@ decided exactly as before ([the report contract](report-contract.md)). Evidence
1014
1014
  | `attempts` | check | Every try of a retried check, with its own status, detail, steps and attachments. A check that passed after failing is flaky. |
1015
1015
  | `tags`, `labels` | check, layer | Filters and groups (`smoke`, `flaky`, an owner, a browser). |
1016
1016
  | `tags` | attachment | Marks one recording or screenshot, for example `demo`: shown as a `#demo` chip, and searchable. See [Tagging artifacts and demos](#tagging-artifacts-and-demos). |
1017
+ | `scope` | attachment, attempt, check, part, layer | What a recording does and does not show: where it ran, whether data and dependencies were real, whether audio was captured. See [Evidence scope](#evidence-scope). |
1017
1018
  | `annotations`, `links` | check, step, attempt, layer | Notes, warnings and issue links next to the result. |
1018
1019
  | `specPath` | check | The spec a check belongs to (a `.feature`, a test file), relative to the repository. Checks are grouped by it. |
1019
1020
  | `specs[]` (`path`, `domain`, `kind`, `title`) | layer | Names each spec and its group (`domain`), so a server can group checks by spec with a title even when no [spec set](integrating.md#tell-atlas-which-spec-a-check-belongs-to) has been published. |
@@ -1049,6 +1050,52 @@ attachments with that tag, and says how many match. Tags describe evidence; they
1049
1050
  - **Older layers.** A layer without attachment tags reads exactly as before. Tags on an attachment that cannot be read
1050
1051
  (not a list of strings) are dropped, and the attachment, check and layer stay.
1051
1052
 
1053
+ ## Evidence scope
1054
+
1055
+ A green result and a video do not say whether the speech was real, the replies scripted, the audio recorded, the device
1056
+ physical or a vendor substituted. Declare it where the evidence is, in an optional `scope`, and the viewer shows it as
1057
+ short badges above the recording (before it is played), with the exact declaration on demand:
1058
+
1059
+ ```ts
1060
+ {
1061
+ name: 'checkout.webm', role: 'video',
1062
+ scope: {
1063
+ surface: 'browser', // where the product ran: browser, native-app, api…
1064
+ deviceKind: 'simulator', // simulator, emulator, physical, virtual-machine, desktop…
1065
+ syntheticData: true, // made-up data (false: real, absent: unknown)
1066
+ dependencies: 'scripted', // scripted, live or mixed
1067
+ audioCaptured: false, // false: silent, absent: unknown
1068
+ pacing: 'paced', // paced, real-time
1069
+ asserted: ['the receipt is shown'], // what the result stands on
1070
+ limitations: ['Replies are scripted'],
1071
+ declaredBy: 'checkout e2e suite',
1072
+ verified: ['deviceKind'], // fields something independent checked
1073
+ verifiedBy: 'device farm session log',
1074
+ provenance: [{ url: 'https://farm.example/session/1', name: 'Device session' }]
1075
+ }
1076
+ }
1077
+ ```
1078
+
1079
+ Every field is optional, and every value except the booleans is an open string.
1080
+
1081
+ - **Unknown is not live.** A recording with no scope, or a scope that does not say a thing, shows `Scope unknown` or
1082
+ `Not declared`. It is never assumed live, physical, audible or end to end.
1083
+ - **Declared is not verified.** Atlas shows `scope` as the producer's word. Only the fields named in `verified` are
1084
+ marked verified (with `verifiedBy`), and `provenance` links where to check them. Atlas does not check any of it.
1085
+ - **Only what was asserted.** `asserted` lists the boundaries the result stands on. The viewer says that other activity
1086
+ visible in the clip is not verified behavior; with no `asserted` it says that no boundary was declared.
1087
+ - **The nearest declaration, whole.** A recording takes the `scope` of the attachment, else its attempt, its check, its
1088
+ part, its layer, in that order. It is never merged field by field, so a retry on a physical phone does not borrow
1089
+ the limitations declared for the simulator try, and the badges change with the exact recording opened.
1090
+ - **Filtering.** `GET /api/reports/tests?scope=device:physical,audio:recorded` (and the explorer's *Recording scope*
1091
+ filter) lists tests with one playable recording that states every token: `surface:<v>`, `device:<v>`,
1092
+ `data:synthetic` or `data:real`, `dependencies:<v>`, `audio:recorded` or `audio:silent`, `pacing:<v>`. A simulator
1093
+ and a physical phone are separate values. Evidence that did not declare a value is never included, and `unknown`
1094
+ asks for recordings that declared nothing. Each test lists its recordings' tokens as `scopeProfiles`, the facets as
1095
+ `scopes`, and the search box finds the words in them.
1096
+ - **It decides nothing.** A scope never changes a status, a slot or a winner. A scope an older Atlas cannot read is
1097
+ dropped, and the attachment, check and layer stay.
1098
+
1052
1099
  ## Finding runs and artifacts
1053
1100
 
1054
1101
  The viewer's **Runs** and **Artifacts** tabs and the API answer one query, so a link means the same thing everywhere.
@@ -2757,6 +2804,7 @@ carry any of these. Every one is optional.
2757
2804
  | `attempts[]` | Every try, when the check was retried. |
2758
2805
  | `tags[]` | Free labels to filter by: `smoke`, `payments`. |
2759
2806
  | `labels{}` | Facts about the result that are not its identity: `owner`, `severity`, `browser`, `build`. |
2807
+ | `scope{}` | What the evidence does and does not show: surface, device kind, synthetic data, scripted dependencies, audio, pacing, what was asserted and its limits. Unknown when absent. See [Evidence scope](rich-evidence.md#evidence-scope). |
2760
2808
  | `links[]` | Where to read more: the CI run, the issue, the trace client. |
2761
2809
  | `extensions{}` | Structured data of your own or a tool's, under a name you own. |
2762
2810
  | `measurements[]` | Numbers, with units and an allowance (already part of the contract). |
@@ -4923,6 +4971,7 @@ Send `format=markdown` (or `Accept: text/markdown`) for the same as a Markdown r
4923
4971
  - `tag` (query): Only tests with this tag. Repeatable or comma-separated.
4924
4972
  - `artifactTag` (query): Only tests with an attachment tagged this (`demo` and `#demo` are the same tag), not counting tags inherited from a check or run: the exact artifact query. `inherited=1` widens it to the tags inherited from the check and run. Repeatable or comma-separated.
4925
4973
  - `inherited` (query): With `artifactTag`: `1` also matches the tags an attachment inherits from its check and run, so a recording without a tag of its own under a run tagged `demo` is found. Without it the query is exact.
4974
+ - `scope` (query): Only tests with one playable recording that states every one of these producer-declared qualifications: `surface:<value>`, `device:<value>` (`device:physical` and `device:simulator` stay apart), `data:synthetic` or `data:real`, `dependencies:<value>`, `audio:recorded` or `audio:silent`, `pacing:<value>`. All must hold for the same recording. A recording that did not declare a value never matches it, and `unknown` matches only a recording that declared no scope. Repeatable or comma-separated.
4926
4975
  - `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
4927
4976
  - `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
4928
4977
  - `offset` (query): Skip this many matching tests.
@@ -4954,6 +5003,7 @@ Scoped to one project. The same project may be selected on the unprefixed API us
4954
5003
  - `tag` (query): Only tests with this tag. Repeatable or comma-separated.
4955
5004
  - `artifactTag` (query): Only tests with an attachment tagged this (`demo` and `#demo` are the same tag), not counting tags inherited from a check or run: the exact artifact query. `inherited=1` widens it to the tags inherited from the check and run. Repeatable or comma-separated.
4956
5005
  - `inherited` (query): With `artifactTag`: `1` also matches the tags an attachment inherits from its check and run, so a recording without a tag of its own under a run tagged `demo` is found. Without it the query is exact.
5006
+ - `scope` (query): Only tests with one playable recording that states every one of these producer-declared qualifications: `surface:<value>`, `device:<value>` (`device:physical` and `device:simulator` stay apart), `data:synthetic` or `data:real`, `dependencies:<value>`, `audio:recorded` or `audio:silent`, `pacing:<value>`. All must hold for the same recording. A recording that did not declare a value never matches it, and `unknown` matches only a recording that declared no scope. Repeatable or comma-separated.
4957
5007
  - `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
4958
5008
  - `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
4959
5009
  - `offset` (query): Skip this many matching tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/atlas",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Atlas: specs of any kind, reported as layered evidence. Publish checks from any runner (JUnit, CTRF, a script), combine them across nodes, branches, environments and backends, gate CI on the result, and run statecharts against the real app.",
5
5
  "license": "MIT",
6
6
  "type": "module",