@crvouga/atlas 0.15.0 → 0.16.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 (30) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/reports/src/evidence-detail.d.ts +19 -2
  3. package/dist/reports/src/evidence-detail.js +63 -14
  4. package/dist/reports/src/evidence-types.d.ts +31 -0
  5. package/dist/reports/src/layer-merge.js +2 -2
  6. package/dist/reports/src/test-catalog.d.ts +24 -3
  7. package/dist/reports/src/test-catalog.js +37 -11
  8. package/dist/schema/src/api.d.ts +167 -0
  9. package/dist/schema/src/api.js +28 -4
  10. package/dist/server/src/app.js +3 -2
  11. package/dist/server/src/llms.js +2 -1
  12. package/dist/server/src/openapi.js +2 -1
  13. package/docs/reference/http-api.md +4 -2
  14. package/docs/reference/reports.md +110 -3
  15. package/docs/reference/schema.md +16 -0
  16. package/docs/server.md +1 -1
  17. package/llms-full.txt +5 -3
  18. package/package.json +1 -1
  19. package/packages/schema/schemas/effective-delta.schema.json +82 -0
  20. package/packages/schema/schemas/effective-evidence.schema.json +82 -0
  21. package/packages/schema/schemas/evidence-page.schema.json +41 -0
  22. package/packages/schema/schemas/layer-rows.schema.json +41 -0
  23. package/packages/schema/schemas/report-run.schema.json +71 -0
  24. package/packages/schema/schemas/report-runs.schema.json +38 -1
  25. package/packages/schema/schemas/test-catalog.schema.json +41 -0
  26. package/packages/schema/schemas/test-history.schema.json +82 -0
  27. package/packages/server/openapi.json +498 -3
  28. package/packages/visualizer/src/explorer/RunsView.tsx +128 -10
  29. package/packages/visualizer/src/explorer/catalog.tsx +16 -6
  30. package/packages/visualizer/src/explorer/search.ts +4 -1
@@ -80,6 +80,21 @@ export declare const BackendInfoSchema: z.ZodObject<{
80
80
  live: z.ZodBoolean;
81
81
  available: z.ZodBoolean;
82
82
  }, z.core.$loose>;
83
+ /** One reference to a video held by a layer, check, retry attempt or step, found by the attachment's effective media type. */
84
+ export declare const VideoRefSchema: z.ZodObject<{
85
+ name: z.ZodString;
86
+ owner: z.ZodString;
87
+ attempt: z.ZodOptional<z.ZodNumber>;
88
+ step: z.ZodOptional<z.ZodString>;
89
+ recording: z.ZodOptional<z.ZodString>;
90
+ available: z.ZodBoolean;
91
+ }, z.core.$loose>;
92
+ /** Video references as counts: how many are referenced, how many distinct recordings can be played, how many cannot. */
93
+ export declare const VideoCountsSchema: z.ZodObject<{
94
+ references: z.ZodNumber;
95
+ recordings: z.ZodNumber;
96
+ unavailable: z.ZodNumber;
97
+ }, z.core.$loose>;
83
98
  /** What a result carries beyond its outcome, as counts and short facts. Present only when there is something. */
84
99
  export declare const EvidenceSummarySchema: z.ZodObject<{
85
100
  attachments: z.ZodNumber;
@@ -91,6 +106,14 @@ export declare const EvidenceSummarySchema: z.ZodObject<{
91
106
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
92
107
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
93
108
  roles: z.ZodArray<z.ZodString>;
109
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
110
+ name: z.ZodString;
111
+ owner: z.ZodString;
112
+ attempt: z.ZodOptional<z.ZodNumber>;
113
+ step: z.ZodOptional<z.ZodString>;
114
+ recording: z.ZodOptional<z.ZodString>;
115
+ available: z.ZodBoolean;
116
+ }, z.core.$loose>>>;
94
117
  }, z.core.$loose>;
95
118
  /** A layer as the report shows it: its origin and its checks by status. */
96
119
  export declare const LayerHeaderSchema: z.ZodObject<{
@@ -133,6 +156,14 @@ export declare const LayerHeaderSchema: z.ZodObject<{
133
156
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
134
157
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
135
158
  roles: z.ZodArray<z.ZodString>;
159
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
160
+ name: z.ZodString;
161
+ owner: z.ZodString;
162
+ attempt: z.ZodOptional<z.ZodNumber>;
163
+ step: z.ZodOptional<z.ZodString>;
164
+ recording: z.ZodOptional<z.ZodString>;
165
+ available: z.ZodBoolean;
166
+ }, z.core.$loose>>>;
136
167
  }, z.core.$loose>>;
