@crvouga/atlas 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/dist/reports/src/artifact-query.d.ts +14 -2
  3. package/dist/reports/src/artifact-query.js +13 -4
  4. package/dist/reports/src/evidence-detail.js +7 -4
  5. package/dist/reports/src/evidence-types.d.ts +4 -0
  6. package/dist/reports/src/recording-receipt.d.ts +23 -0
  7. package/dist/reports/src/recording-receipt.js +16 -0
  8. package/dist/reports/src/recording-verify.d.ts +74 -0
  9. package/dist/reports/src/recording-verify.js +142 -0
  10. package/dist/reports/src/report-cli.d.ts +2 -2
  11. package/dist/reports/src/report-cli.js +53 -3
  12. package/dist/reports/src/test-catalog.d.ts +7 -1
  13. package/dist/reports/src/test-catalog.js +10 -4
  14. package/dist/schema/src/api.d.ts +84 -0
  15. package/dist/schema/src/api.js +11 -2
  16. package/dist/schema/src/cli-results.d.ts +51 -1
  17. package/dist/schema/src/cli-results.js +25 -1
  18. package/dist/server/src/openapi.js +1 -0
  19. package/docs/agents.md +1 -0
  20. package/docs/reference/cli.md +11 -2
  21. package/docs/reference/http-api.md +2 -0
  22. package/docs/reference/reports.md +185 -9
  23. package/docs/reference/schema.md +19 -1
  24. package/docs/rich-evidence.md +32 -5
  25. package/docs/server.md +1 -1
  26. package/llms-full.txt +47 -8
  27. package/package.json +1 -1
  28. package/packages/schema/schemas/effective-delta.schema.json +8 -0
  29. package/packages/schema/schemas/effective-evidence.schema.json +8 -0
  30. package/packages/schema/schemas/evidence-page.schema.json +4 -0
  31. package/packages/schema/schemas/layer-rows.schema.json +4 -0
  32. package/packages/schema/schemas/report-run.schema.json +74 -0
  33. package/packages/schema/schemas/report-runs.schema.json +86 -0
  34. package/packages/schema/schemas/review-collection.schema.json +4 -0
  35. package/packages/schema/schemas/review-manifest.schema.json +4 -0
  36. package/packages/schema/schemas/test-catalog.schema.json +4 -0
  37. package/packages/schema/schemas/test-history.schema.json +8 -0
  38. package/packages/schema/schemas/verify-recordings-result.schema.json +97 -0
  39. package/packages/server/openapi.json +252 -0
  40. package/packages/visualizer/src/explorer/ArtifactsView.tsx +3 -2
  41. package/packages/visualizer/src/explorer/RunsView.tsx +28 -3
  42. package/packages/visualizer/src/explorer/filters.tsx +11 -3
  43. package/packages/visualizer/src/explorer/search.ts +6 -2
@@ -23,7 +23,8 @@
23
23
  * @see docs/server.md#reading-the-report-as-an-agent
24
24
  */
25
25
  import { displayTag, normalizeTag, normalizeTags } from './artifact-tags.js';
26
- import { facetsOf, forRuns, labelList, matchesFilter, outcomeOfCounts, splitList, usedFilters } from './artifact-query.js';
26
+ import { facetsOf, forArtifacts, forRuns, labelList, matchesFilter, outcomeOfCounts, splitList, usedFilters } from './artifact-query.js';
27
+ import { runRecordings } from './recording-receipt.js';
27
28
  import { videoCounts } from './evidence-detail.js';
28
29
  import { productLatency } from './latency.js';
29
30
  import { matchesScope, scopeTokens, UNKNOWN_SCOPE } from './evidence-scope.js';
