@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
@@ -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({
@@ -27,7 +27,8 @@ import { ReportIndex } from '../../reports/src/report-index.js';
27
27
  import { listRunSummaries, readRunFile } from '../../reports/src/run-bundle.js';
28
28
  import { latestSpecSet, readSpecBody } from '../../reports/src/spec-set.js';
29
29
  import { catalogHealth, healthMarkdown, parseRootMappings } from '../../reports/src/catalog-health.js';
30
- import { catalogMarkdown, catalogModel, historyMarkdown, parseTestQuery, runDetail, runList, runMarkdown, runsMarkdown, testCatalog, testHistory } from '../../reports/src/test-catalog.js';
30
+ import { parseArtifactFilter } from '../../reports/src/artifact-query.js';
31
+ import { artifactList, artifactsMarkdown, catalogMarkdown, catalogModel, historyMarkdown, parseTestQuery, runDetail, runList, runMarkdown, runsMarkdown, testCatalog, testHistory } from '../../reports/src/test-catalog.js';
31
32
  import { llmsTxt } from './llms.js';
32
33
  import { openApiDocument } from './openapi.js';
33
34
  import { asProblem, problemResponse } from './problem.js';
@@ -256,7 +257,7 @@ function createProjectApplication(options) {
256
257
  name: 'atlas', version: options.version ?? null, role: identity?.role ?? 'read', open: auth.open,
257
258
  write: options.write, ready: index.ready, viewers: events.count, time: now(),
258
259
  backends: snapshot.backends.map(({ id, label, available }) => ({ id, label, available, write: id === options.write })),
259
- ...(({ backends: _unused, ...rest }) => rest)(index.stats()), capabilities: { events: true, ingest: true, store: true, checks: true, status: true, effective: true, live: true, tests: true, artifactTags: true }
260
+ ...(({ backends: _unused, ...rest }) => rest)(index.stats()), capabilities: { events: true, ingest: true, store: true, checks: true, status: true, effective: true, live: true, tests: true, artifactTags: true, artifactSearch: true }
260
261
  });
261
262
  });
262
263
  app.get('/api/live', async (c) => c.json({ runs: await live.list() }, 200, { 'cache-control': 'no-store' }));
@@ -471,9 +472,19 @@ function createProjectApplication(options) {
471
472
  return notReady(c);
472
473
  const found = await catalog();
473
474
  const limit = /^\d+$/.test(c.req.query('limit') ?? '') ? Number(c.req.query('limit')) : 100;
474
- const video = ['1', 'true'].includes(c.req.query('video') ?? '');
475
- const document = () => runList(index.history, found.version, limit, { video });
476
- return answer(c, found.version, document, () => runsMarkdown(document()), `runs|${limit}|${video}|${wantsMarkdown(c)}`);
475
+ const search = new URL(c.req.url).searchParams;
476
+ const document = () => runList(index.history, found.version, limit, parseArtifactFilter(search));
477
+ return answer(c, found.version, document, () => runsMarkdown(document()), `runs|${search.toString()}|${wantsMarkdown(c)}`);
478
+ });
479
+ app.get('/api/reports/artifacts', async (c) => {
480
+ if (!index.ready)
481
+ return notReady(c);
482
+ const found = await catalog();
483
+ const search = new URL(c.req.url).searchParams;
484
+ const number = (name, fallback) => (/^\d+$/.test(search.get(name) ?? '') ? Number(search.get(name)) : fallback);
485
+ const domains = new Map(found.facets.specs.map(spec => [spec.path, spec.domain]));
486
+ const document = () => artifactList(index.history, found.version, parseArtifactFilter(search), { limit: number('limit', 100), offset: number('offset', 0), domainOf: path => domains.get(path) });
487
+ return answer(c, found.version, document, () => artifactsMarkdown(document()), `artifacts|${search.toString()}|${wantsMarkdown(c)}`);
477
488
  });