137
168
  }, z.core.$loose>;
138
169
  /** One check of a layer as the report shows it. */
@@ -167,6 +198,14 @@ export declare const LayerRowSchema: z.ZodObject<{
167
198
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
168
199
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
169
200
  roles: z.ZodArray<z.ZodString>;
201
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
202
+ name: z.ZodString;
203
+ owner: z.ZodString;
204
+ attempt: z.ZodOptional<z.ZodNumber>;
205
+ step: z.ZodOptional<z.ZodString>;
206
+ recording: z.ZodOptional<z.ZodString>;
207
+ available: z.ZodBoolean;
208
+ }, z.core.$loose>>>;
170
209
  }, z.core.$loose>>;
171
210
  }, z.core.$loose>;
172
211
  /** A layer with its checks: the rows of one layer, read apart from its header. */
@@ -203,6 +242,14 @@ export declare const LayerRowsSchema: z.ZodObject<{
203
242
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
204
243
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
205
244
  roles: z.ZodArray<z.ZodString>;
245
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
246
+ name: z.ZodString;
247
+ owner: z.ZodString;
248
+ attempt: z.ZodOptional<z.ZodNumber>;
249
+ step: z.ZodOptional<z.ZodString>;
250
+ recording: z.ZodOptional<z.ZodString>;
251
+ available: z.ZodBoolean;
252
+ }, z.core.$loose>>>;
206
253
  }, z.core.$loose>>;
207
254
  }, z.core.$loose>>;
208
255
  fingerprints: z.ZodRecord<z.ZodString, z.ZodString>;
@@ -241,6 +288,14 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
241
288
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
242
289
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
243
290
  roles: z.ZodArray<z.ZodString>;
291
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
292
+ name: z.ZodString;
293
+ owner: z.ZodString;
294
+ attempt: z.ZodOptional<z.ZodNumber>;
295
+ step: z.ZodOptional<z.ZodString>;
296
+ recording: z.ZodOptional<z.ZodString>;
297
+ available: z.ZodBoolean;
298
+ }, z.core.$loose>>>;
244
299
  }, z.core.$loose>>;
245
300
  }, z.core.$loose>>;
246
301
  fingerprints: z.ZodRecord<z.ZodString, z.ZodString>;
@@ -284,6 +339,14 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
284
339
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
285
340
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
286
341
  roles: z.ZodArray<z.ZodString>;
342
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
343
+ name: z.ZodString;
344
+ owner: z.ZodString;
345
+ attempt: z.ZodOptional<z.ZodNumber>;
346
+ step: z.ZodOptional<z.ZodString>;
347
+ recording: z.ZodOptional<z.ZodString>;
348
+ available: z.ZodBoolean;
349
+ }, z.core.$loose>>>;
287
350
  }, z.core.$loose>>;
288
351
  }, z.core.$loose>;
289
352
  }, z.core.$loose>;
@@ -327,6 +390,14 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
327
390
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
328
391
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
329
392
  roles: z.ZodArray<z.ZodString>;
393
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
394
+ name: z.ZodString;
395
+ owner: z.ZodString;
396
+ attempt: z.ZodOptional<z.ZodNumber>;
397
+ step: z.ZodOptional<z.ZodString>;
398
+ recording: z.ZodOptional<z.ZodString>;
399
+ available: z.ZodBoolean;
400
+ }, z.core.$loose>>>;
330
401
  }, z.core.$loose>>;
331
402
  }, z.core.$loose>>;
332
403
  fingerprints: z.ZodRecord<z.ZodString, z.ZodString>;
@@ -370,6 +441,14 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
370
441
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
371
442
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
372
443
  roles: z.ZodArray<z.ZodString>;
444
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
445
+ name: z.ZodString;
446
+ owner: z.ZodString;
447
+ attempt: z.ZodOptional<z.ZodNumber>;
448
+ step: z.ZodOptional<z.ZodString>;
449
+ recording: z.ZodOptional<z.ZodString>;
450
+ available: z.ZodBoolean;
451
+ }, z.core.$loose>>>;
373
452
  }, z.core.$loose>>;
374
453
  }, z.core.$loose>;
375
454
  }, z.core.$loose>>;
@@ -434,6 +513,14 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
434
513
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
435
514
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
436
515
  roles: z.ZodArray<z.ZodString>;
