@crvouga/atlas 0.14.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 (46) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/reports/src/artifact-tag-store.d.ts +20 -0
  3. package/dist/reports/src/artifact-tag-store.js +70 -0
  4. package/dist/reports/src/artifact-tags.d.ts +96 -0
  5. package/dist/reports/src/artifact-tags.js +96 -0
  6. package/dist/reports/src/evidence-detail.d.ts +20 -3
  7. package/dist/reports/src/evidence-detail.js +66 -13
  8. package/dist/reports/src/evidence-types.d.ts +33 -0
  9. package/dist/reports/src/layer-merge.js +2 -2
  10. package/dist/reports/src/reports-explorer-route.d.ts +8 -0
  11. package/dist/reports/src/reports-route.d.ts +8 -0
  12. package/dist/reports/src/test-catalog.d.ts +35 -26
  13. package/dist/reports/src/test-catalog.js +79 -15
  14. package/dist/schema/src/api.d.ts +246 -0
  15. package/dist/schema/src/api.js +59 -5
  16. package/dist/schema/src/layer.d.ts +33 -0
  17. package/dist/schema/src/layer.js +11 -1
  18. package/dist/server/src/app.js +35 -3
  19. package/dist/server/src/llms.js +2 -1
  20. package/dist/server/src/openapi.d.ts +1 -1
  21. package/dist/server/src/openapi.js +20 -3
  22. package/docs/extending.md +3 -0
  23. package/docs/reference/http-api.md +52 -2
  24. package/docs/reference/reports.md +327 -5
  25. package/docs/reference/schema.md +48 -0
  26. package/docs/rich-evidence.md +28 -0
  27. package/docs/server.md +25 -1
  28. package/llms-full.txt +108 -3
  29. package/package.json +1 -1
  30. package/packages/schema/schemas/check-detail.schema.json +28 -0
  31. package/packages/schema/schemas/effective-delta.schema.json +96 -0
  32. package/packages/schema/schemas/effective-evidence.schema.json +96 -0
  33. package/packages/schema/schemas/evidence-page.schema.json +48 -0
  34. package/packages/schema/schemas/layer-rows.schema.json +48 -0
  35. package/packages/schema/schemas/layer.schema.json +70 -0
  36. package/packages/schema/schemas/publish-checks.schema.json +35 -0
  37. package/packages/schema/schemas/report-run.schema.json +71 -0
  38. package/packages/schema/schemas/report-runs.schema.json +38 -1
  39. package/packages/schema/schemas/test-catalog.schema.json +83 -0
  40. package/packages/schema/schemas/test-history.schema.json +110 -0
  41. package/packages/server/openapi.json +1259 -33
  42. package/packages/visualizer/src/evidence/artifact-tags.tsx +285 -0
  43. package/packages/visualizer/src/evidence/artifacts.tsx +18 -12
  44. package/packages/visualizer/src/explorer/RunsView.tsx +128 -10
  45. package/packages/visualizer/src/explorer/catalog.tsx +16 -6
  46. package/packages/visualizer/src/explorer/search.ts +4 -1