478
489
  app.get('/api/reports/runs/detail', async (c) => {
479
490
  if (!index.ready)
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { z } from 'zod/v4';
16
16
  import { ProjectsSchema } from '../../schema/src/project.js';
17
- import { EffectiveDeltaSchema, EffectiveEvidenceSchema, EvidencePageSchema, HealthSchema, LayerRowsSchema, CheckDetailSchema, OutlineSchema, ProblemSchema, ReadySchema, ServerInfoSchema, StoreListingSchema, StoreWriteResultSchema, ChecksResultSchema, HelloEventSchema, ReportEventSchema, PingEventSchema, TestCatalogSchema, CatalogHealthSchema, TestHistorySchema, RunListSchema, RunDetailSchema, ArtifactTagEditSchema, ArtifactTagEntrySchema, ArtifactTagsSchema } from '../../schema/src/api.js';
17
+ import { EffectiveDeltaSchema, EffectiveEvidenceSchema, EvidencePageSchema, HealthSchema, LayerRowsSchema, CheckDetailSchema, OutlineSchema, ProblemSchema, ReadySchema, ServerInfoSchema, StoreListingSchema, StoreWriteResultSchema, ChecksResultSchema, HelloEventSchema, ReportEventSchema, PingEventSchema, TestCatalogSchema, CatalogHealthSchema, TestHistorySchema, RunListSchema, RunDetailSchema, ArtifactListSchema, ArtifactTagEditSchema, ArtifactTagEntrySchema, ArtifactTagsSchema } from '../../schema/src/api.js';
18
18
  import { LiveRunSchema, LiveRunStartSchema, LiveRunUpdateSchema, ServerEditableSettingsSchema, ServerSettingsSnapshotSchema, GcPolicySchema, GcSnapshotSchema, GcRunResultSchema, AtlasChangeSchema, PublishChecksSchema, ReportSourcesSchema, ReportStatusSchema, RunsIndexSchema, SpecIndexSchema } from '../../schema/src/index.js';
19
19
  /** The version of the API described, `major.minor.patch`: the minor grows when an operation or field is added, the major never within `/api`. */
20
20
  export const API_VERSION = '1.5.0';
@@ -37,7 +37,7 @@ const COMPONENTS = {
37
37
  StoreListing: [StoreListingSchema, 'output'], StoreWriteResult: [StoreWriteResultSchema, 'output'],
38
38
  Outline: [OutlineSchema, 'output'], EvidencePage: [EvidencePageSchema, 'output'], LayerRows: [LayerRowsSchema, 'output'], CheckDetail: [CheckDetailSchema, 'output'],
39
39
  ArtifactTagEdit: [ArtifactTagEditSchema, 'input'], ArtifactTagEntry: [ArtifactTagEntrySchema, 'output'], ArtifactTags: [ArtifactTagsSchema, 'output'],
40
- TestCatalog: [TestCatalogSchema, 'output'], CatalogHealth: [CatalogHealthSchema, 'output'], TestHistory: [TestHistorySchema, 'output'], RunList: [RunListSchema, 'output'], RunDetail: [RunDetailSchema, 'output'],
40
+ TestCatalog: [TestCatalogSchema, 'output'], CatalogHealth: [CatalogHealthSchema, 'output'], TestHistory: [TestHistorySchema, 'output'], RunList: [RunListSchema, 'output'], ArtifactList: [ArtifactListSchema, 'output'], RunDetail: [RunDetailSchema, 'output'],
41
41
  SpecIndex: [SpecIndexSchema, 'output'], ReportSources: [ReportSourcesSchema, 'output'], RunsIndex: [RunsIndexSchema, 'output'],
42
42
  RunEvent: [LiveRunSchema, 'output'], HelloEvent: [HelloEventSchema, 'output'], ReportEvent: [ReportEventSchema, 'output'], AtlasChangeEvent: [AtlasChangeSchema, 'output'], PingEvent: [PingEventSchema, 'output']
43
43
  };
@@ -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 } },
@@ -343,14 +344,52 @@ const BASE_OPERATIONS = [
343
344
  {
344
345
  route: 'GET /api/reports/runs', path: '/api/reports/runs', operationId: 'getReportRuns', tag: 'Report', role: 'read',
345
346
  summary: 'The runs that recorded the report',
346
- description: 'Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures and their video counts (references, distinct playable recordings, unavailable). Send `format=markdown` for Markdown.',
347
+ description: 'Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures, their video counts (references, distinct playable recordings, unavailable) and the artifact tags, media types and labels their results carry. Filters narrow the list before `limit`: AND across the parameters, OR within one; `filters` in the answer names those that were applied and `facets` offers each dimension\'s values with counts. A server that does not know a filter ignores it and does not name it in `filters`. Send `format=markdown` for Markdown.',
347
348
  parameters: [
348
349
  { name: 'limit', in: 'query', description: 'At most this many runs (default 100).', schema: { type: 'integer', minimum: 0 } },
349
350
  { name: 'video', in: 'query', description: '`true` to list only runs that have a playable video recording. Applied to every run before `limit`; `total` counts the matches. A server that does not know it answers with every run.', schema: { type: 'boolean' } },
351
+ { name: 'artifactTag', in: 'query', description: 'Artifact tags (`demo` and `#demo` are the same), repeatable or comma-separated. A comma or backslash inside a value is written `\\,` or `\\\\`. OR within the parameter, AND with the other filters.', schema: str },
352
+ { name: 'inherited', in: 'query', description: '`1` with `artifactTag` to also match the tags attachments inherit from their check and run. Default: the exact artifact query.', schema: { type: 'boolean' } },
353
+ { name: 'type', in: 'query', description: 'Media types as roles (`video`, `image`, `audio`, `snapshot`, `text`, `file`, or a producer\'s own). `video` means a playable recording. With `artifactTag`, one artifact must be both where the report says which videos carry which tags.', schema: str },
354
+ { name: 'label', in: 'query', description: '`key=value`, or `key` for any value.', schema: str },
355
+ { name: 'environment', in: 'query', description: 'Environments or devices (`ios`, `desktop-web`).', schema: str },
356
+ { name: 'producer', in: 'query', description: 'Producers.', schema: str },
357
+ { name: 'branch', in: 'query', description: 'Branches.', schema: str },
358
+ { name: 'sha', in: 'query', description: 'Revisions: a full commit id or a prefix of at least seven characters.', schema: str },
359
+ { name: 'outcome', in: 'query', description: 'Statuses: `passed`, `failed`, `running`, `not-run`. A run\'s outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`.', schema: str },
360
+ { name: 'q', in: 'query', description: 'Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.', schema: str },
361
+ { name: 'since', in: 'query', description: 'Only records at or after this time: epoch milliseconds or an ISO date (a date alone is UTC). A result\'s recording time; a run\'s end.', schema: str },
362
+ { name: 'until', in: 'query', description: 'Only records at or before this time (same forms as `since`).', schema: str },
350
363
  { name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
351
364
  ],
352
365
  responses: { '200': json('The runs.', 'RunList'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '401': UNAUTHORIZED, '503': NOT_READY }
353
366
  },
367
+ {
368
+ route: 'GET /api/reports/artifacts', path: '/api/reports/artifacts', operationId: 'getReportArtifacts', tag: 'Report', role: 'read',
369
+ summary: 'Recorded results that carry evidence, across the whole history',
370
+ description: 'Every recorded result that has attachments (recordings, screenshots, traces), newest first, found in the history and not only among the current winners: a recording tagged `demo` is found with its original run, revision and status even when a later attempt is untagged. Narrow it by artifact tag, media type, label, feature (`domain`, `spec`), environment or device, producer, branch, revision, outcome and time: AND across the parameters, OR within one; with both `artifactTag` and `type`, one artifact must be both. `filters` names the filters that were applied and `facets` offers each dimension\'s values with counts: the count beside a value is what the list would hold if that value alone were chosen in its dimension, every other filter applied. Tags written by reviewers after upload are not searched; only those in the layer are. Send `format=markdown` for Markdown.',
371
+ parameters: [
372
+ { name: 'limit', in: 'query', description: 'At most this many results (default 100).', schema: { type: 'integer', minimum: 0 } },
373
+ { name: 'offset', in: 'query', description: 'Skip this many matches (default 0).', schema: { type: 'integer', minimum: 0 } },
374
+ { name: 'video', in: 'query', description: '`true` for results with a playable video recording (the same as `type=video`).', schema: { type: 'boolean' } },
375
+ { name: 'domain', in: 'query', description: 'The feature domain of the result\'s spec.', schema: str },
376
+ { name: 'spec', in: 'query', description: 'Spec paths.', schema: str },
377
+ { name: 'artifactTag', in: 'query', description: 'Artifact tags (`demo` and `#demo` are the same), repeatable or comma-separated. A comma or backslash inside a value is written `\\,` or `\\\\`. OR within the parameter, AND with the other filters.', schema: str },
378
+ { name: 'inherited', in: 'query', description: '`1` with `artifactTag` to also match the tags attachments inherit from their check and run. Default: the exact artifact query.', schema: { type: 'boolean' } },
379
+ { name: 'type', in: 'query', description: 'Media types as roles (`video`, `image`, `audio`, `snapshot`, `text`, `file`, or a producer\'s own). `video` means a playable recording. With `artifactTag`, one artifact must be both where the report says which videos carry which tags.', schema: str },
380
+ { name: 'label', in: 'query', description: '`key=value`, or `key` for any value.', schema: str },
381
+ { name: 'environment', in: 'query', description: 'Environments or devices (`ios`, `desktop-web`).', schema: str },
382
+ { name: 'producer', in: 'query', description: 'Producers.', schema: str },
383
+ { name: 'branch', in: 'query', description: 'Branches.', schema: str },
384
+ { name: 'sha', in: 'query', description: 'Revisions: a full commit id or a prefix of at least seven characters.', schema: str },
385
+ { name: 'outcome', in: 'query', description: 'Statuses: `passed`, `failed`, `running`, `not-run`. A run\'s outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`.', schema: str },
386
+ { name: 'q', in: 'query', description: 'Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.', schema: str },
387
+ { name: 'since', in: 'query', description: 'Only records at or after this time: epoch milliseconds or an ISO date (a date alone is UTC). A result\'s recording time; a run\'s end.', schema: str },
388
+ { name: 'until', in: 'query', description: 'Only records at or before this time (same forms as `since`).', schema: str },
389
+ { name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
390
+ ],
391
+ responses: { '200': json('The matching results and the facets.', 'ArtifactList'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '401': UNAUTHORIZED, '503': NOT_READY }
392
+ },
354
393
  {
355
394
  route: 'GET /api/reports/runs/detail', path: '/api/reports/runs/detail', operationId: 'getReportRun', tag: 'Report', role: 'read',
356
395
  summary: 'Every test one run recorded',
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.
@@ -644,10 +646,22 @@ Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `A
644
646
 
645
647
  `GET /api/reports/runs` · role: read
646
648
 
647
- Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures and their video counts (references, distinct playable recordings, unavailable). Send `format=markdown` for Markdown.
649
+ Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures, their video counts (references, distinct playable recordings, unavailable) and the artifact tags, media types and labels their results carry. Filters narrow the list before `limit`: AND across the parameters, OR within one; `filters` in the answer names those that were applied and `facets` offers each dimension's values with counts. A server that does not know a filter ignores it and does not name it in `filters`. Send `format=markdown` for Markdown.
648
650
 
649
651
  - `limit` (query): At most this many runs (default 100).
650
652
  - `video` (query): `true` to list only runs that have a playable video recording. Applied to every run before `limit`; `total` counts the matches. A server that does not know it answers with every run.
653
+ - `artifactTag` (query): Artifact tags (`demo` and `#demo` are the same), repeatable or comma-separated. A comma or backslash inside a value is written `\,` or `\\`. OR within the parameter, AND with the other filters.
654
+ - `inherited` (query): `1` with `artifactTag` to also match the tags attachments inherit from their check and run. Default: the exact artifact query.
655
+ - `type` (query): Media types as roles (`video`, `image`, `audio`, `snapshot`, `text`, `file`, or a producer's own). `video` means a playable recording. With `artifactTag`, one artifact must be both where the report says which videos carry which tags.
656
+ - `label` (query): `key=value`, or `key` for any value.
657
+ - `environment` (query): Environments or devices (`ios`, `desktop-web`).
658
+ - `producer` (query): Producers.
659
+ - `branch` (query): Branches.
660
+ - `sha` (query): Revisions: a full commit id or a prefix of at least seven characters.
661
+ - `outcome` (query): Statuses: `passed`, `failed`, `running`, `not-run`. A run's outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`.
662
+ - `q` (query): Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.
663
+ - `since` (query): Only records at or after this time: epoch milliseconds or an ISO date (a date alone is UTC). A result's recording time; a run's end.
664
+ - `until` (query): Only records at or before this time (same forms as `since`).
651
665
  - `format` (query): `markdown` for Markdown instead of JSON.
652
666
  - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
653
667
 
@@ -657,17 +671,87 @@ Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `40
657
671
 
658
672
  `GET /api/projects/{project}/reports/runs` · role: read
659
673
 
660
- Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures and their video counts (references, distinct playable recordings, unavailable). Send `format=markdown` for Markdown.
674
+ Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures, their video counts (references, distinct playable recordings, unavailable) and the artifact tags, media types and labels their results carry. Filters narrow the list before `limit`: AND across the parameters, OR within one; `filters` in the answer names those that were applied and `facets` offers each dimension's values with counts. A server that does not know a filter ignores it and does not name it in `filters`. Send `format=markdown` for Markdown.
661
675
 
662
676
  Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
663
677
 
664
678
  - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
665
679
  - `limit` (query): At most this many runs (default 100).
666
680
  - `video` (query): `true` to list only runs that have a playable video recording. Applied to every run before `limit`; `total` counts the matches. A server that does not know it answers with every run.
681
+ - `artifactTag` (query): Artifact tags (`demo` and `#demo` are the same), repeatable or comma-separated. A comma or backslash inside a value is written `\,` or `\\`. OR within the parameter, AND with the other filters.
682
+ - `inherited` (query): `1` with `artifactTag` to also match the tags attachments inherit from their check and run. Default: the exact artifact query.
683
+ - `type` (query): Media types as roles (`video`, `image`, `audio`, `snapshot`, `text`, `file`, or a producer's own). `video` means a playable recording. With `artifactTag`, one artifact must be both where the report says which videos carry which tags.
684
+ - `label` (query): `key=value`, or `key` for any value.
685
+ - `environment` (query): Environments or devices (`ios`, `desktop-web`).
686
+ - `producer` (query): Producers.
687
+ - `branch` (query): Branches.
688
+ - `sha` (query): Revisions: a full commit id or a prefix of at least seven characters.
689
+ - `outcome` (query): Statuses: `passed`, `failed`, `running`, `not-run`. A run's outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`.
690
+ - `q` (query): Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.
691
+ - `since` (query): Only records at or after this time: epoch milliseconds or an ISO date (a date alone is UTC). A result's recording time; a run's end.
692
+ - `until` (query): Only records at or before this time (same forms as `since`).
667
693
  - `format` (query): `markdown` for Markdown instead of JSON.
668
694
 
669
695
  Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
670
696
 
697
+ ### Recorded results that carry evidence, across the whole history
698
+
699
+ `GET /api/reports/artifacts` · role: read
700
+
701
+ Every recorded result that has attachments (recordings, screenshots, traces), newest first, found in the history and not only among the current winners: a recording tagged `demo` is found with its original run, revision and status even when a later attempt is untagged. Narrow it by artifact tag, media type, label, feature (`domain`, `spec`), environment or device, producer, branch, revision, outcome and time: AND across the parameters, OR within one; with both `artifactTag` and `type`, one artifact must be both. `filters` names the filters that were applied and `facets` offers each dimension's values with counts: the count beside a value is what the list would hold if that value alone were chosen in its dimension, every other filter applied. Tags written by reviewers after upload are not searched; only those in the layer are. Send `format=markdown` for Markdown.
702
+
703
+ - `limit` (query): At most this many results (default 100).
704
+ - `offset` (query): Skip this many matches (default 0).
705
+ - `video` (query): `true` for results with a playable video recording (the same as `type=video`).
706
+ - `domain` (query): The feature domain of the result's spec.
707
+ - `spec` (query): Spec paths.
708
+ - `artifactTag` (query): Artifact tags (`demo` and `#demo` are the same), repeatable or comma-separated. A comma or backslash inside a value is written `\,` or `\\`. OR within the parameter, AND with the other filters.
709
+ - `inherited` (query): `1` with `artifactTag` to also match the tags attachments inherit from their check and run. Default: the exact artifact query.
710
+ - `type` (query): Media types as roles (`video`, `image`, `audio`, `snapshot`, `text`, `file`, or a producer's own). `video` means a playable recording. With `artifactTag`, one artifact must be both where the report says which videos carry which tags.
711
+ - `label` (query): `key=value`, or `key` for any value.
712
+ - `environment` (query): Environments or devices (`ios`, `desktop-web`).
713
+ - `producer` (query): Producers.
714
+ - `branch` (query): Branches.
715
+ - `sha` (query): Revisions: a full commit id or a prefix of at least seven characters.
716
+ - `outcome` (query): Statuses: `passed`, `failed`, `running`, `not-run`. A run's outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`.
717
+ - `q` (query): Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.
718
+ - `since` (query): Only records at or after this time: epoch milliseconds or an ISO date (a date alone is UTC). A result's recording time; a run's end.
719
+ - `until` (query): Only records at or before this time (same forms as `since`).
720
+ - `format` (query): `markdown` for Markdown instead of JSON.
721
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
722
+
723
+ Responses: `200` The matching results and the facets.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
724
+
725
+ ### Recorded results that carry evidence, across the whole history
726
+
727
+ `GET /api/projects/{project}/reports/artifacts` · role: read
728
+
729
+ Every recorded result that has attachments (recordings, screenshots, traces), newest first, found in the history and not only among the current winners: a recording tagged `demo` is found with its original run, revision and status even when a later attempt is untagged. Narrow it by artifact tag, media type, label, feature (`domain`, `spec`), environment or device, producer, branch, revision, outcome and time: AND across the parameters, OR within one; with both `artifactTag` and `type`, one artifact must be both. `filters` names the filters that were applied and `facets` offers each dimension's values with counts: the count beside a value is what the list would hold if that value alone were chosen in its dimension, every other filter applied. Tags written by reviewers after upload are not searched; only those in the layer are. Send `format=markdown` for Markdown.
730
+
731
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
732
+
733
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
734
+ - `limit` (query): At most this many results (default 100).
735
+ - `offset` (query): Skip this many matches (default 0).
736
+ - `video` (query): `true` for results with a playable video recording (the same as `type=video`).
737
+ - `domain` (query): The feature domain of the result's spec.
738
+ - `spec` (query): Spec paths.
739
+ - `artifactTag` (query): Artifact tags (`demo` and `#demo` are the same), repeatable or comma-separated. A comma or backslash inside a value is written `\,` or `\\`. OR within the parameter, AND with the other filters.
740
+ - `inherited` (query): `1` with `artifactTag` to also match the tags attachments inherit from their check and run. Default: the exact artifact query.
741
+ - `type` (query): Media types as roles (`video`, `image`, `audio`, `snapshot`, `text`, `file`, or a producer's own). `video` means a playable recording. With `artifactTag`, one artifact must be both where the report says which videos carry which tags.
742
+ - `label` (query): `key=value`, or `key` for any value.
743
+ - `environment` (query): Environments or devices (`ios`, `desktop-web`).
744
+ - `producer` (query): Producers.
745
+ - `branch` (query): Branches.
746
+ - `sha` (query): Revisions: a full commit id or a prefix of at least seven characters.
747
+ - `outcome` (query): Statuses: `passed`, `failed`, `running`, `not-run`. A run's outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`.
748
+ - `q` (query): Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.
749
+ - `since` (query): Only records at or after this time: epoch milliseconds or an ISO date (a date alone is UTC). A result's recording time; a run's end.
750
+ - `until` (query): Only records at or before this time (same forms as `since`).
751
+ - `format` (query): `markdown` for Markdown instead of JSON.
752
+
753
+ Responses: `200` The matching results and the facets.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
754
+
671
755
  ### Every test one run recorded
672
756
 
673
757
  `GET /api/reports/runs/detail` · role: read