516
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
517
+ name: z.ZodString;
518
+ owner: z.ZodString;
519
+ attempt: z.ZodOptional<z.ZodNumber>;
520
+ step: z.ZodOptional<z.ZodString>;
521
+ recording: z.ZodOptional<z.ZodString>;
522
+ available: z.ZodBoolean;
523
+ }, z.core.$loose>>>;
437
524
  }, z.core.$loose>>;
438
525
  }, z.core.$loose>>;
439
526
  fingerprints: z.ZodRecord<z.ZodString, z.ZodString>;
@@ -477,6 +564,14 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
477
564
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
478
565
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
479
566
  roles: z.ZodArray<z.ZodString>;
567
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
568
+ name: z.ZodString;
569
+ owner: z.ZodString;
570
+ attempt: z.ZodOptional<z.ZodNumber>;
571
+ step: z.ZodOptional<z.ZodString>;
572
+ recording: z.ZodOptional<z.ZodString>;
573
+ available: z.ZodBoolean;
574
+ }, z.core.$loose>>>;
480
575
  }, z.core.$loose>>;
481
576
  }, z.core.$loose>;
482
577
  }, z.core.$loose>>;
@@ -544,6 +639,14 @@ export declare const EvidencePageSchema: z.ZodObject<{
544
639
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
545
640
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
546
641
  roles: z.ZodArray<z.ZodString>;
642
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
643
+ name: z.ZodString;
644
+ owner: z.ZodString;
645
+ attempt: z.ZodOptional<z.ZodNumber>;
646
+ step: z.ZodOptional<z.ZodString>;
647
+ recording: z.ZodOptional<z.ZodString>;
648
+ available: z.ZodBoolean;
649
+ }, z.core.$loose>>>;
547
650
  }, z.core.$loose>>;
548
651
  }, z.core.$loose>>;
549
652
  cursor: z.ZodNullable<z.ZodString>;
@@ -923,6 +1026,14 @@ export declare const TestResultEntrySchema: z.ZodObject<{
923
1026
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
924
1027
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
925
1028
  roles: z.ZodArray<z.ZodString>;
1029
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
1030
+ name: z.ZodString;
1031
+ owner: z.ZodString;
1032
+ attempt: z.ZodOptional<z.ZodNumber>;
1033
+ step: z.ZodOptional<z.ZodString>;
1034
+ recording: z.ZodOptional<z.ZodString>;
1035
+ available: z.ZodBoolean;
1036
+ }, z.core.$loose>>>;
926
1037
  }, z.core.$loose>>;
927
1038
  }, z.core.$loose>;
928
1039
  /** One test of the catalog: what it is, its current result per environment, and how it has gone over its recorded history. */
@@ -975,6 +1086,14 @@ export declare const TestSummarySchema: z.ZodObject<{
975
1086
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
976
1087
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
977
1088
  roles: z.ZodArray<z.ZodString>;
1089
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
1090
+ name: z.ZodString;
1091
+ owner: z.ZodString;
1092
+ attempt: z.ZodOptional<z.ZodNumber>;
1093
+ step: z.ZodOptional<z.ZodString>;
1094
+ recording: z.ZodOptional<z.ZodString>;
1095
+ available: z.ZodBoolean;
1096
+ }, z.core.$loose>>>;
978
1097
  }, z.core.$loose>>;
979
1098
  }, z.core.$loose>>;
980
1099
  stats: z.ZodObject<{
@@ -1217,6 +1336,14 @@ export declare const TestCatalogSchema: z.ZodObject<{
1217
1336
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1218
1337
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
1219
1338
  roles: z.ZodArray<z.ZodString>;
1339
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
1340
+ name: z.ZodString;
1341
+ owner: z.ZodString;
1342
+ attempt: z.ZodOptional<z.ZodNumber>;
1343
+ step: z.ZodOptional<z.ZodString>;
1344
+ recording: z.ZodOptional<z.ZodString>;
1345
+ available: z.ZodBoolean;
1346
+ }, z.core.$loose>>>;
1220
1347
  }, z.core.$loose>>;
1221
1348
  }, z.core.$loose>>;
1222
1349
  stats: z.ZodObject<{
@@ -1390,6 +1517,14 @@ export declare const TestHistorySchema: z.ZodObject<{
1390
1517
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1391
1518
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
1392
1519
  roles: z.ZodArray<z.ZodString>;
1520
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
1521
+ name: z.ZodString;
1522
+ owner: z.ZodString;
1523
+ attempt: z.ZodOptional<z.ZodNumber>;
1524
+ step: z.ZodOptional<z.ZodString>;
1525
+ recording: z.ZodOptional<z.ZodString>;
1526
+ available: z.ZodBoolean;
1527
+ }, z.core.$loose>>>;
1393
1528
  }, z.core.$loose>>;