@@ -148,7 +149,7 @@ export class TestHistory {
148
149
  const key = runKey(layer);
149
150
  let run = this.#runs.get(key);
150
151
  if (!run) {
151
- run = { key, runUrl: layer.runUrl, branch: layer.branch, sha: layer.sha, start: layer.recordedAtMs, end: layer.recordedAtMs, firstAt: layer.recordedAtMs, first: layer.id, producers: new Set(), environments: new Set(), layers: 0, runId: layer.run?.id, title: undefined, titledAt: 0, titledBy: '', freshness: undefined, videos: [], attachments: 0, members: [], evidence: { tags: new Set(), artifactTags: new Set(), roles: new Set(), labels: new Set() } };
152
+ run = { key, runUrl: layer.runUrl, branch: layer.branch, sha: layer.sha, start: layer.recordedAtMs, end: layer.recordedAtMs, firstAt: layer.recordedAtMs, first: layer.id, producers: new Set(), environments: new Set(), layers: 0, runId: layer.run?.id, title: undefined, titledAt: 0, titledBy: '', freshness: undefined, videos: [], receipts: [], attachments: 0, members: [], evidence: { tags: new Set(), artifactTags: new Set(), roles: new Set(), labels: new Set() } };
152
153
  this.#runs.set(key, run);
153
154
  }
154
155
  // A run is told by its first layer (by time, then id), whatever order its layers arrive in.
@@ -177,6 +178,7 @@ export class TestHistory {
177
178
  run.layers += 1;
178
179
  if (layer.evidence?.videos)
179
180
  run.videos.push(...layer.evidence.videos);
181
+ run.receipts.push(layer.recordings);
180
182
  const held = layer.evidence?.attachments ?? 0;
181
183
  run.attachments += held;
182
184
  const layerVideos = videoCounts(layer.evidence?.videos);
@@ -376,6 +378,7 @@ function runSummary(run, cells) {
376
378
  }
377
379
  failed.sort((a, b) => compareText(a.i.slot, b.i.slot) || compareText(a.r.o.place, b.r.o.place) || compareText(a.r.k, b.r.k));
378
380
  const video = videoCounts(videos);
381
+ const recordings = runRecordings(run.receipts);
379
382
  const failing = failed.slice(0, 20).map(cell => ({ key: cell.i.slot, title: cell.i.title, environment: cell.r.o.environment, detail: (cell.r.t.split('\n')[0] ?? '').slice(0, 500) }));
380
383
  return {
381
384
  key: run.key, ...(run.runId ? { runId: run.runId } : {}), ...(run.title ? { title: run.title } : {}), ...(run.freshness ? { freshness: run.freshness } : {}),
@@ -384,6 +387,7 @@ function runSummary(run, cells) {
384
387
  producers: [...run.producers].sort(), environments: [...run.environments].sort(),
385
388
  layers: run.layers, tests: tests.size, counts, totalDurationMs: measured ? total : null, failing,
386
389
  ...(video ? { videos: video } : {}),
390
+ ...(recordings ? { recordings } : {}),
387
391
  ...(run.attachments ? { attachments: run.attachments } : {}),
388
392
  ...(carried.artifactTags.size ? { artifactTags: [...carried.artifactTags].sort() } : {}),
389
393
  ...(carried.roles.size ? { types: typesOf(carried.roles, (video?.recordings ?? 0) > 0) } : {}),
@@ -737,7 +741,7 @@ export function fieldsOfRun(run) {
737
741
  text: [run.title ?? '', run.key, run.branch ?? '', run.sha ?? '', ...run.producers, ...run.environments, ...artifactTags, ...(run.tags ?? []), ...labels].join('\n').toLowerCase(),
738
742
  artifactTags, tags: [...new Set([...(run.tags ?? []), ...artifactTags])], types, media: types.map(type => ({ type, tags: artifactTags })), labels,
739
743
  environments: run.environments, producers: run.producers, branches: run.branch ? [run.branch] : [], shas: run.sha ? [run.sha.toLowerCase()] : [],
740
- outcomes: [outcomeOfCounts(run.counts)], at: run.endedAtMs,
744
+ outcomes: [outcomeOfCounts(run.counts)], recordings: [run.recordings?.state ?? 'unknown'], at: run.endedAtMs,
741
745
  };
742
746
  }
743
747
  /**
@@ -752,7 +756,9 @@ export function fieldsOfRun(run) {
752
756
  * list.results.map(r => [r.title, r.run, r.sha, r.status]);
753
757
  * ```
754
758
  */
755
- export function artifactList(history, version, query = {}, page = {}) {
759
+ export function artifactList(history, version, asked = {}, page = {}) {
760
+ // Recording completeness is a property of a run: a recorded result is not asked about it.
761
+ const query = forArtifacts(asked);
756
762
  const domains = new Map();
757
763
  const domainOf = (path) => {
758
764
  if (path === null)
@@ -90,6 +90,7 @@ export declare const VideoRefSchema: z.ZodObject<{
90
90
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
91
91
  recording: z.ZodOptional<z.ZodString>;
92
92
  available: z.ZodBoolean;
93
+ reason: z.ZodOptional<z.ZodString>;
93
94
  scope: z.ZodOptional<z.ZodObject<{
94
95
  surface: z.ZodOptional<z.ZodString>;
95
96
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -115,6 +116,7 @@ export declare const VideoCountsSchema: z.ZodObject<{
115
116
  references: z.ZodNumber;
116
117
  recordings: z.ZodNumber;
117
118
  unavailable: z.ZodNumber;
119
+ missing: z.ZodOptional<z.ZodNumber>;
118
120
  }, z.core.$loose>;
119
121
  /** What a result carries beyond its outcome, as counts and short facts. Present only when there is something. */
120
122
  export declare const EvidenceSummarySchema: z.ZodObject<{
@@ -141,6 +143,7 @@ export declare const EvidenceSummarySchema: z.ZodObject<{
141
143
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
142
144
  recording: z.ZodOptional<z.ZodString>;
143
145
  available: z.ZodBoolean;
146
+ reason: z.ZodOptional<z.ZodString>;
144
147
  scope: z.ZodOptional<z.ZodObject<{
145
148
  surface: z.ZodOptional<z.ZodString>;
146
149
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -217,6 +220,7 @@ export declare const LayerHeaderSchema: z.ZodObject<{
217
220
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
218
221
  recording: z.ZodOptional<z.ZodString>;
219
222
  available: z.ZodBoolean;
223
+ reason: z.ZodOptional<z.ZodString>;
220
224
  scope: z.ZodOptional<z.ZodObject<{
221
225
  surface: z.ZodOptional<z.ZodString>;
222
226
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -304,6 +308,7 @@ export declare const LayerRowSchema: z.ZodObject<{
304
308
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
305
309
  recording: z.ZodOptional<z.ZodString>;
306
310
  available: z.ZodBoolean;
311
+ reason: z.ZodOptional<z.ZodString>;
307
312
  scope: z.ZodOptional<z.ZodObject<{
308
313
  surface: z.ZodOptional<z.ZodString>;
309
314
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -409,6 +414,7 @@ export declare const LayerRowsSchema: z.ZodObject<{
409
414
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
410
415
  recording: z.ZodOptional<z.ZodString>;
411
416
  available: z.ZodBoolean;
417
+ reason: z.ZodOptional<z.ZodString>;
412
418
  scope: z.ZodOptional<z.ZodObject<{
413
419
  surface: z.ZodOptional<z.ZodString>;
414
420
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -516,6 +522,7 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
516
522
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
517
523
  recording: z.ZodOptional<z.ZodString>;
518
524
  available: z.ZodBoolean;
525
+ reason: z.ZodOptional<z.ZodString>;
519
526
  scope: z.ZodOptional<z.ZodObject<{
520
527
  surface: z.ZodOptional<z.ZodString>;
521
528
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -617,6 +624,7 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
617
624
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
618
625
  recording: z.ZodOptional<z.ZodString>;
619
626
  available: z.ZodBoolean;
627
+ reason: z.ZodOptional<z.ZodString>;
620
628
  scope: z.ZodOptional<z.ZodObject<{
621
629
  surface: z.ZodOptional<z.ZodString>;
622
630
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -713,6 +721,7 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
713
721
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
714
722
  recording: z.ZodOptional<z.ZodString>;
715
723
  available: z.ZodBoolean;
724
+ reason: z.ZodOptional<z.ZodString>;
716
725
  scope: z.ZodOptional<z.ZodObject<{
717
726
  surface: z.ZodOptional<z.ZodString>;
718
727
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -814,6 +823,7 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
814
823
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
815
824
  recording: z.ZodOptional<z.ZodString>;
816
825
  available: z.ZodBoolean;
826
+ reason: z.ZodOptional<z.ZodString>;
817
827
  scope: z.ZodOptional<z.ZodObject<{
818
828
  surface: z.ZodOptional<z.ZodString>;
819
829
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -931,6 +941,7 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
931
941
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
932
942
  recording: z.ZodOptional<z.ZodString>;
933
943
  available: z.ZodBoolean;
944
+ reason: z.ZodOptional<z.ZodString>;
934
945
  scope: z.ZodOptional<z.ZodObject<{
935
946
  surface: z.ZodOptional<z.ZodString>;
936
947
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1032,6 +1043,7 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
1032
1043
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1033
1044
  recording: z.ZodOptional<z.ZodString>;
1034
1045
  available: z.ZodBoolean;
1046
+ reason: z.ZodOptional<z.ZodString>;
1035
1047
  scope: z.ZodOptional<z.ZodObject<{
1036
1048
  surface: z.ZodOptional<z.ZodString>;
1037
1049
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1141,6 +1153,7 @@ export declare const EvidencePageSchema: z.ZodObject<{
1141
1153
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1142
1154
  recording: z.ZodOptional<z.ZodString>;
1143
1155
  available: z.ZodBoolean;
1156
+ reason: z.ZodOptional<z.ZodString>;
1144
1157
  scope: z.ZodOptional<z.ZodObject<{
1145
1158
  surface: z.ZodOptional<z.ZodString>;
1146
1159
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1721,6 +1734,7 @@ export declare const ReviewSnapshotSchema: z.ZodObject<{
1721
1734
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1722
1735
  recording: z.ZodOptional<z.ZodString>;
1723
1736
  available: z.ZodBoolean;
1737
+ reason: z.ZodOptional<z.ZodString>;
1724
1738
  scope: z.ZodOptional<z.ZodObject<{
1725
1739
  surface: z.ZodOptional<z.ZodString>;
1726
1740
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1784,6 +1798,7 @@ export declare const ReviewEntrySchema: z.ZodObject<{
1784
1798
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1785
1799
  recording: z.ZodOptional<z.ZodString>;
1786
1800
  available: z.ZodBoolean;
1801
+ reason: z.ZodOptional<z.ZodString>;
1787
1802
  scope: z.ZodOptional<z.ZodObject<{
1788
1803
  surface: z.ZodOptional<z.ZodString>;
1789
1804
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1866,6 +1881,7 @@ export declare const ReviewCollectionSchema: z.ZodObject<{
1866
1881
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1867
1882
  recording: z.ZodOptional<z.ZodString>;
1868
1883
  available: z.ZodBoolean;
1884
+ reason: z.ZodOptional<z.ZodString>;
1869
1885
  scope: z.ZodOptional<z.ZodObject<{
1870
1886
  surface: z.ZodOptional<z.ZodString>;
1871
1887
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2025,6 +2041,7 @@ export declare const ReviewCollectionDetailSchema: z.ZodObject<{
2025
2041
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2026
2042
  recording: z.ZodOptional<z.ZodString>;
2027
2043
  available: z.ZodBoolean;
2044
+ reason: z.ZodOptional<z.ZodString>;
2028
2045
  scope: z.ZodOptional<z.ZodObject<{
2029
2046
  surface: z.ZodOptional<z.ZodString>;
2030
2047
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2156,6 +2173,7 @@ export declare const ReviewManifestSchema: z.ZodObject<{
2156
2173
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2157
2174
  recording: z.ZodOptional<z.ZodString>;
2158
2175
  available: z.ZodBoolean;
2176
+ reason: z.ZodOptional<z.ZodString>;
2159
2177
  scope: z.ZodOptional<z.ZodObject<{
2160
2178
  surface: z.ZodOptional<z.ZodString>;
2161
2179
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2284,6 +2302,7 @@ export declare const TestResultEntrySchema: z.ZodObject<{
2284
2302
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2285
2303
  recording: z.ZodOptional<z.ZodString>;
2286
2304
  available: z.ZodBoolean;
2305
+ reason: z.ZodOptional<z.ZodString>;
2287
2306
  scope: z.ZodOptional<z.ZodObject<{
2288
2307
  surface: z.ZodOptional<z.ZodString>;
2289
2308
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2394,6 +2413,7 @@ export declare const TestSummarySchema: z.ZodObject<{
2394
2413
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2395
2414
  recording: z.ZodOptional<z.ZodString>;
2396
2415
  available: z.ZodBoolean;
2416
+ reason: z.ZodOptional<z.ZodString>;
2397
2417
  scope: z.ZodOptional<z.ZodObject<{
2398
2418
  surface: z.ZodOptional<z.ZodString>;
2399
2419
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2700,6 +2720,7 @@ export declare const TestCatalogSchema: z.ZodObject<{
2700
2720
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2701
2721
  recording: z.ZodOptional<z.ZodString>;
2702
2722
  available: z.ZodBoolean;
2723
+ reason: z.ZodOptional<z.ZodString>;
2703
2724
  scope: z.ZodOptional<z.ZodObject<{
2704
2725
  surface: z.ZodOptional<z.ZodString>;
2705
2726
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2938,6 +2959,7 @@ export declare const TestHistorySchema: z.ZodObject<{
2938
2959
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2939
2960
  recording: z.ZodOptional<z.ZodString>;
2940
2961
  available: z.ZodBoolean;
2962
+ reason: z.ZodOptional<z.ZodString>;
2941
2963
  scope: z.ZodOptional<z.ZodObject<{
2942
2964
  surface: z.ZodOptional<z.ZodString>;
2943
2965
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -3066,6 +3088,7 @@ export declare const TestHistorySchema: z.ZodObject<{
3066
3088
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
3067
3089
  recording: z.ZodOptional<z.ZodString>;
3068
3090
  available: z.ZodBoolean;
3091
+ reason: z.ZodOptional<z.ZodString>;
3069
3092
  scope: z.ZodOptional<z.ZodObject<{
3070
3093
  surface: z.ZodOptional<z.ZodString>;
3071
3094
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -3267,6 +3290,21 @@ export declare const ReportRunSchema: z.ZodObject<{
3267
3290
  references: z.ZodNumber;
3268
3291
  recordings: z.ZodNumber;
3269
3292
  unavailable: z.ZodNumber;
3293
+ missing: z.ZodOptional<z.ZodNumber>;
3294
+ }, z.core.$loose>>;
3295
+ recordings: z.ZodOptional<z.ZodObject<{
3296
+ state: z.ZodEnum<{
3297
+ unknown: "unknown";
3298
+ complete: "complete";
3299
+ incomplete: "incomplete";
3300
+ "none-expected": "none-expected";
3301
+ }>;
3302
+ expected: z.ZodNumber;
3303
+ published: z.ZodNumber;
3304
+ skipped: z.ZodNumber;
3305
+ unresolved: z.ZodNumber;
3306
+ notApplicable: z.ZodNumber;
3307
+ layersWithoutReceipt: z.ZodNumber;
3270
3308
  }, z.core.$loose>>;
3271
3309
  attachments: z.ZodOptional<z.ZodNumber>;
3272
3310
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -3313,6 +3351,10 @@ export declare const QueryFacetsSchema: z.ZodObject<{
3313
3351
  value: z.ZodString;
3314
3352
  count: z.ZodNumber;
3315
3353
  }, z.core.$loose>>>;
3354
+ recordings: z.ZodOptional<z.ZodArray<z.ZodObject<{
3355
+ value: z.ZodString;
3356
+ count: z.ZodNumber;
3357
+ }, z.core.$loose>>>;
3316
3358
  }, z.core.$loose>;
3317
3359
  /** `GET /api/reports/runs`: runs, newest first. */
3318
3360
  export declare const RunListSchema: z.ZodObject<{
@@ -3354,6 +3396,21 @@ export declare const RunListSchema: z.ZodObject<{
3354
3396
  references: z.ZodNumber;
3355
3397
  recordings: z.ZodNumber;
3356
3398
  unavailable: z.ZodNumber;
3399
+ missing: z.ZodOptional<z.ZodNumber>;
3400
+ }, z.core.$loose>>;
3401
+ recordings: z.ZodOptional<z.ZodObject<{
3402
+ state: z.ZodEnum<{
3403
+ unknown: "unknown";
3404
+ complete: "complete";
3405
+ incomplete: "incomplete";
3406
+ "none-expected": "none-expected";
3407
+ }>;
3408
+ expected: z.ZodNumber;
3409
+ published: z.ZodNumber;
3410
+ skipped: z.ZodNumber;
3411
+ unresolved: z.ZodNumber;
3412
+ notApplicable: z.ZodNumber;
3413
+ layersWithoutReceipt: z.ZodNumber;
3357
3414
  }, z.core.$loose>>;
3358
3415
  attachments: z.ZodOptional<z.ZodNumber>;
3359
3416
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -3396,6 +3453,10 @@ export declare const RunListSchema: z.ZodObject<{
3396
3453
  value: z.ZodString;
3397
3454
  count: z.ZodNumber;
3398
3455
  }, z.core.$loose>>>;
3456
+ recordings: z.ZodOptional<z.ZodArray<z.ZodObject<{
3457
+ value: z.ZodString;
3458
+ count: z.ZodNumber;
3459
+ }, z.core.$loose>>>;
3399
3460
  }, z.core.$loose>>;
3400
3461
  }, z.core.$loose>;
3401
3462
  /** One recorded result that carries evidence: where and when it was recorded and what it carries. */
@@ -3435,6 +3496,7 @@ export declare const ArtifactResultSchema: z.ZodObject<{
3435
3496
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
3436
3497
  recording: z.ZodOptional<z.ZodString>;
3437
3498
  available: z.ZodBoolean;
3499
+ reason: z.ZodOptional<z.ZodString>;
3438
3500
  scope: z.ZodOptional<z.ZodObject<{
3439
3501
  surface: z.ZodOptional<z.ZodString>;
3440
3502
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -3505,6 +3567,7 @@ export declare const ArtifactListSchema: z.ZodObject<{
3505
3567
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
3506
3568
  recording: z.ZodOptional<z.ZodString>;
3507
3569
  available: z.ZodBoolean;
3570
+ reason: z.ZodOptional<z.ZodString>;
3508
3571
  scope: z.ZodOptional<z.ZodObject<{
3509
3572
  surface: z.ZodOptional<z.ZodString>;
3510
3573
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -3566,6 +3629,10 @@ export declare const ArtifactListSchema: z.ZodObject<{
3566
3629
  value: z.ZodString;
3567
3630
  count: z.ZodNumber;
3568
3631
  }, z.core.$loose>>>;
3632
+ recordings: z.ZodOptional<z.ZodArray<z.ZodObject<{
3633
+ value: z.ZodString;
3634
+ count: z.ZodNumber;
3635
+ }, z.core.$loose>>>;
3569
3636
  }, z.core.$loose>;
3570
3637
  }, z.core.$loose>;
3571
3638
  /** `GET /api/reports/runs/detail`: one run and its last result for every test, environment and producer it recorded. */
@@ -3607,6 +3674,21 @@ export declare const RunDetailSchema: z.ZodObject<{
3607
3674
  references: z.ZodNumber;
3608
3675
  recordings: z.ZodNumber;
3609
3676
  unavailable: z.ZodNumber;
3677
+ missing: z.ZodOptional<z.ZodNumber>;
3678
+ }, z.core.$loose>>;
3679
+ recordings: z.ZodOptional<z.ZodObject<{
3680
+ state: z.ZodEnum<{
3681
+ unknown: "unknown";
3682
+ complete: "complete";
3683
+ incomplete: "incomplete";
3684
+ "none-expected": "none-expected";
3685
+ }>;
3686
+ expected: z.ZodNumber;
3687
+ published: z.ZodNumber;
3688
+ skipped: z.ZodNumber;
3689
+ unresolved: z.ZodNumber;
3690
+ notApplicable: z.ZodNumber;
3691
+ layersWithoutReceipt: z.ZodNumber;
3610
3692
  }, z.core.$loose>>;
3611
3693
  attachments: z.ZodOptional<z.ZodNumber>;
3612
3694
  artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -3639,6 +3721,7 @@ export declare const RunDetailSchema: z.ZodObject<{
3639
3721
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
3640
3722
  recording: z.ZodOptional<z.ZodString>;
3641
3723
  available: z.ZodBoolean;
3724
+ reason: z.ZodOptional<z.ZodString>;
3642
3725
  scope: z.ZodOptional<z.ZodObject<{
3643
3726
  surface: z.ZodOptional<z.ZodString>;
3644
3727
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -3676,6 +3759,7 @@ export declare const RunDetailSchema: z.ZodObject<{
3676
3759
  references: z.ZodNumber;
3677
3760
  recordings: z.ZodNumber;
3678
3761
  unavailable: z.ZodNumber;
3762
+ missing: z.ZodOptional<z.ZodNumber>;
3679
3763
  }, z.core.$loose>>;
3680
3764
  }, z.core.$loose>>>;
3681
3765
  }, z.core.$loose>;
@@ -71,13 +71,15 @@ export const VideoRefSchema = z.looseObject({
71
71
  tags: z.array(z.string()).optional().describe('The normalized tags of this attachment itself. Absent when it has none (and in documents of older servers)'),
72
72
  recording: z.string().optional().describe('What identifies the bytes; references with the same value are one recording. Absent when it cannot be played'),
73
73
  available: z.boolean().describe('False when the reference names no bytes: no bundle key, address or inline text, or a bundle key that the layer\'s own bundle does not hold'),
74
+ reason: z.string().optional().describe('Why it is unavailable, when the report can tell: `not-in-bundle` is a file the layer names but does not hold (stored, then gone). An open vocabulary: treat an unknown reason as plain unavailable'),
74
75
  scope: EvidenceScopeSchema.optional().describe('What this recording shows as its producer declared it: its own `scope`, else its attempt\'s, check\'s, part\'s or layer\'s. Absent: the scope is unknown, never live, physical or audible')
75
76
  });
76
77
  /** Video references as counts: how many are referenced, how many distinct recordings can be played, how many cannot. */
77
78
  export const VideoCountsSchema = z.looseObject({
78
79
  references: z.number().int().nonnegative().describe('Every reference to a video; identical bytes referenced twice count twice'),
79
80
  recordings: z.number().int().nonnegative().describe('Distinct playable recordings'),
80
- unavailable: z.number().int().nonnegative().describe('References that name no bytes, or bytes the layer does not hold')
81
+ unavailable: z.number().int().nonnegative().describe('References that name no bytes, or bytes the layer does not hold'),
82
+ missing: z.number().int().positive().optional().describe('Of `unavailable`, those that name a file the layer\'s bundle does not hold (stored, then gone). Absent when there are none')
81
83
  });
82
84
  /** What a result carries beyond its outcome, as counts and short facts. Present only when there is something. */
83
85
  export const EvidenceSummarySchema = z.looseObject({
@@ -625,6 +627,12 @@ export const ReportRunSchema = z.looseObject({
625
627
  totalDurationMs: z.number().nullable(),
626
628
  failing: z.array(z.looseObject({ key: z.string(), title: z.string(), environment: z.string().nullable(), detail: z.string() })),
627
629
  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'),
630
+ recordings: z.looseObject({
631
+ state: z.enum(['complete', 'incomplete', 'none-expected', 'unknown']).describe('`incomplete` when an expected recording was skipped or unresolved; else `unknown` when a layer declared no expectations; else `complete` when something was expected and `none-expected` when nothing was'),
632
+ expected: z.number().int().nonnegative(), published: z.number().int().nonnegative().describe('Of the expected recordings, uploaded or referenced by address'),
633
+ skipped: z.number().int().nonnegative(), unresolved: z.number().int().nonnegative(), notApplicable: z.number().int().nonnegative(),
634
+ layersWithoutReceipt: z.number().int().nonnegative().describe('Layers of the run that declared no recording expectations: nothing is claimed for them')
635
+ }).optional().describe('Whether the recordings its layers expected were published, apart from its outcomes. Absent when no layer declared an expectation: its completeness is unknown, never complete. Matched by the `recordings` filter'),
628
636
  attachments: z.number().int().positive().optional().describe('Files attached to the run itself, in its layers, not to a check. Absent when there are none'),
629
637
  artifactTags: z.array(z.string()).optional().describe('The normalized tags of the attachments of its results, sorted and distinct. Absent when none is tagged, and in reports of older servers'),
630
638
  tags: z.array(z.string()).optional().describe('The tags of its checks and layers, which its attachments inherit (what `inherited=1` also matches). Absent when none'),
@@ -637,7 +645,8 @@ export const QueryFacetValueSchema = z.looseObject({ value: z.string(), count: z
637
645
  export const QueryFacetsSchema = z.looseObject({
638
646
  artifactTags: z.array(QueryFacetValueSchema), types: z.array(QueryFacetValueSchema), labels: z.array(QueryFacetValueSchema).describe('Values are `key=value`'),
639
647
  environments: z.array(QueryFacetValueSchema), producers: z.array(QueryFacetValueSchema), branches: z.array(QueryFacetValueSchema), outcomes: z.array(QueryFacetValueSchema),
640
- domains: z.array(QueryFacetValueSchema).optional().describe('Only for artifacts: runs have no domain')
648
+ domains: z.array(QueryFacetValueSchema).optional().describe('Only for artifacts: runs have no domain'),
649
+ recordings: z.array(QueryFacetValueSchema).optional().describe('Only for runs: `complete`, `incomplete`, `none-expected` and `unknown` with how many runs have each. Absent on servers that predate the filter')
641
650
  });
642
651
  /** `GET /api/reports/runs`: runs, newest first. */
643
652
  export const RunListSchema = z.looseObject({
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module Command results (`--format json`)
3
3
  *
4
- * What `atlas report publish`, `stream`, `merge` and `doctor` print on standard output with `--format json` (alias
4
+ * What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings` print on standard output with `--format json` (alias
5
5
  * `--json`), and what any `atlas` command prints on standard error for a command that was wrong: one JSON document,
6
6
  * so an agent or a script never has to parse prose. (`status` and `policy` already print documents:
7
7
  * `ReportStatusSchema`, and an IAM policy.) Each is also published as a JSON Schema.
@@ -152,6 +152,36 @@ export declare const DoctorResultSchema: z.ZodObject<{
152
152
  }, z.core.$loose>>;
153
153
  }, z.core.$loose>>;
154
154
  }, z.core.$loose>;
155
+ /** A published video that could not be read back from the store as published (see `atlas report verify-recordings`). */
156
+ export declare const RecordingGapSchema: z.ZodObject<{
157
+ layer: z.ZodString;
158
+ check: z.ZodNullable<z.ZodString>;
159
+ name: z.ZodString;
160
+ bundleKey: z.ZodString;
161
+ problem: z.ZodString;
162
+ reason: z.ZodString;
163
+ }, z.core.$loose>;
164
+ /**
165
+ * What `atlas report verify-recordings --format json` prints: whether the videos the layers published can still be read
166
+ * back from the store as they were published. It describes stored bytes, never playback in a browser, and it never changes
167
+ * a check\'s result.
168
+ */
169
+ export declare const VerifyRecordingsResultSchema: z.ZodObject<{
170
+ ok: z.ZodBoolean;
171
+ layers: z.ZodNumber;
172
+ checked: z.ZodNumber;
173
+ verified: z.ZodNumber;
174
+ referenced: z.ZodNumber;
175
+ gaps: z.ZodArray<z.ZodObject<{
176
+ layer: z.ZodString;
177
+ check: z.ZodNullable<z.ZodString>;
178
+ name: z.ZodString;
179
+ bundleKey: z.ZodString;
180
+ problem: z.ZodString;
181
+ reason: z.ZodString;
182
+ }, z.core.$loose>>;
183
+ notices: z.ZodArray<z.ZodString>;
184
+ }, z.core.$loose>;
155
185
  /** What any `atlas` command prints on standard error, with `--format json`, when the command itself was wrong (exit code 2). */
156
186
  export declare const CommandErrorSchema: z.ZodObject<{
157
187
  ok: z.ZodLiteral<false>;
@@ -532,6 +562,26 @@ export declare const CLI_RESULTS: {
532
562
  }, z.core.$loose>>;
533
563
  }, z.core.$loose>;
534
564
  };
565
+ readonly verifyRecordings: {
566
+ readonly command: "atlas report verify-recordings";
567
+ readonly schemaName: "verify-recordings-result";
568
+ readonly schema: z.ZodObject<{
569
+ ok: z.ZodBoolean;
570
+ layers: z.ZodNumber;
571
+ checked: z.ZodNumber;
572
+ verified: z.ZodNumber;
573
+ referenced: z.ZodNumber;
574
+ gaps: z.ZodArray<z.ZodObject<{
575
+ layer: z.ZodString;
576
+ check: z.ZodNullable<z.ZodString>;
577
+ name: z.ZodString;
578
+ bundleKey: z.ZodString;
579
+ problem: z.ZodString;
580
+ reason: z.ZodString;
581
+ }, z.core.$loose>>;
582
+ notices: z.ZodArray<z.ZodString>;
583
+ }, z.core.$loose>;
584
+ };
535
585
  readonly error: {
536
586
  readonly command: "a command that was wrong (standard error)";
537
587
  readonly schemaName: "command-error";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module Command results (`--format json`)
3
3
  *
4
- * What `atlas report publish`, `stream`, `merge` and `doctor` print on standard output with `--format json` (alias
4
+ * What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings` print on standard output with `--format json` (alias
5
5
  * `--json`), and what any `atlas` command prints on standard error for a command that was wrong: one JSON document,
6
6
  * so an agent or a script never has to parse prose. (`status` and `policy` already print documents:
7
7
  * `ReportStatusSchema`, and an IAM policy.) Each is also published as a JSON Schema.
@@ -75,6 +75,29 @@ export const DoctorResultSchema = z.looseObject({
75
75
  ok: z.boolean().describe('True when every check of every store passed (otherwise the exit code is 1)'),
76
76
  stores: z.array(z.looseObject({ id: z.string(), checks: z.array(DoctorCheckSchema) }))
77
77
  });
78
+ /** A published video that could not be read back from the store as published (see `atlas report verify-recordings`). */
79
+ export const RecordingGapSchema = z.looseObject({
80
+ layer: z.string().describe('The layer that holds the video'),
81
+ check: z.string().nullable().describe('The check that holds it, or null for a video attached to the layer or a part'),
82
+ name: z.string().describe('The attachment\'s name'),
83
+ bundleKey: z.string().describe('Its key in the layer\'s bundle (`attachments/<sha256>`)'),
84
+ problem: z.string().describe('`not-in-bundle` (the layer names a file its bundle does not list), `blob-missing` (a blob holding it is gone: a 404 when played), `unreadable` (the store could not be read: not proof it is gone), `size-mismatch`, `checksum-mismatch` or `undecodable` (empty, or not the container its media type names). An open vocabulary: treat an unknown problem as a gap'),
85
+ reason: z.string().describe('A short explanation without tokens, paths or payload')
86
+ });
87
+ /**
88
+ * What `atlas report verify-recordings --format json` prints: whether the videos the layers published can still be read
89
+ * back from the store as they were published. It describes stored bytes, never playback in a browser, and it never changes
90
+ * a check\'s result.
91
+ */
92
+ export const VerifyRecordingsResultSchema = z.looseObject({
93
+ ok: z.boolean().describe('True when no layer has a gap and every layer could be read (otherwise the exit code is 1). Test results are unaffected either way'),
94
+ layers: z.number().int().nonnegative().describe('Layers examined'),
95
+ checked: z.number().int().nonnegative().describe('Videos read back from the store (those that name a bundle key)'),
96
+ verified: z.number().int().nonnegative().describe('Of those, the ones that passed every check'),
97
+ referenced: z.number().int().nonnegative().describe('Videos given only by address, which Atlas holds no bytes for and does not check'),
98
+ gaps: z.array(RecordingGapSchema).describe('Every video that is missing, unreadable, corrupt or not the container it claims. Re-publish the layer\'s recordings to repair them'),
99
+ notices: z.array(z.string()).describe('Layers that could not be read at all: their recordings are unknown, never verified')
100
+ });
78
101
  /** What any `atlas` command prints on standard error, with `--format json`, when the command itself was wrong (exit code 2). */
79
102
  export const CommandErrorSchema = z.looseObject({
80
103
  ok: z.literal(false),
@@ -142,5 +165,6 @@ export const CLI_RESULTS = {
142
165
  merge: { command: 'atlas report merge', schemaName: 'merge-result', schema: MergeResultSchema },
143
166
  doctor: { command: 'atlas report doctor', schemaName: 'doctor-result', schema: DoctorResultSchema },
144
167
  status: { command: 'atlas report status', schemaName: 'report-status', schema: ReportStatusSchema },
168
+ verifyRecordings: { command: 'atlas report verify-recordings', schemaName: 'verify-recordings-result', schema: VerifyRecordingsResultSchema },
145
169
  error: { command: 'a command that was wrong (standard error)', schemaName: 'command-error', schema: CommandErrorSchema }
146
170
  };
@@ -427,6 +427,7 @@ const BASE_OPERATIONS = [
427
427
  { name: 'branch', in: 'query', description: 'Branches.', schema: str },
428
428
  { name: 'sha', in: 'query', description: 'Revisions: a full commit id or a prefix of at least seven characters.', schema: str },
429
429
  { 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 },
430
+ { name: 'recordings', in: 'query', description: 'Recording completeness of the run, apart from its outcome: `complete`, `incomplete`, `none-expected` or `unknown` (a run whose layers declared no recording expectations is `unknown`, never `complete`). Repeatable or comma-separated. Answered from the layers\' receipts; no recording is read. A server that does not know it ignores it and does not name it in `filters`.', schema: str },
430
431
  { name: 'q', in: 'query', description: 'Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.', schema: str },
431
432
  { 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 },
432
433
  { name: 'until', in: 'query', description: 'Only records at or before this time (same forms as `since`).', schema: str },
package/docs/agents.md CHANGED
@@ -163,6 +163,7 @@ JSON Schema (`@crvouga/atlas/schemas/<name>.schema.json`, draft 2020-12):
163
163
  | `atlas report merge` | `merge-result` |
164
164
  | `atlas report doctor` | `doctor-result` |
165
165
  | `atlas report status` | `report-status` |
166
+ | `atlas report verify-recordings` | `verify-recordings-result` |
166
167
  | `a command that was wrong (standard error)` | `command-error` |
167
168
  <!-- /generated:json-command-schemas -->
168
169
 
@@ -105,6 +105,8 @@ atlas report — layered evidence for any kind of spec
105
105
  [--specs <dir>] [--format text|json|markdown]
106
106
  [--require-environments a,b] [--require-all-specs]
107
107
  [--branch <name>]... [--ignore-modified]
108
+ atlas report verify-recordings [--store <dir|s3://…|id=url>]... [--backends <module>] [--layer <id>]...
109
+ [--limit <n>] [--branch <name>]... [--ignore-modified] [--format text|json]
108
110
  atlas report merge [--store …]... [--backends <module>] --into <dir|s3://…>
109
111
  atlas report doctor [--store …]... [--role publisher|reader|both]
110
112
  atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]
@@ -148,6 +150,13 @@ atlas report — layered evidence for any kind of spec
148
150
  --require-all-specs fail while any spec has no results
149
151
  --branch <name> only layers recorded on this branch (repeatable)
150
152
  --ignore-modified leave out layers from modified checkouts and unknown origins
153
+ verify-recordings Read back the videos the layers published and check they are still there as published: the
154
+ layer still lists the file, its blob is still stored, its size and SHA-256 are the recorded ones, and it
155
+ begins like the container its media type names. Exit 1 when any video is missing, unreadable, corrupt or
156
+ not that container, naming the layer and check; test results are never changed. This checks stored bytes,
157
+ not playback in a browser. Videos given only by address are counted, not checked.
158
+ --layer <id> only the layer whose id starts with this (repeatable; at least 7 digits)
159
+ --limit <n> only the newest n layers
151
160
  merge Copy the layers of several stores into one.
152
161
  doctor Check that a store works: credentials, listing, writing, and what it holds.
153
162
  policy Print the least-privilege IAM policy for publishers, readers, or both.
@@ -156,8 +165,8 @@ atlas report — layered evidence for any kind of spec
156
165
  { id, label?, store } (or a function returning it): a device lab, anything
157
166
  that implements ObjectStore.
158
167
 
159
- --format json (alias --json) publish, stream, merge, doctor and status print one JSON document instead of prose
160
- (schemas: publish-result, stream-result, merge-result, doctor-result, report-status). A command
168
+ --format json (alias --json) publish, stream, merge, doctor, status and verify-recordings print one JSON document instead of prose
169
+ (schemas: publish-result, stream-result, merge-result, doctor-result, report-status, verify-recordings-result). A command
161
170
  that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
162
171
  A store may be an Atlas server (https://…): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.
163
172
 
@@ -833,6 +833,7 @@ Every run, newest first: layers grouped by their CI run URL (else branch and com
833
833
  - `branch` (query): Branches.
834
834
  - `sha` (query): Revisions: a full commit id or a prefix of at least seven characters.
835
835
  - `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`.
836
+ - `recordings` (query): Recording completeness of the run, apart from its outcome: `complete`, `incomplete`, `none-expected` or `unknown` (a run whose layers declared no recording expectations is `unknown`, never `complete`). Repeatable or comma-separated. Answered from the layers' receipts; no recording is read. A server that does not know it ignores it and does not name it in `filters`.
836
837
  - `q` (query): Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.
837
838
  - `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.
838
839
  - `until` (query): Only records at or before this time (same forms as `since`).
@@ -861,6 +862,7 @@ Scoped to one project. The same project may be selected on the unprefixed API us
861
862
  - `branch` (query): Branches.
862
863
  - `sha` (query): Revisions: a full commit id or a prefix of at least seven characters.
863
864
  - `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`.
865
+ - `recordings` (query): Recording completeness of the run, apart from its outcome: `complete`, `incomplete`, `none-expected` or `unknown` (a run whose layers declared no recording expectations is `unknown`, never `complete`). Repeatable or comma-separated. Answered from the layers' receipts; no recording is read. A server that does not know it ignores it and does not name it in `filters`.
864
866
  - `q` (query): Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.
865
867
  - `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.
866
868
  - `until` (query): Only records at or before this time (same forms as `since`).