@@ -121,6 +121,7 @@ export declare const AttachmentSchema: z.ZodObject<{
121
121
  height: z.ZodOptional<z.ZodNumber>;
122
122
  durationMs: z.ZodOptional<z.ZodNumber>;
123
123
  description: z.ZodOptional<z.ZodString>;
124
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
124
125
  at: z.ZodOptional<z.ZodObject<{
125
126
  step: z.ZodOptional<z.ZodString>;
126
127
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -181,6 +182,7 @@ export declare const StepSchema: z.ZodObject<{
181
182
  height: z.ZodOptional<z.ZodNumber>;
182
183
  durationMs: z.ZodOptional<z.ZodNumber>;
183
184
  description: z.ZodOptional<z.ZodString>;
185
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
184
186
  at: z.ZodOptional<z.ZodObject<{
185
187
  step: z.ZodOptional<z.ZodString>;
186
188
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -248,6 +250,7 @@ export declare const AttemptSchema: z.ZodObject<{
248
250
  height: z.ZodOptional<z.ZodNumber>;
249
251
  durationMs: z.ZodOptional<z.ZodNumber>;
250
252
  description: z.ZodOptional<z.ZodString>;
253
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
251
254
  at: z.ZodOptional<z.ZodObject<{
252
255
  step: z.ZodOptional<z.ZodString>;
253
256
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -292,6 +295,7 @@ export declare const AttemptSchema: z.ZodObject<{
292
295
  height: z.ZodOptional<z.ZodNumber>;
293
296
  durationMs: z.ZodOptional<z.ZodNumber>;
294
297
  description: z.ZodOptional<z.ZodString>;
298
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
295
299
  at: z.ZodOptional<z.ZodObject<{
296
300
  step: z.ZodOptional<z.ZodString>;
297
301
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -355,6 +359,7 @@ export declare const CheckSchema: z.ZodObject<{
355
359
  height: z.ZodOptional<z.ZodNumber>;
356
360
  durationMs: z.ZodOptional<z.ZodNumber>;
357
361
  description: z.ZodOptional<z.ZodString>;
362
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
358
363
  at: z.ZodOptional<z.ZodObject<{
359
364
  step: z.ZodOptional<z.ZodString>;
360
365
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -419,6 +424,7 @@ export declare const CheckSchema: z.ZodObject<{
419
424
  height: z.ZodOptional<z.ZodNumber>;
420
425
  durationMs: z.ZodOptional<z.ZodNumber>;
421
426
  description: z.ZodOptional<z.ZodString>;
427
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
422
428
  at: z.ZodOptional<z.ZodObject<{
423
429
  step: z.ZodOptional<z.ZodString>;
424
430
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -463,6 +469,7 @@ export declare const CheckSchema: z.ZodObject<{
463
469
  height: z.ZodOptional<z.ZodNumber>;
464
470
  durationMs: z.ZodOptional<z.ZodNumber>;
465
471
  description: z.ZodOptional<z.ZodString>;
472
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
466
473
  at: z.ZodOptional<z.ZodObject<{
467
474
  step: z.ZodOptional<z.ZodString>;
468
475
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -514,6 +521,7 @@ export declare const CheckSchema: z.ZodObject<{
514
521
  height: z.ZodOptional<z.ZodNumber>;
515
522
  durationMs: z.ZodOptional<z.ZodNumber>;
516
523
  description: z.ZodOptional<z.ZodString>;
524
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
517
525
  at: z.ZodOptional<z.ZodObject<{
518
526
  step: z.ZodOptional<z.ZodString>;
519
527
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -600,6 +608,7 @@ export declare const ReportPartSchema: z.ZodObject<{
600
608
  height: z.ZodOptional<z.ZodNumber>;
601
609
  durationMs: z.ZodOptional<z.ZodNumber>;
602
610
  description: z.ZodOptional<z.ZodString>;
611
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
603
612
  at: z.ZodOptional<z.ZodObject<{
604
613
  step: z.ZodOptional<z.ZodString>;
605
614
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -663,6 +672,7 @@ export declare const ReportPartSchema: z.ZodObject<{
663
672
  height: z.ZodOptional<z.ZodNumber>;
664
673
  durationMs: z.ZodOptional<z.ZodNumber>;
665
674
  description: z.ZodOptional<z.ZodString>;
675
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
666
676
  at: z.ZodOptional<z.ZodObject<{
667
677
  step: z.ZodOptional<z.ZodString>;
668
678
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -727,6 +737,7 @@ export declare const ReportPartSchema: z.ZodObject<{
727
737
  height: z.ZodOptional<z.ZodNumber>;
728
738
  durationMs: z.ZodOptional<z.ZodNumber>;
729
739
  description: z.ZodOptional<z.ZodString>;
740
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
730
741
  at: z.ZodOptional<z.ZodObject<{
731
742
  step: z.ZodOptional<z.ZodString>;
732
743
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -771,6 +782,7 @@ export declare const ReportPartSchema: z.ZodObject<{
771
782
  height: z.ZodOptional<z.ZodNumber>;
772
783
  durationMs: z.ZodOptional<z.ZodNumber>;
773
784
  description: z.ZodOptional<z.ZodString>;
785
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
774
786
  at: z.ZodOptional<z.ZodObject<{
775
787
  step: z.ZodOptional<z.ZodString>;
776
788
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -822,6 +834,7 @@ export declare const ReportPartSchema: z.ZodObject<{
822
834
  height: z.ZodOptional<z.ZodNumber>;
823
835
  durationMs: z.ZodOptional<z.ZodNumber>;
824
836
  description: z.ZodOptional<z.ZodString>;
837
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
825
838
  at: z.ZodOptional<z.ZodObject<{
826
839
  step: z.ZodOptional<z.ZodString>;
827
840
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -910,6 +923,7 @@ export declare const LayerSourceSchema: z.ZodObject<{
910
923
  height: z.ZodOptional<z.ZodNumber>;
911
924
  durationMs: z.ZodOptional<z.ZodNumber>;
912
925
  description: z.ZodOptional<z.ZodString>;
926
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
913
927
  at: z.ZodOptional<z.ZodObject<{
914
928
  step: z.ZodOptional<z.ZodString>;
915
929
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -974,6 +988,7 @@ export declare const LayerSourceSchema: z.ZodObject<{
974
988
  height: z.ZodOptional<z.ZodNumber>;
975
989
  durationMs: z.ZodOptional<z.ZodNumber>;
976
990
  description: z.ZodOptional<z.ZodString>;
991
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
977
992
  at: z.ZodOptional<z.ZodObject<{
978
993
  step: z.ZodOptional<z.ZodString>;
979
994
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1018,6 +1033,7 @@ export declare const LayerSourceSchema: z.ZodObject<{
1018
1033
  height: z.ZodOptional<z.ZodNumber>;
1019
1034
  durationMs: z.ZodOptional<z.ZodNumber>;
1020
1035
  description: z.ZodOptional<z.ZodString>;
1036
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1021
1037
  at: z.ZodOptional<z.ZodObject<{
1022
1038
  step: z.ZodOptional<z.ZodString>;
1023
1039
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1069,6 +1085,7 @@ export declare const LayerSourceSchema: z.ZodObject<{
1069
1085
  height: z.ZodOptional<z.ZodNumber>;
1070
1086
  durationMs: z.ZodOptional<z.ZodNumber>;
1071
1087
  description: z.ZodOptional<z.ZodString>;
1088
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1072
1089
  at: z.ZodOptional<z.ZodObject<{
1073
1090
  step: z.ZodOptional<z.ZodString>;
1074
1091
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1183,6 +1200,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1183
1200
  height: z.ZodOptional<z.ZodNumber>;
1184
1201
  durationMs: z.ZodOptional<z.ZodNumber>;
1185
1202
  description: z.ZodOptional<z.ZodString>;
1203
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1186
1204
  at: z.ZodOptional<z.ZodObject<{
1187
1205
  step: z.ZodOptional<z.ZodString>;
1188
1206
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1260,6 +1278,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1260
1278
  height: z.ZodOptional<z.ZodNumber>;
1261
1279
  durationMs: z.ZodOptional<z.ZodNumber>;
1262
1280
  description: z.ZodOptional<z.ZodString>;
1281
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1263
1282
  at: z.ZodOptional<z.ZodObject<{
1264
1283
  step: z.ZodOptional<z.ZodString>;
1265
1284
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1323,6 +1342,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1323
1342
  height: z.ZodOptional<z.ZodNumber>;
1324
1343
  durationMs: z.ZodOptional<z.ZodNumber>;
1325
1344
  description: z.ZodOptional<z.ZodString>;
1345
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1326
1346
  at: z.ZodOptional<z.ZodObject<{
1327
1347
  step: z.ZodOptional<z.ZodString>;
1328
1348
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1387,6 +1407,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1387
1407
  height: z.ZodOptional<z.ZodNumber>;
1388
1408
  durationMs: z.ZodOptional<z.ZodNumber>;
1389
1409
  description: z.ZodOptional<z.ZodString>;
1410
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1390
1411
  at: z.ZodOptional<z.ZodObject<{
1391
1412
  step: z.ZodOptional<z.ZodString>;
1392
1413
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1431,6 +1452,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1431
1452
  height: z.ZodOptional<z.ZodNumber>;
1432
1453
  durationMs: z.ZodOptional<z.ZodNumber>;
1433
1454
  description: z.ZodOptional<z.ZodString>;
1455
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1434
1456
  at: z.ZodOptional<z.ZodObject<{
1435
1457
  step: z.ZodOptional<z.ZodString>;
1436
1458
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1482,6 +1504,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1482
1504
  height: z.ZodOptional<z.ZodNumber>;
1483
1505
  durationMs: z.ZodOptional<z.ZodNumber>;
1484
1506
  description: z.ZodOptional<z.ZodString>;
1507
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1485
1508
  at: z.ZodOptional<z.ZodObject<{
1486
1509
  step: z.ZodOptional<z.ZodString>;
1487
1510
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1569,6 +1592,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1569
1592
  height: z.ZodOptional<z.ZodNumber>;
1570
1593
  durationMs: z.ZodOptional<z.ZodNumber>;
1571
1594
  description: z.ZodOptional<z.ZodString>;
1595
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1572
1596
  at: z.ZodOptional<z.ZodObject<{
1573
1597
  step: z.ZodOptional<z.ZodString>;
1574
1598
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1633,6 +1657,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1633
1657
  height: z.ZodOptional<z.ZodNumber>;
1634
1658
  durationMs: z.ZodOptional<z.ZodNumber>;
1635
1659
  description: z.ZodOptional<z.ZodString>;
1660
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1636
1661
  at: z.ZodOptional<z.ZodObject<{
1637
1662
  step: z.ZodOptional<z.ZodString>;
1638
1663
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1677,6 +1702,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1677
1702
  height: z.ZodOptional<z.ZodNumber>;
1678
1703
  durationMs: z.ZodOptional<z.ZodNumber>;
1679
1704
  description: z.ZodOptional<z.ZodString>;
1705
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1680
1706
  at: z.ZodOptional<z.ZodObject<{
1681
1707
  step: z.ZodOptional<z.ZodString>;
1682
1708
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1728,6 +1754,7 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1728
1754
  height: z.ZodOptional<z.ZodNumber>;
1729
1755
  durationMs: z.ZodOptional<z.ZodNumber>;
1730
1756
  description: z.ZodOptional<z.ZodString>;
1757
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1731
1758
  at: z.ZodOptional<z.ZodObject<{
1732
1759
  step: z.ZodOptional<z.ZodString>;
1733
1760
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1855,6 +1882,7 @@ export declare const AttachmentInputSchema: z.ZodObject<{
1855
1882
  height: z.ZodOptional<z.ZodNumber>;
1856
1883
  durationMs: z.ZodOptional<z.ZodNumber>;
1857
1884
  description: z.ZodOptional<z.ZodString>;
1885
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1858
1886
  at: z.ZodOptional<z.ZodObject<{
1859
1887
  step: z.ZodOptional<z.ZodString>;
1860
1888
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1935,6 +1963,7 @@ export declare const PublishChecksSchema: z.ZodObject<{
1935
1963
  height: z.ZodOptional<z.ZodNumber>;
1936
1964
  durationMs: z.ZodOptional<z.ZodNumber>;
1937
1965
  description: z.ZodOptional<z.ZodString>;
1966
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1938
1967
  at: z.ZodOptional<z.ZodObject<{
1939
1968
  step: z.ZodOptional<z.ZodString>;
1940
1969
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -1990,6 +2019,7 @@ export declare const PublishChecksSchema: z.ZodObject<{
1990
2019
  height: z.ZodOptional<z.ZodNumber>;
1991
2020
  durationMs: z.ZodOptional<z.ZodNumber>;
1992
2021
  description: z.ZodOptional<z.ZodString>;
2022
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1993
2023
  at: z.ZodOptional<z.ZodObject<{
1994
2024
  step: z.ZodOptional<z.ZodString>;
1995
2025
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -2078,6 +2108,7 @@ export declare const PublishChecksSchema: z.ZodObject<{
2078
2108
  height: z.ZodOptional<z.ZodNumber>;
2079
2109
  durationMs: z.ZodOptional<z.ZodNumber>;
2080
2110
  description: z.ZodOptional<z.ZodString>;
2111
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2081
2112
  at: z.ZodOptional<z.ZodObject<{
2082
2113
  step: z.ZodOptional<z.ZodString>;
2083
2114
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -2104,6 +2135,7 @@ export declare const PublishChecksSchema: z.ZodObject<{
2104
2135
  height: z.ZodOptional<z.ZodNumber>;
2105
2136
  durationMs: z.ZodOptional<z.ZodNumber>;
2106
2137
  description: z.ZodOptional<z.ZodString>;
2138
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2107
2139
  at: z.ZodOptional<z.ZodObject<{
2108
2140
  step: z.ZodOptional<z.ZodString>;
2109
2141
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -2163,6 +2195,7 @@ export declare const PublishChecksSchema: z.ZodObject<{
2163
2195
  height: z.ZodOptional<z.ZodNumber>;
2164
2196
  durationMs: z.ZodOptional<z.ZodNumber>;
2165
2197
  description: z.ZodOptional<z.ZodString>;
2198
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2166
2199
  at: z.ZodOptional<z.ZodObject<{
2167
2200
  step: z.ZodOptional<z.ZodString>;
2168
2201
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -109,6 +109,7 @@ export const AttachmentSchema = z.looseObject({
109
109
  height: z.number().nonnegative().optional(),
110
110
  durationMs: z.number().nonnegative().optional().describe('For video and audio'),
111
111
  description: z.string().optional(),
112
+ 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'),
112
113
  at: MomentSchema.optional(),
113
114
  poster: z.string().optional().describe('The `id` of an attachment that stands for this one before it is opened'),
114
115
  captions: z.array(z.string()).optional().describe('The `id`s of attachments (WebVTT, SRT) that caption this one'),
@@ -285,6 +286,15 @@ function lenientList(value, schema, prepare = (item) => item) {
285
286
  });
286
287
  }
287
288
  const object = (value) => (typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null);
289
+ /** An attachment whose `tags` cannot be read keeps its file: only the tags that are strings are kept. */
290
+ function lenientAttachment(item) {
291
+ const attachment = object(item);
292
+ if (!attachment || !('tags' in attachment))
293
+ return item;
294
+ const { tags, ...rest } = attachment;
295
+ const kept = lenientList(tags, z.string());
296
+ return kept ? { ...rest, tags: kept } : rest;
297
+ }
288
298
  function lenientEvidence(holder, nested = []) {
289
299
  const out = { ...holder };
290
300
  const set = (key, value) => { if (value === undefined)
@@ -296,7 +306,7 @@ function lenientEvidence(holder, nested = []) {
296
306
  if ('links' in out)
297
307
  set('links', lenientList(out.links, LinkSchema));
298
308
  if ('attachments' in out)
299
- set('attachments', lenientList(out.attachments, AttachmentSchema));
309
+ set('attachments', lenientList(out.attachments, AttachmentSchema, lenientAttachment));
300
310
  if ('annotations' in out)
301
311
  set('annotations', lenientList(out.annotations, AnnotationSchema));
302
312
  if ('labels' in out)
@@ -20,6 +20,7 @@ import { combineBackends, summarize } from '../../reports/src/compose.js';
20
20
  import { combineEvidence } from '../../reports/src/layer-merge.js';
21
21
  import { layerCatalogKey } from '../../reports/src/layer-store.js';
22
22
  import { createLayersApp } from '../../reports/src/layers-route.js';
23
+ import { readArtifactTagEntries, recordArtifactTags, ArtifactTagError } from '../../reports/src/artifact-tag-store.js';
23
24
  import { publishChecks } from '../../reports/src/publish-checks.js';
24
25
  import { reportStatus } from '../../reports/src/report-status.js';
25
26
  import { ReportIndex } from '../../reports/src/report-index.js';
@@ -254,7 +255,7 @@ function createProjectApplication(options) {
254
255
  name: 'atlas', version: options.version ?? null, role: identity?.role ?? 'read', open: auth.open,
255
256
  write: options.write, ready: index.ready, viewers: events.count, time: now(),
256
257
  backends: snapshot.backends.map(({ id, label, available }) => ({ id, label, available, write: id === options.write })),
257
- ...(({ backends: _unused, ...rest }) => rest)(index.stats()), capabilities: { events: true, ingest: true, store: true, checks: true, status: true, effective: true, live: true, tests: true }
258
+ ...(({ 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 }
258
259
  });
259
260
  });
260
261
  app.get('/api/live', async (c) => c.json({ runs: await live.list() }, 200, { 'cache-control': 'no-store' }));
@@ -425,8 +426,9 @@ function createProjectApplication(options) {
425
426
  return notReady(c);
426
427
  const found = await catalog();
427
428
  const limit = /^\d+$/.test(c.req.query('limit') ?? '') ? Number(c.req.query('limit')) : 100;
428
- const document = () => runList(index.history, found.version, limit);
429
- 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)}`);
430
432
  });
431
433
  app.get('/api/reports/runs/detail', async (c) => {
432
434
  if (!index.ready)
@@ -440,6 +442,36 @@ function createProjectApplication(options) {
440
442
  return problemResponse(404, 'No run has that key.');
441
443
  return answer(c, found.version, () => document, () => runMarkdown(document), `run|${key}|${wantsMarkdown(c)}`);
442
444
  });
445
+ // Reviewer tags for attachments: an audit trail of edits kept beside the layers, which they never change. Read
446
+ // with a read token; an edit needs a publish token in a Bearer header, and is recorded under that token's name.
447
+ const tagStore = options.liveStore ?? writeBackend.store;
448
+ // Edits are ordered by time, so two in one millisecond are told apart by moving the later one on.
449
+ let lastTagAt = 0;
450
+ const artifactTags = cached(() => readArtifactTagEntries(tagStore), 5_000);
451
+ app.get('/api/reports/artifact-tags', async (c) => c.json({ schemaVersion: 1, entries: await artifactTags() }, 200, { 'cache-control': 'no-store' }));
452
+ app.post('/api/reports/artifact-tags', async (c) => {
453
+ const verdict = auth.check(c.req.raw, 'publish');
454
+ if (!verdict.ok)
455
+ return unauthorized(verdict);
456
+ try {
457
+ const bytes = await readLimited(c.req.raw, Math.min(limits.json, 64 * 1024));
458
+ let input;
459
+ try {
460
+ input = JSON.parse(new TextDecoder().decode(bytes));
461
+ }
462
+ catch {
463
+ return problemResponse(422, 'The body is not JSON.');
464
+ }
465
+ const entry = await recordArtifactTags(tagStore, input, verdict.identity.name, lastTagAt = Math.max(now(), lastTagAt + 1));
466
+ await artifactTags.invalidate();
467
+ return c.json(entry, 201, { 'cache-control': 'no-store' });
468
+ }
469
+ catch (error) {
470
+ if (error instanceof ArtifactTagError || error instanceof Refusal)
471
+ return problemResponse(error.status, error.message);
472
+ throw error;
473
+ }
474
+ });
443
475
  app.route('/api/reports', layers);
444
476
  // Upload: the store protocol, and a simple JSON way in for anything that can make a request.
445
477
  app.route('/api/store', storeApi({
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  /** The version of the API described, `major.minor.patch`: the minor grows when an operation or field is added, the major never within `/api`. */
2
- export declare const API_VERSION = "1.4.0";
2
+ export declare const API_VERSION = "1.5.0";
3
3
  /** Who may call an operation: `none` (anyone), or the least role that may (`read` < `publish` < `admin`). */
4
4
  export type Role = 'none' | 'read' | 'publish' | 'admin';
5
5
  type JsonSchema = Record<string, unknown>;
@@ -14,10 +14,10 @@
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 } 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, ArtifactTagEditSchema, ArtifactTagEntrySchema, ArtifactTagsSchema } from '../../schema/src/api.js';
18
18
  import { LiveRunSchema, LiveRunStartSchema, LiveRunUpdateSchema, ServerEditableSettingsSchema, ServerSettingsSnapshotSchema, 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
- export const API_VERSION = '1.4.0';
20
+ export const API_VERSION = '1.5.0';
21
21
  /** The JSON Schema of a Zod schema, as OpenAPI 3.1 embeds it. `output` for what the server sends, `input` for what it accepts. */
22
22
  function schemaOf(schema, io = 'output') {
23
23
  const { $schema: _dialect, ...json } = z.toJSONSchema(schema, { target: 'draft-2020-12', unrepresentable: 'any', io });
@@ -35,6 +35,7 @@ const COMPONENTS = {
35
35
  PublishChecks: [PublishChecksSchema.partial({ recordedAtMs: true }), 'input'], ChecksResult: [ChecksResultSchema, 'output'],
36
36
  StoreListing: [StoreListingSchema, 'output'], StoreWriteResult: [StoreWriteResultSchema, 'output'],
37
37
  Outline: [OutlineSchema, 'output'], EvidencePage: [EvidencePageSchema, 'output'], LayerRows: [LayerRowsSchema, 'output'], CheckDetail: [CheckDetailSchema, 'output'],
38
+ ArtifactTagEdit: [ArtifactTagEditSchema, 'input'], ArtifactTagEntry: [ArtifactTagEntrySchema, 'output'], ArtifactTags: [ArtifactTagsSchema, 'output'],
38
39
  TestCatalog: [TestCatalogSchema, 'output'], CatalogHealth: [CatalogHealthSchema, 'output'], TestHistory: [TestHistorySchema, 'output'], RunList: [RunListSchema, 'output'], RunDetail: [RunDetailSchema, 'output'],
39
40
  SpecIndex: [SpecIndexSchema, 'output'], ReportSources: [ReportSourcesSchema, 'output'], RunsIndex: [RunsIndexSchema, 'output'],
40
41
  RunEvent: [LiveRunSchema, 'output'], HelloEvent: [HelloEventSchema, 'output'], ReportEvent: [ReportEventSchema, 'output'], AtlasChangeEvent: [AtlasChangeSchema, 'output'], PingEvent: [PingEventSchema, 'output']
@@ -271,6 +272,8 @@ const BASE_OPERATIONS = [
271
272
  { name: 'domain', in: 'query', description: 'Only tests whose spec is in this domain (`(no spec)` for none). Repeatable or comma-separated.', schema: str },
272
273
  { name: 'spec', in: 'query', description: 'Only tests of this spec path. Repeatable or comma-separated.', schema: str, example: 'specs/checkout/pay.feature' },
273
274
  { name: 'tag', in: 'query', description: 'Only tests with this tag. Repeatable or comma-separated.', schema: str },
275
+ { 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' },
276
+ { 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'] } },
274
277
  { name: 'run', in: 'query', description: 'Only tests recorded in this run (a `key` from `/api/reports/runs`).', schema: str },
275
278
  { 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'] } },
276
279
  { name: 'offset', in: 'query', description: 'Skip this many matching tests.', schema: { type: 'integer', minimum: 0 } },
@@ -305,12 +308,26 @@ const BASE_OPERATIONS = [
305
308
  ],
306
309
  responses: { '200': json('The test and its history.', 'TestHistory'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '400': problem('No `key`.'), '401': UNAUTHORIZED, '404': problem('No test in the report has that key.'), '503': NOT_READY }
307
310
  },
311
+ {
312
+ route: 'GET /api/reports/artifact-tags', path: '/api/reports/artifact-tags', operationId: 'getArtifactTags', tag: 'Report', role: 'read',
313
+ summary: 'Reviewer tags of attachments: every recorded edit',
314
+ description: 'The audit trail of tags that reviewers added to and removed from attachments after upload, oldest first. Each entry says which attachment (`target`), which normalized tags were added and removed, the name of the token that made the edit (`by`, never the token) and when (`atMs`). The layers themselves are never rewritten, so a producer\'s own `tags` on an attachment stay as uploaded; fold the entries (oldest first, the last edit of a tag wins) to get the reviewer tags that stand now. Tags are normalized: `#Demo` and `demo` are `demo`.',
315
+ responses: { '200': json('The edits.', 'ArtifactTags'), '401': UNAUTHORIZED }
316
+ },
317
+ {
318
+ route: 'POST /api/reports/artifact-tags', path: '/api/reports/artifact-tags', operationId: 'editArtifactTags', tag: 'Report', role: 'publish',
319
+ summary: 'Tag or untag one attachment after upload',
320
+ description: 'Records one edit of the reviewer tags of one attachment, by the name of the publish (or admin) token that sends it. It adds an immutable entry to the audit trail and changes nothing else: not the layer, not the movie, not any result. The tags it removes are reviewer tags only; a producer\'s tags are part of the layer and stay. A reader-only token, a browser sign-in box and an anonymous reader are refused. Name the attachment as `artifactRef` does (`id:<id>`, else `key:<bundleKey>`, else `name:<name>`).',
321
+ body: { description: 'The attachment and the tags to add and remove. At most 20 of each, 64 KiB.', contentType: 'application/json', required: true, schema: ref('ArtifactTagEdit') },
322
+ responses: { '201': json('The recorded entry.', 'ArtifactTagEntry'), '401': UNAUTHORIZED, '403': FORBIDDEN, '413': problem('Request too large.'), '422': problem('Not a tag edit: a bad target, an empty or over-long tag, or nothing to change.') }
323
+ },
308
324
  {
309
325
  route: 'GET /api/reports/runs', path: '/api/reports/runs', operationId: 'getReportRuns', tag: 'Report', role: 'read',
310
326
  summary: 'The runs that recorded the report',
311
- 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.',
312
328
  parameters: [
313
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' } },
314
331
  { name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
315
332
  ],
316
333
  responses: { '200': json('The runs.', 'RunList'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '401': UNAUTHORIZED, '503': NOT_READY }
package/docs/extending.md CHANGED
@@ -35,6 +35,9 @@ carry any of these. Every one is optional.
35
35
  | `extensions{}` | Structured data of your own or a tool's, under a name you own. |
36
36
  | `measurements[]` | Numbers, with units and an allowance (already part of the contract). |
37
37
 
38
+ An attachment may carry its own `tags[]` (`demo`): shown as `#demo` and searchable; see
39
+ [Tagging artifacts and demos](rich-evidence.md#tagging-artifacts-and-demos).
40
+
38
41
  On a **layer** the same fields describe the whole run: its CI log, a coverage report, an archive of traces, the
39
42
  build number, a link to the pipeline.
40
43
 
@@ -467,6 +467,8 @@ Send `format=markdown` (or `Accept: text/markdown`) for the same as a Markdown r
467
467
  - `domain` (query): Only tests whose spec is in this domain (`(no spec)` for none). Repeatable or comma-separated.
468
468
  - `spec` (query): Only tests of this spec path. Repeatable or comma-separated.
469
469
  - `tag` (query): Only tests with this tag. Repeatable or comma-separated.
470
+ - `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.
471
+ - `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.
470
472
  - `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
471
473
  - `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
472
474
  - `offset` (query): Skip this many matching tests.
@@ -496,6 +498,8 @@ Scoped to one project. The same project may be selected on the unprefixed API us
496
498
  - `domain` (query): Only tests whose spec is in this domain (`(no spec)` for none). Repeatable or comma-separated.
497
499
  - `spec` (query): Only tests of this spec path. Repeatable or comma-separated.
498
500
  - `tag` (query): Only tests with this tag. Repeatable or comma-separated.
501
+ - `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.
502
+ - `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.
499
503
  - `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
500
504
  - `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
501
505
  - `offset` (query): Skip this many matching tests.
@@ -564,13 +568,58 @@ Scoped to one project. The same project may be selected on the unprefixed API us
564
568
 
565
569
  Responses: `200` The test and its history.; `304` Unchanged since the `If-None-Match` ETag.; `400` No `key`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No test in the report has that key.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
566
570
 
571
+ ### Reviewer tags of attachments: every recorded edit
572
+
573
+ `GET /api/reports/artifact-tags` · role: read
574
+
575
+ The audit trail of tags that reviewers added to and removed from attachments after upload, oldest first. Each entry says which attachment (`target`), which normalized tags were added and removed, the name of the token that made the edit (`by`, never the token) and when (`atMs`). The layers themselves are never rewritten, so a producer's own `tags` on an attachment stay as uploaded; fold the entries (oldest first, the last edit of a tag wins) to get the reviewer tags that stand now. Tags are normalized: `#Demo` and `demo` are `demo`.
576
+
577
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
578
+
579
+ Responses: `200` The edits.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
580
+
581
+ ### Reviewer tags of attachments: every recorded edit
582
+
583
+ `GET /api/projects/{project}/reports/artifact-tags` · role: read
584
+
585
+ The audit trail of tags that reviewers added to and removed from attachments after upload, oldest first. Each entry says which attachment (`target`), which normalized tags were added and removed, the name of the token that made the edit (`by`, never the token) and when (`atMs`). The layers themselves are never rewritten, so a producer's own `tags` on an attachment stay as uploaded; fold the entries (oldest first, the last edit of a tag wins) to get the reviewer tags that stand now. Tags are normalized: `#Demo` and `demo` are `demo`.
586
+
587
+ 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.
588
+
589
+ - `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.
590
+
591
+ Responses: `200` The edits.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
592
+
593
+ ### Tag or untag one attachment after upload
594
+
595
+ `POST /api/reports/artifact-tags` · role: publish
596
+
597
+ Records one edit of the reviewer tags of one attachment, by the name of the publish (or admin) token that sends it. It adds an immutable entry to the audit trail and changes nothing else: not the layer, not the movie, not any result. The tags it removes are reviewer tags only; a producer's tags are part of the layer and stay. A reader-only token, a browser sign-in box and an anonymous reader are refused. Name the attachment as `artifactRef` does (`id:<id>`, else `key:<bundleKey>`, else `name:<name>`).
598
+
599
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
600
+
601
+ Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `413` Request too large.; `422` Not a tag edit: a bad target, an empty or over-long tag, or nothing to change.
602
+
603
+ ### Tag or untag one attachment after upload
604
+
605
+ `POST /api/projects/{project}/reports/artifact-tags` · role: publish
606
+
607
+ Records one edit of the reviewer tags of one attachment, by the name of the publish (or admin) token that sends it. It adds an immutable entry to the audit trail and changes nothing else: not the layer, not the movie, not any result. The tags it removes are reviewer tags only; a producer's tags are part of the layer and stay. A reader-only token, a browser sign-in box and an anonymous reader are refused. Name the attachment as `artifactRef` does (`id:<id>`, else `key:<bundleKey>`, else `name:<name>`).
608
+
609
+ 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.
610
+
611
+ - `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.
612
+
613
+ Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `413` Request too large.; `422` Not a tag edit: a bad target, an empty or over-long tag, or nothing to change.
614
+
567
615
  ### The runs that recorded the report
568
616
 
569
617
  `GET /api/reports/runs` · role: read
570
618
 
571
- 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.
572
620
 
573
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.
574
623
  - `format` (query): `markdown` for Markdown instead of JSON.
575
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.
576
625
 
@@ -580,12 +629,13 @@ Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `40
580
629
 
581
630
  `GET /api/projects/{project}/reports/runs` · role: read
582
631
 
583
- 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.
584
633
 
585
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.
586
635
 
587
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.
588
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.
589
639
  - `format` (query): `markdown` for Markdown instead of JSON.
590
640
 
591
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`.