1394
1529
  }, z.core.$loose>>;
1395
1530
  stats: z.ZodObject<{
@@ -1460,6 +1595,14 @@ export declare const TestHistorySchema: z.ZodObject<{
1460
1595
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1461
1596
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
1462
1597
  roles: z.ZodArray<z.ZodString>;
1598
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
1599
+ name: z.ZodString;
1600
+ owner: z.ZodString;
1601
+ attempt: z.ZodOptional<z.ZodNumber>;
1602
+ step: z.ZodOptional<z.ZodString>;
1603
+ recording: z.ZodOptional<z.ZodString>;
1604
+ available: z.ZodBoolean;
1605
+ }, z.core.$loose>>>;
1463
1606
  }, z.core.$loose>>;
1464
1607
  }, z.core.$loose>>;
1465
1608
  }, z.core.$loose>;
@@ -1495,6 +1638,11 @@ export declare const ReportRunSchema: z.ZodObject<{
1495
1638
  environment: z.ZodNullable<z.ZodString>;
1496
1639
  detail: z.ZodString;
1497
1640
  }, z.core.$loose>>;
1641
+ videos: z.ZodOptional<z.ZodObject<{
1642
+ references: z.ZodNumber;
1643
+ recordings: z.ZodNumber;
1644
+ unavailable: z.ZodNumber;
1645
+ }, z.core.$loose>>;
1498
1646
  }, z.core.$loose>;
1499
1647
  /** `GET /api/reports/runs`: runs, newest first. */
1500
1648
  export declare const RunListSchema: z.ZodObject<{
@@ -1532,7 +1680,13 @@ export declare const RunListSchema: z.ZodObject<{
1532
1680
  environment: z.ZodNullable<z.ZodString>;
1533
1681
  detail: z.ZodString;
1534
1682
  }, z.core.$loose>>;
1683
+ videos: z.ZodOptional<z.ZodObject<{
1684
+ references: z.ZodNumber;
1685
+ recordings: z.ZodNumber;
1686
+ unavailable: z.ZodNumber;
1687
+ }, z.core.$loose>>;
1535
1688
  }, z.core.$loose>>;
1689
+ withVideo: z.ZodOptional<z.ZodNumber>;
1536
1690
  }, z.core.$loose>;
1537
1691
  /** `GET /api/reports/runs/detail`: one run and its last result for every test, environment and producer it recorded. */
1538
1692
  export declare const RunDetailSchema: z.ZodObject<{
@@ -1569,6 +1723,11 @@ export declare const RunDetailSchema: z.ZodObject<{
1569
1723
  environment: z.ZodNullable<z.ZodString>;
1570
1724
  detail: z.ZodString;
1571
1725
  }, z.core.$loose>>;
1726
+ videos: z.ZodOptional<z.ZodObject<{
1727
+ references: z.ZodNumber;
1728
+ recordings: z.ZodNumber;
1729
+ unavailable: z.ZodNumber;
1730
+ }, z.core.$loose>>;
1572
1731
  }, z.core.$loose>;
1573
1732
  results: z.ZodArray<z.ZodObject<{
1574
1733
  key: z.ZodString;
@@ -1587,5 +1746,13 @@ export declare const RunDetailSchema: z.ZodObject<{
1587
1746
  at: z.ZodNumber;
1588
1747
  layer: z.ZodString;
1589
1748
  detail: z.ZodString;
1749
+ videos: z.ZodOptional<z.ZodArray<z.ZodObject<{
1750
+ name: z.ZodString;
1751
+ owner: z.ZodString;
1752
+ attempt: z.ZodOptional<z.ZodNumber>;
1753
+ step: z.ZodOptional<z.ZodString>;
1754
+ recording: z.ZodOptional<z.ZodString>;
1755
+ available: z.ZodBoolean;
1756
+ }, z.core.$loose>>>;
1590
1757
  }, z.core.$loose>>;
1591
1758
  }, z.core.$loose>;
@@ -62,6 +62,21 @@ export const BackendInfoSchema = z.looseObject({
62
62
  live: z.boolean().describe('Always false on a server'),
63
63
  available: z.boolean().describe('False when the backend could not be read; its results are then unknown, never passed')
64
64
  });
65
+ /** One reference to a video held by a layer, check, retry attempt or step, found by the attachment's effective media type. */
66
+ export const VideoRefSchema = z.looseObject({
67
+ name: z.string().describe('The attachment name'),
68
+ owner: z.string().describe('Who holds it: `layer`, `check`, `attempt` or `step`. An open vocabulary: treat an unknown owner as a check'),
69
+ attempt: z.number().int().nonnegative().optional().describe('The retry attempt that holds it, or that holds the step that does'),
70
+ step: z.string().optional().describe('The name of the step that holds it'),
71
+ recording: z.string().optional().describe('What identifies the bytes; references with the same value are one recording. Absent when it cannot be played'),
72
+ available: z.boolean().describe('False when the reference names no bytes (no bundle key, address or inline text)')
73
+ });
74
+ /** Video references as counts: how many are referenced, how many distinct recordings can be played, how many cannot. */
75
+ export const VideoCountsSchema = z.looseObject({
76
+ references: z.number().int().nonnegative().describe('Every reference to a video; identical bytes referenced twice count twice'),
77
+ recordings: z.number().int().nonnegative().describe('Distinct playable recordings'),
78
+ unavailable: z.number().int().nonnegative().describe('References that name no bytes')
79
+ });
65
80
  /** What a result carries beyond its outcome, as counts and short facts. Present only when there is something. */
66
81
  export const EvidenceSummarySchema = z.looseObject({
67
82
  attachments: z.number().int().nonnegative(),
@@ -72,7 +87,8 @@ export const EvidenceSummarySchema = z.looseObject({
72
87
  tags: z.array(z.string()),
73
88
  artifactTags: z.array(z.string()).optional().describe('Normalized tags of the attachments themselves, sorted and distinct. Absent when no attachment is tagged; never inherited from a check or run'),
74
89
  labels: z.record(z.string(), z.string()),
75
- roles: z.array(z.string())
90
+ roles: z.array(z.string()),
91
+ videos: z.array(VideoRefSchema).optional().describe('Its references to videos and who holds each. Absent when it has none, and in reports written before this was recorded')
76
92
  });
77
93
  /** A layer as the report shows it: its origin and its checks by status. */
78
94
  export const LayerHeaderSchema = z.looseObject({
@@ -374,10 +390,17 @@ export const ReportRunSchema = z.looseObject({
374
390
  producers: z.array(z.string()), environments: z.array(z.string()),
375
391
  layers: z.number().int().nonnegative(), tests: z.number().int().nonnegative(), counts: StatusCountsSchema,
376
392
  totalDurationMs: z.number().nullable(),
377
- failing: z.array(z.looseObject({ key: z.string(), title: z.string(), environment: z.string().nullable(), detail: z.string() }))
393
+ failing: z.array(z.looseObject({ key: z.string(), title: z.string(), environment: z.string().nullable(), detail: z.string() })),
394
+ videos: VideoCountsSchema.optional().describe('Its videos, from its layers and the last result of each test. Absent when it has none or the report carries no video information')
378
395
  });
379
396
  /** `GET /api/reports/runs`: runs, newest first. */
380
- export const RunListSchema = z.looseObject({ schemaVersion: z.literal(1), version: z.string(), total: z.number().int().nonnegative(), runs: z.array(ReportRunSchema) });
397
+ export const RunListSchema = z.looseObject({
398
+ schemaVersion: z.literal(1),
399
+ version: z.string(),
400
+ total: z.number().int().nonnegative().describe('Runs that matched, before `limit`'),
401
+ runs: z.array(ReportRunSchema),
402
+ withVideo: z.number().int().nonnegative().optional().describe('How many of all runs (before `video` and `limit`) have a playable video recording')
403
+ });
381
404
  /** `GET /api/reports/runs/detail`: one run and its last result for every test, environment and producer it recorded. */
382
405
  export const RunDetailSchema = z.looseObject({
383
406
  schemaVersion: z.literal(1),
@@ -385,6 +408,7 @@ export const RunDetailSchema = z.looseObject({
385
408
  run: ReportRunSchema,
386
409
  results: z.array(z.looseObject({
387
410
  key: z.string(), id: z.string(), title: z.string(), specPath: z.string().nullable(), environment: z.string().nullable(), producer: z.string(),
388
- status: EvidenceStatusSchema, durationMs: z.number().nullable(), at: z.number(), layer: z.string(), detail: z.string()
411
+ status: EvidenceStatusSchema, durationMs: z.number().nullable(), at: z.number(), layer: z.string(), detail: z.string(),
412
+ videos: z.array(VideoRefSchema).optional().describe('Every video its evidence references, with who holds it')
389
413
  }))
390
414
  });
@@ -426,8 +426,9 @@ function createProjectApplication(options) {
426
426
  return notReady(c);
427
427
  const found = await catalog();
428
428
  const limit = /^\d+$/.test(c.req.query('limit') ?? '') ? Number(c.req.query('limit')) : 100;
429
- const document = () => runList(index.history, found.version, limit);
430
- return answer(c, found.version, document, () => runsMarkdown(document()), `runs|${limit}|${wantsMarkdown(c)}`);
429
+ const video = ['1', 'true'].includes(c.req.query('video') ?? '');
430
+ const document = () => runList(index.history, found.version, limit, { video });
431
+ return answer(c, found.version, document, () => runsMarkdown(document()), `runs|${limit}|${video}|${wantsMarkdown(c)}`);
431
432
  });
432
433
  app.get('/api/reports/runs/detail', async (c) => {
433
434
  if (!index.ready)
@@ -69,7 +69,8 @@ curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests?format=markdown&limit=50"
69
69
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests?status=failed,flaky&environment=desktop-web" # filter: q, status, environment, producer, domain, spec, tag, run; sort, offset, limit
70
70
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests/history?key=<url-encoded test key>" # one test: every recorded result, durations, branches, CI runs
71
71
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests/health?format=markdown" # why Specs, domains and counts disagree; add root=<from>><to> to declare a path-root mapping
72
- curl "${url}/api/projects/$ATLAS_PROJECT/reports/runs?format=markdown" # CI runs, newest first, with their results by status
72
+ curl "${url}/api/projects/$ATLAS_PROJECT/reports/runs?format=markdown" # CI runs, newest first, with their results by status and video counts
73
+ curl "${url}/api/projects/$ATLAS_PROJECT/reports/runs?video=true&format=markdown" # only runs with a playable video recording
73
74
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/runs/detail?key=<url-encoded run key>" # every test one run recorded
74
75
  \`\`\`
75
76
 
@@ -324,9 +324,10 @@ const BASE_OPERATIONS = [
324
324
  {
325
325
  route: 'GET /api/reports/runs', path: '/api/reports/runs', operationId: 'getReportRuns', tag: 'Report', role: 'read',
326
326
  summary: 'The runs that recorded the report',
327
- 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 and up to twenty failures. Send `format=markdown` for Markdown.',
327
+ 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.',
328
328
  parameters: [
329
329
  { name: 'limit', in: 'query', description: 'At most this many runs (default 100).', schema: { type: 'integer', minimum: 0 } },
330
+ { 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' } },
330
331
  { name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
331
332
  ],
332
333
  responses: { '200': json('The runs.', 'RunList'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '401': UNAUTHORIZED, '503': NOT_READY }
@@ -616,9 +616,10 @@ Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `A
616
616
 
617
617
  `GET /api/reports/runs` · role: read
618
618
 
619
- 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 and up to twenty failures. Send `format=markdown` for Markdown.
619
+ 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.
620
620
 
621
621
  - `limit` (query): At most this many runs (default 100).
622
+ - `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.
622
623
  - `format` (query): `markdown` for Markdown instead of JSON.
623
624
  - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
624
625
 
@@ -628,12 +629,13 @@ Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `40
628
629
 
629
630
  `GET /api/projects/{project}/reports/runs` · role: read
630
631
 
631
- 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 and up to twenty failures. Send `format=markdown` for Markdown.
632
+ 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.
632
633
 
633
634
  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.
634
635
 
635
636
  - `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.
636
637
  - `limit` (query): At most this many runs (default 100).
638
+ - `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.
637
639
  - `format` (query): `markdown` for Markdown instead of JSON.
638
640
 
639
641
  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`.
@@ -1426,7 +1426,36 @@ read when someone asks for it. Undefined when there is none.
1426
1426
  Several holders (for example a check and its layer) are summed: `attachments` also counts those on steps and
1427
1427
  attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
1428
1428
  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
1429
- name implies), sorted and distinct.
1429
+ name implies), sorted and distinct. `videos` lists every attachment whose effective media type is video, with the
1430
+ check, retry attempt or step that holds it (see {@link isVideoAttachment}).
1431
+
1432
+ #### `summarizeLayerEvidence`
1433
+
1434
+ ```ts
1435
+ export function summarizeLayerEvidence(...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined
1436
+ ```
1437
+
1438
+ Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
1439
+ part is owned by the `layer`, not by a check.
1440
+
1441
+ #### `isVideoAttachment`
1442
+
1443
+ ```ts
1444
+ export function isVideoAttachment(attachment: Pick<Attachment, 'name' | 'role' | 'contentType'>): boolean
1445
+ ```
1446
+
1447
+ Whether an attachment is a video, by its effective media type: the producer's `contentType`, else the one its file
1448
+ name implies. A role never decides this on its own (`demo` on a PNG is not a video), except that a `video` role
1449
+ settles a file whose type nothing says (`application/octet-stream`).
1450
+
1451
+ #### `videoCounts`
1452
+
1453
+ ```ts
1454
+ export function videoCounts(videos: readonly VideoRef[] | undefined): VideoCounts | undefined
1455
+ ```
1456
+
1457
+ Counts references, distinct playable recordings and unplayable references. Undefined when there is no reference, so
1458
+ "no video" and "nothing known about video" look the same on the wire.
1430
1459
 
1431
1460
  #### `roleOfType`
1432
1461
 
@@ -1609,6 +1638,51 @@ export type StatusCounts = Readonly<Record<EvidenceStatus, number>>;
1609
1638
 
1610
1639
  How many results have each status (`passed`, `failed`, `running`, `not-run`).
1611
1640
 
1641
+ #### `VideoOwner`
1642
+
1643
+ ```ts
1644
+ export type VideoOwner = 'layer' | 'check' | 'attempt' | 'step';
1645
+ ```
1646
+
1647
+ Who holds a recording: the whole layer (or one of its parts), one check, one retry attempt of a check, or one step.
1648
+
1649
+ #### `VideoRef`
1650
+
1651
+ ```ts
1652
+ export type VideoRef = {
1653
+ /** The attachment's name, for a person. */
1654
+ readonly name: string;
1655
+ readonly owner: VideoOwner;
1656
+ /** The try that holds it, when `owner` is `attempt` or the step is inside one. */
1657
+ readonly attempt?: number;
1658
+ /** The name of the step that holds it, when `owner` is `step`. */
1659
+ readonly step?: string;
1660
+ /** What identifies the bytes (digest, bundle key or address); references with the same one are the same recording. Absent when `available` is false. */
1661
+ readonly recording?: string;
1662
+ /** False when the reference names no bytes at all (no bundle key, address or inline text), so it can never be played. */
1663
+ readonly available: boolean;
1664
+ };
1665
+ ```
1666
+
1667
+ One reference to a video, found by its effective media type (the producer's `contentType`, else the one the file
1668
+ name implies; a `video` role alone counts only when nothing else says what the file is). A reference is not a
1669
+ recording: the same bytes referenced from three checks are three references and one recording.
1670
+
1671
+ #### `VideoCounts`
1672
+
1673
+ ```ts
1674
+ export type VideoCounts = {
1675
+ /** Every reference to a video, playable or not. */
1676
+ readonly references: number;
1677
+ /** Distinct playable recordings: identical bytes referenced twice are one. */
1678
+ readonly recordings: number;
1679
+ /** References that name no bytes. A report that predates availability metadata has none of these and none of the others. */
1680
+ readonly unavailable: number;
1681
+ };
1682
+ ```
1683
+
1684
+ Video availability as counts: references, distinct playable recordings (deduplicated), and references that cannot be played.
1685
+
1612
1686
  #### `EvidenceSummary`
1613
1687
 
1614
1688
  ```ts
@@ -1626,6 +1700,8 @@ export type EvidenceSummary = {
1626
1700
  readonly labels: Readonly<Record<string, string>>;
1627
1701
  /** The distinct roles of its attachments (or what their media types say), to show what kind of evidence there is. */
1628
1702
  readonly roles: readonly string[];
1703
+ /** Its references to videos, wherever they are held (layer, check, attempt, step). Only there when it has any. */
1704
+ readonly videos?: readonly VideoRef[];
1629
1705
  };
1630
1706
  ```
1631
1707
 
@@ -7930,20 +8006,39 @@ export type RunSummary = {
7930
8006
  readonly totalDurationMs: number | null;
7931
8007
  /** Up to twenty tests that failed in it. */
7932
8008
  readonly failing: readonly { readonly key: string; readonly title: string; readonly environment: string | null; readonly detail: string }[];
8009
+ /**
8010
+ * Its videos, from the run's layers and from the last result of each test: references, distinct playable recordings
8011
+ * and references that cannot be played. Absent when it has none, and in a report that carries no video information.
8012
+ */
8013
+ readonly videos?: VideoCounts;
7933
8014
  };
7934
8015
  ```
7935
8016
 
7936
8017
  A run: the layers that name the same execution (`run.id`), else share a CI run URL, else a branch and commit. When,
7937
8018
  where, and how its tests went.
7938
8019
 
8020
+ #### `RunQuery`
8021
+
8022
+ ```ts
8023
+ export type RunQuery = {
8024
+ /** Only runs that have at least one playable video recording. `total` then counts only those. */
8025
+ readonly video?: boolean;
8026
+ };
8027
+ ```
8028
+
8029
+ What `runList` can be asked.
8030
+
7939
8031
  #### `RunList`
7940
8032
 
7941
8033
  ```ts
7942
8034
  export type RunList = {
7943
8035
  readonly schemaVersion: typeof TEST_CATALOG_VERSION;
7944
8036
  readonly version: string;
8037
+ /** Runs that matched, before `limit`. */
7945
8038
  readonly total: number;
7946
8039
  readonly runs: readonly RunSummary[];
8040
+ /** How many of all runs (whatever was asked, before `limit`) have a playable video recording. */
8041
+ readonly withVideo?: number;
7947
8042
  };
7948
8043
  ```
7949
8044
 
@@ -7964,6 +8059,8 @@ export type RunResult = {
7964
8059
  readonly at: number;
7965
8060
  readonly layer: string;
7966
8061
  readonly detail: string;
8062
+ /** Every video its evidence references, with who holds it (check, retry attempt or step). Absent when there is none. */
8063
+ readonly videos?: readonly VideoRef[];
7967
8064
  };
7968
8065
  ```
7969
8066
 
@@ -8114,13 +8211,23 @@ export function testHistory(model: CatalogModel, key: string): TestHistoryDocume
8114
8211
 
8115
8212
  One test and its recorded results, or null when the report has no test with that key.
8116
8213
 
8214
+ #### `hasVideo`
8215
+
8216
+ ```ts
8217
+ export function hasVideo(run: Pick<RunSummary, 'videos'>): boolean
8218
+ ```
8219
+
8220
+ Whether a run has at least one playable video recording.
8221
+
8117
8222
  #### `runList`
8118
8223
 
8119
8224
  ```ts
8120
- export function runList(history: TestHistory, version: string, limit = 100): RunList
8225
+ export function runList(history: TestHistory, version: string, limit = 100, query: RunQuery = {}): RunList
8121
8226
  ```
8122
8227
 
8123
- Every run the history knows of, newest first, `limit` at most (default 100).
8228
+ Every run the history knows of, newest first, `limit` at most (default 100). With `query.video` only the runs that
8229
+ have a playable video recording: the filter and `total` apply to every run before `limit`, and nothing is read from
8230
+ any recording's bytes. `withVideo` always counts every run that has one.
8124
8231
 
8125
8232
  #### `runDetail`
8126
8233
 
@@ -69,6 +69,22 @@ export const BackendInfoSchema = …
69
69
 
70
70
  A backend as the report shows it: where results came from, and whether it could be read.
71
71
 
72
+ #### `VideoRefSchema`
73
+
74
+ ```ts
75
+ export const VideoRefSchema = …
76
+ ```
77
+
78
+ One reference to a video held by a layer, check, retry attempt or step, found by the attachment's effective media type.
79
+
80
+ #### `VideoCountsSchema`
81
+
82
+ ```ts
83
+ export const VideoCountsSchema = …
84
+ ```
85
+
86
+ Video references as counts: how many are referenced, how many distinct recordings can be played, how many cannot.
87
+
72
88
  #### `EvidenceSummarySchema`
73
89
 
74
90
  ```ts
package/docs/server.md CHANGED
@@ -276,7 +276,7 @@ with `format=markdown` (or `Accept: text/markdown`), and takes the project prefi
276
276
  | Only some: `q` (all words), `status` (or `flaky`), `environment`, `producer`, `domain`, `spec`, `tag`, `run`; `sort`, `offset`, `limit` | `GET /api/reports/tests?status=failed&environment=ios` |
277
277
  | One test: every recorded result (status, duration, environment, branch, commit, CI run), newest first | `GET /api/reports/tests/history?key=<key>` |
278
278
  | Why Specs, domains and counts disagree: ambiguous spec paths, shared domains, gaps, a reconciliation of the totals | `GET /api/reports/tests/health` |
279
- | The runs that recorded the report (by CI run URL, else branch and commit) | `GET /api/reports/runs` |
279
+ | The runs that recorded the report (by CI run URL, else branch and commit); `?video=true` keeps those with a playable video | `GET /api/reports/runs` |
280
280
  | Every test one run recorded | `GET /api/reports/runs/detail?key=<run key>` |
281
281
 
282
282
  ```sh