@crvouga/atlas 1.18.0 → 1.19.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 (45) hide show
  1. package/CHANGELOG.md +8 -1
  2. package/dist/reports/src/evidence-detail.js +13 -3
  3. package/dist/reports/src/evidence-types.d.ts +4 -0
  4. package/dist/schema/src/api.d.ts +50 -0
  5. package/dist/schema/src/api.js +2 -0
  6. package/docs/reference/reports.md +4 -0
  7. package/docs/rich-evidence.md +1 -0
  8. package/docs/ui.md +40 -1
  9. package/llms-full.txt +41 -1
  10. package/package.json +1 -1
  11. package/packages/schema/schemas/effective-delta.schema.json +18 -0
  12. package/packages/schema/schemas/effective-evidence.schema.json +18 -0
  13. package/packages/schema/schemas/evidence-page.schema.json +9 -0
  14. package/packages/schema/schemas/layer-rows.schema.json +9 -0
  15. package/packages/schema/schemas/report-run.schema.json +9 -0
  16. package/packages/schema/schemas/review-collection.schema.json +9 -0
  17. package/packages/schema/schemas/review-manifest.schema.json +9 -0
  18. package/packages/schema/schemas/test-catalog.schema.json +9 -0
  19. package/packages/schema/schemas/test-history.schema.json +18 -0
  20. package/packages/server/openapi.json +126 -0
  21. package/packages/visualizer/src/app/AppShell.tsx +2 -2
  22. package/packages/visualizer/src/app/router.tsx +25 -1
  23. package/packages/visualizer/src/components/Header.tsx +15 -4
  24. package/packages/visualizer/src/evidence/behavior-index.test.ts +119 -0
  25. package/packages/visualizer/src/evidence/behavior-index.ts +162 -0
  26. package/packages/visualizer/src/evidence/behavior-panel.tsx +36 -22
  27. package/packages/visualizer/src/evidence/behavior-return.test.ts +41 -0
  28. package/packages/visualizer/src/evidence/behavior-return.ts +59 -0
  29. package/packages/visualizer/src/evidence/behavior.ts +32 -1
  30. package/packages/visualizer/src/evidence/behaviors-page.tsx +465 -0
  31. package/packages/visualizer/src/evidence/feature-page.tsx +5 -26
  32. package/packages/visualizer/src/evidence/scoped-summary-view.tsx +110 -0
  33. package/packages/visualizer/src/evidence/scoped-summary.test.ts +62 -0
  34. package/packages/visualizer/src/evidence/scoped-summary.ts +118 -0
  35. package/packages/visualizer/src/evidence/spec-texts.ts +88 -0
  36. package/packages/visualizer/src/explorer/ArtifactsView.tsx +4 -4
  37. package/packages/visualizer/src/explorer/Explorer.tsx +1 -1
  38. package/packages/visualizer/src/explorer/RunsView.tsx +4 -4
  39. package/packages/visualizer/src/explorer/collections.tsx +5 -5
  40. package/packages/visualizer/src/explorer/navigate.ts +17 -0
  41. package/packages/visualizer/src/explorer/playlist-entries.test.ts +10 -1
  42. package/packages/visualizer/src/explorer/playlist-entries.ts +10 -0
  43. package/packages/visualizer/src/explorer/playlist.tsx +43 -8
  44. package/packages/visualizer/src/explorer/search.ts +7 -1
  45. package/packages/visualizer/src/find/Finder.tsx +1 -62
package/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Changelog: @crvouga/atlas
2
2
 
3
- ## 1.18.0 (2026-10-29)
3
+ ## 1.19.0 (2026-10-29)
4
+
5
+ ### Features
6
+
7
+ - add a project-wide behavior index, a read-and-watch workspace and a scoped summary ([bb2d270](https://github.com/crvouga/atlas/commit/bb2d270fcfc2374c316db804e80af67885bdd8ee))
8
+ - restore the watch playlist from the address and show clip posters and lengths ([2dfc480](https://github.com/crvouga/atlas/commit/2dfc4806a72621f67f304cd9fda7619a9dbbea3f))
9
+
10
+ ## 1.18.0 (2026-10-28)
4
11
 
5
12
  ### Features
6
13
 
@@ -62,7 +62,7 @@ function summarize(holders, owner, inherited, firstIsParent = false, unstored) {
62
62
  for (const tag of normalizeTags(item.tags))
63
63
  artifactTags.add(tag);
64
64
  roles.add(item.role ?? roleOfType(item.contentType ?? contentTypeOf(item.name)));
65
- const video = videoOf(item, by, where, effectiveScope(item.scope, ...scopes), unstored);
65
+ const video = videoOf(item, by, where, effectiveScope(item.scope, ...scopes), unstored, list);
66
66
  if (video)
67
67
  videos.push(video);
68
68
  }
@@ -110,14 +110,24 @@ export function isVideoAttachment(attachment) {
110
110
  export function attachmentIdentity(item) {
111
111
  return item.sha256 ? `sha256:${item.sha256}` : item.bundleKey ?? item.uri ?? (item.text === undefined ? undefined : `inline:${item.name}`);
112
112
  }
113
- function videoOf(item, owner, where, scope, unstored) {
113
+ /** The bundle key of the image an attachment names as its poster among its siblings, when the layer's bundle holds it. */
114
+ function posterKey(item, siblings, unstored) {
115
+ if (item.poster === undefined)
116
+ return undefined;
117
+ const poster = siblings?.find(other => other !== item && other.id === item.poster);
118
+ if (!poster?.bundleKey || unstored?.has(poster.bundleKey))
119
+ return undefined;
120
+ return (poster.contentType ?? contentTypeOf(poster.name)).toLowerCase().startsWith('image/') ? poster.bundleKey : undefined;
121
+ }
122
+ function videoOf(item, owner, where, scope, unstored, siblings) {
114
123
  if (!isVideoAttachment(item))
115
124
  return undefined;
116
125
  // A bundle key the layer's bundle does not hold names bytes that are not there: only an address or inline text can still carry them, and the digest of lost bytes identifies nothing playable.
117
126
  const lost = item.bundleKey !== undefined && unstored?.has(item.bundleKey) === true;
118
127
  const recording = lost ? attachmentIdentity({ name: item.name, uri: item.uri, text: item.text }) : attachmentIdentity(item);
119
128
  const tags = normalizeTags(item.tags);
120
- return { name: item.name, owner, ...where, ...(tags.length ? { tags } : {}), ...(recording === undefined ? {} : { recording }), available: recording !== undefined, ...(lost && recording === undefined ? { reason: 'not-in-bundle' } : {}), ...(scope ? { scope } : {}) };
129
+ const poster = posterKey(item, siblings, unstored);
130
+ return { name: item.name, owner, ...where, ...(tags.length ? { tags } : {}), ...(recording === undefined ? {} : { recording }), available: recording !== undefined, ...(lost && recording === undefined ? { reason: 'not-in-bundle' } : {}), ...(item.durationMs !== undefined && Number.isFinite(item.durationMs) ? { durationMs: item.durationMs } : {}), ...(poster === undefined ? {} : { poster }), ...(scope ? { scope } : {}) };
121
131
  }
122
132
  /**
123
133
  * Counts references, distinct playable recordings and unplayable references. Undefined when there is no reference, so
@@ -60,6 +60,10 @@ export type VideoRef = {
60
60
  readonly available: boolean;
61
61
  /** Why it is unavailable, when the report can tell: `not-in-bundle` names a file the layer should hold (its bundle key) but does not, a recording that was acknowledged and is no longer there. Absent otherwise, and for an available video. An open vocabulary: treat an unknown reason as plain unavailable. */
62
62
  readonly reason?: 'not-in-bundle';
63
+ /** How long the recording is, in milliseconds, as its producer declared (`Attachment.durationMs`): a hint to show before the file is opened, never measured here and never a latency. Absent when none was declared, and in documents of older servers. */
64
+ readonly durationMs?: number;
65
+ /** The bundle key, in the same layer, of the image the producer named as this recording's poster (`Attachment.poster`): a hint to show before the file is opened. Only an image the layer's bundle holds; absent when there is none, and in documents of older servers. */
66
+ readonly poster?: string;
63
67
  /** What this recording shows, as declared for it: its own `scope`, else its attempt's, check's, part's or layer's. Absent when none declared one: its scope is unknown, not live, physical or audible. */
64
68
  readonly scope?: EvidenceScope;
65
69
  };
@@ -91,6 +91,8 @@ export declare const VideoRefSchema: z.ZodObject<{
91
91
  recording: z.ZodOptional<z.ZodString>;
92
92
  available: z.ZodBoolean;
93
93
  reason: z.ZodOptional<z.ZodString>;
94
+ durationMs: z.ZodOptional<z.ZodNumber>;
95
+ poster: z.ZodOptional<z.ZodString>;
94
96
  scope: z.ZodOptional<z.ZodObject<{
95
97
  surface: z.ZodOptional<z.ZodString>;
96
98
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -144,6 +146,8 @@ export declare const EvidenceSummarySchema: z.ZodObject<{
144
146
  recording: z.ZodOptional<z.ZodString>;
145
147
  available: z.ZodBoolean;
146
148
  reason: z.ZodOptional<z.ZodString>;
149
+ durationMs: z.ZodOptional<z.ZodNumber>;
150
+ poster: z.ZodOptional<z.ZodString>;
147
151
  scope: z.ZodOptional<z.ZodObject<{
148
152
  surface: z.ZodOptional<z.ZodString>;
149
153
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -221,6 +225,8 @@ export declare const LayerHeaderSchema: z.ZodObject<{
221
225
  recording: z.ZodOptional<z.ZodString>;
222
226
  available: z.ZodBoolean;
223
227
  reason: z.ZodOptional<z.ZodString>;
228
+ durationMs: z.ZodOptional<z.ZodNumber>;
229
+ poster: z.ZodOptional<z.ZodString>;
224
230
  scope: z.ZodOptional<z.ZodObject<{
225
231
  surface: z.ZodOptional<z.ZodString>;
226
232
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -309,6 +315,8 @@ export declare const LayerRowSchema: z.ZodObject<{
309
315
  recording: z.ZodOptional<z.ZodString>;
310
316
  available: z.ZodBoolean;
311
317
  reason: z.ZodOptional<z.ZodString>;
318
+ durationMs: z.ZodOptional<z.ZodNumber>;
319
+ poster: z.ZodOptional<z.ZodString>;
312
320
  scope: z.ZodOptional<z.ZodObject<{
313
321
  surface: z.ZodOptional<z.ZodString>;
314
322
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -415,6 +423,8 @@ export declare const LayerRowsSchema: z.ZodObject<{
415
423
  recording: z.ZodOptional<z.ZodString>;
416
424
  available: z.ZodBoolean;
417
425
  reason: z.ZodOptional<z.ZodString>;
426
+ durationMs: z.ZodOptional<z.ZodNumber>;
427
+ poster: z.ZodOptional<z.ZodString>;
418
428
  scope: z.ZodOptional<z.ZodObject<{
419
429
  surface: z.ZodOptional<z.ZodString>;
420
430
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -523,6 +533,8 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
523
533
  recording: z.ZodOptional<z.ZodString>;
524
534
  available: z.ZodBoolean;
525
535
  reason: z.ZodOptional<z.ZodString>;
536
+ durationMs: z.ZodOptional<z.ZodNumber>;
537
+ poster: z.ZodOptional<z.ZodString>;
526
538
  scope: z.ZodOptional<z.ZodObject<{
527
539
  surface: z.ZodOptional<z.ZodString>;
528
540
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -625,6 +637,8 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
625
637
  recording: z.ZodOptional<z.ZodString>;
626
638
  available: z.ZodBoolean;
627
639
  reason: z.ZodOptional<z.ZodString>;
640
+ durationMs: z.ZodOptional<z.ZodNumber>;
641
+ poster: z.ZodOptional<z.ZodString>;
628
642
  scope: z.ZodOptional<z.ZodObject<{
629
643
  surface: z.ZodOptional<z.ZodString>;
630
644
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -722,6 +736,8 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
722
736
  recording: z.ZodOptional<z.ZodString>;
723
737
  available: z.ZodBoolean;
724
738
  reason: z.ZodOptional<z.ZodString>;
739
+ durationMs: z.ZodOptional<z.ZodNumber>;
740
+ poster: z.ZodOptional<z.ZodString>;
725
741
  scope: z.ZodOptional<z.ZodObject<{
726
742
  surface: z.ZodOptional<z.ZodString>;
727
743
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -824,6 +840,8 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
824
840
  recording: z.ZodOptional<z.ZodString>;
825
841
  available: z.ZodBoolean;
826
842
  reason: z.ZodOptional<z.ZodString>;
843
+ durationMs: z.ZodOptional<z.ZodNumber>;
844
+ poster: z.ZodOptional<z.ZodString>;
827
845
  scope: z.ZodOptional<z.ZodObject<{
828
846
  surface: z.ZodOptional<z.ZodString>;
829
847
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -942,6 +960,8 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
942
960
  recording: z.ZodOptional<z.ZodString>;
943
961
  available: z.ZodBoolean;
944
962
  reason: z.ZodOptional<z.ZodString>;
963
+ durationMs: z.ZodOptional<z.ZodNumber>;
964
+ poster: z.ZodOptional<z.ZodString>;
945
965
  scope: z.ZodOptional<z.ZodObject<{
946
966
  surface: z.ZodOptional<z.ZodString>;
947
967
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1044,6 +1064,8 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
1044
1064
  recording: z.ZodOptional<z.ZodString>;
1045
1065
  available: z.ZodBoolean;
1046
1066
  reason: z.ZodOptional<z.ZodString>;
1067
+ durationMs: z.ZodOptional<z.ZodNumber>;
1068
+ poster: z.ZodOptional<z.ZodString>;
1047
1069
  scope: z.ZodOptional<z.ZodObject<{
1048
1070
  surface: z.ZodOptional<z.ZodString>;
1049
1071
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1154,6 +1176,8 @@ export declare const EvidencePageSchema: z.ZodObject<{
1154
1176
  recording: z.ZodOptional<z.ZodString>;
1155
1177
  available: z.ZodBoolean;
1156
1178
  reason: z.ZodOptional<z.ZodString>;
1179
+ durationMs: z.ZodOptional<z.ZodNumber>;
1180
+ poster: z.ZodOptional<z.ZodString>;
1157
1181
  scope: z.ZodOptional<z.ZodObject<{
1158
1182
  surface: z.ZodOptional<z.ZodString>;
1159
1183
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1735,6 +1759,8 @@ export declare const ReviewSnapshotSchema: z.ZodObject<{
1735
1759
  recording: z.ZodOptional<z.ZodString>;
1736
1760
  available: z.ZodBoolean;
1737
1761
  reason: z.ZodOptional<z.ZodString>;
1762
+ durationMs: z.ZodOptional<z.ZodNumber>;
1763
+ poster: z.ZodOptional<z.ZodString>;
1738
1764
  scope: z.ZodOptional<z.ZodObject<{
1739
1765
  surface: z.ZodOptional<z.ZodString>;
1740
1766
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1799,6 +1825,8 @@ export declare const ReviewEntrySchema: z.ZodObject<{
1799
1825
  recording: z.ZodOptional<z.ZodString>;
1800
1826
  available: z.ZodBoolean;
1801
1827
  reason: z.ZodOptional<z.ZodString>;
1828
+ durationMs: z.ZodOptional<z.ZodNumber>;
1829
+ poster: z.ZodOptional<z.ZodString>;
1802
1830
  scope: z.ZodOptional<z.ZodObject<{
1803
1831
  surface: z.ZodOptional<z.ZodString>;
1804
1832
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -1882,6 +1910,8 @@ export declare const ReviewCollectionSchema: z.ZodObject<{
1882
1910
  recording: z.ZodOptional<z.ZodString>;
1883
1911
  available: z.ZodBoolean;
1884
1912
  reason: z.ZodOptional<z.ZodString>;
1913
+ durationMs: z.ZodOptional<z.ZodNumber>;
1914
+ poster: z.ZodOptional<z.ZodString>;
1885
1915
  scope: z.ZodOptional<z.ZodObject<{
1886
1916
  surface: z.ZodOptional<z.ZodString>;
1887
1917
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2042,6 +2072,8 @@ export declare const ReviewCollectionDetailSchema: z.ZodObject<{
2042
2072
  recording: z.ZodOptional<z.ZodString>;
2043
2073
  available: z.ZodBoolean;
2044
2074
  reason: z.ZodOptional<z.ZodString>;
2075
+ durationMs: z.ZodOptional<z.ZodNumber>;
2076
+ poster: z.ZodOptional<z.ZodString>;
2045
2077
  scope: z.ZodOptional<z.ZodObject<{
2046
2078
  surface: z.ZodOptional<z.ZodString>;
2047
2079
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2174,6 +2206,8 @@ export declare const ReviewManifestSchema: z.ZodObject<{
2174
2206
  recording: z.ZodOptional<z.ZodString>;
2175
2207
  available: z.ZodBoolean;
2176
2208
  reason: z.ZodOptional<z.ZodString>;
2209
+ durationMs: z.ZodOptional<z.ZodNumber>;
2210
+ poster: z.ZodOptional<z.ZodString>;
2177
2211
  scope: z.ZodOptional<z.ZodObject<{
2178
2212
  surface: z.ZodOptional<z.ZodString>;
2179
2213
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2303,6 +2337,8 @@ export declare const TestResultEntrySchema: z.ZodObject<{
2303
2337
  recording: z.ZodOptional<z.ZodString>;
2304
2338
  available: z.ZodBoolean;
2305
2339
  reason: z.ZodOptional<z.ZodString>;
2340
+ durationMs: z.ZodOptional<z.ZodNumber>;
2341
+ poster: z.ZodOptional<z.ZodString>;
2306
2342
  scope: z.ZodOptional<z.ZodObject<{
2307
2343
  surface: z.ZodOptional<z.ZodString>;
2308
2344
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2414,6 +2450,8 @@ export declare const TestSummarySchema: z.ZodObject<{
2414
2450
  recording: z.ZodOptional<z.ZodString>;
2415
2451
  available: z.ZodBoolean;
2416
2452
  reason: z.ZodOptional<z.ZodString>;
2453
+ durationMs: z.ZodOptional<z.ZodNumber>;
2454
+ poster: z.ZodOptional<z.ZodString>;
2417
2455
  scope: z.ZodOptional<z.ZodObject<{
2418
2456
  surface: z.ZodOptional<z.ZodString>;
2419
2457
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2721,6 +2759,8 @@ export declare const TestCatalogSchema: z.ZodObject<{
2721
2759
  recording: z.ZodOptional<z.ZodString>;
2722
2760
  available: z.ZodBoolean;
2723
2761
  reason: z.ZodOptional<z.ZodString>;
2762
+ durationMs: z.ZodOptional<z.ZodNumber>;
2763
+ poster: z.ZodOptional<z.ZodString>;
2724
2764
  scope: z.ZodOptional<z.ZodObject<{
2725
2765
  surface: z.ZodOptional<z.ZodString>;
2726
2766
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -2960,6 +3000,8 @@ export declare const TestHistorySchema: z.ZodObject<{
2960
3000
  recording: z.ZodOptional<z.ZodString>;
2961
3001
  available: z.ZodBoolean;
2962
3002
  reason: z.ZodOptional<z.ZodString>;
3003
+ durationMs: z.ZodOptional<z.ZodNumber>;
3004
+ poster: z.ZodOptional<z.ZodString>;
2963
3005
  scope: z.ZodOptional<z.ZodObject<{
2964
3006
  surface: z.ZodOptional<z.ZodString>;
2965
3007
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -3089,6 +3131,8 @@ export declare const TestHistorySchema: z.ZodObject<{
3089
3131
  recording: z.ZodOptional<z.ZodString>;
3090
3132
  available: z.ZodBoolean;
3091
3133
  reason: z.ZodOptional<z.ZodString>;
3134
+ durationMs: z.ZodOptional<z.ZodNumber>;
3135
+ poster: z.ZodOptional<z.ZodString>;
3092
3136
  scope: z.ZodOptional<z.ZodObject<{
3093
3137
  surface: z.ZodOptional<z.ZodString>;
3094
3138
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -3497,6 +3541,8 @@ export declare const ArtifactResultSchema: z.ZodObject<{
3497
3541
  recording: z.ZodOptional<z.ZodString>;
3498
3542
  available: z.ZodBoolean;
3499
3543
  reason: z.ZodOptional<z.ZodString>;
3544
+ durationMs: z.ZodOptional<z.ZodNumber>;
3545
+ poster: z.ZodOptional<z.ZodString>;
3500
3546
  scope: z.ZodOptional<z.ZodObject<{
3501
3547
  surface: z.ZodOptional<z.ZodString>;
3502
3548
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -3568,6 +3614,8 @@ export declare const ArtifactListSchema: z.ZodObject<{
3568
3614
  recording: z.ZodOptional<z.ZodString>;
3569
3615
  available: z.ZodBoolean;
3570
3616
  reason: z.ZodOptional<z.ZodString>;
3617
+ durationMs: z.ZodOptional<z.ZodNumber>;
3618
+ poster: z.ZodOptional<z.ZodString>;
3571
3619
  scope: z.ZodOptional<z.ZodObject<{
3572
3620
  surface: z.ZodOptional<z.ZodString>;
3573
3621
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -3722,6 +3770,8 @@ export declare const RunDetailSchema: z.ZodObject<{
3722
3770
  recording: z.ZodOptional<z.ZodString>;
3723
3771
  available: z.ZodBoolean;
3724
3772
  reason: z.ZodOptional<z.ZodString>;
3773
+ durationMs: z.ZodOptional<z.ZodNumber>;
3774
+ poster: z.ZodOptional<z.ZodString>;
3725
3775
  scope: z.ZodOptional<z.ZodObject<{
3726
3776
  surface: z.ZodOptional<z.ZodString>;
3727
3777
  deviceKind: z.ZodOptional<z.ZodString>;
@@ -72,6 +72,8 @@ export const VideoRefSchema = z.looseObject({
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
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'),
75
+ durationMs: z.number().nonnegative().optional().describe('How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers'),
76
+ poster: z.string().optional().describe('The bundle key, in the same layer, of the image the producer named as this recording\'s poster: a hint to show before the file is opened. Absent when there is none or the layer\'s bundle does not hold it, and in documents of older servers'),
75
77
  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')
76
78
  });
77
79
  /** Video references as counts: how many are referenced, how many distinct recordings can be played, how many cannot. */
@@ -2181,6 +2181,10 @@ export type VideoRef = {
2181
2181
  readonly available: boolean;
2182
2182
  /** Why it is unavailable, when the report can tell: `not-in-bundle` names a file the layer should hold (its bundle key) but does not, a recording that was acknowledged and is no longer there. Absent otherwise, and for an available video. An open vocabulary: treat an unknown reason as plain unavailable. */
2183
2183
  readonly reason?: 'not-in-bundle';
2184
+ /** How long the recording is, in milliseconds, as its producer declared (`Attachment.durationMs`): a hint to show before the file is opened, never measured here and never a latency. Absent when none was declared, and in documents of older servers. */
2185
+ readonly durationMs?: number;
2186
+ /** The bundle key, in the same layer, of the image the producer named as this recording's poster (`Attachment.poster`): a hint to show before the file is opened. Only an image the layer's bundle holds; absent when there is none, and in documents of older servers. */
2187
+ readonly poster?: string;
2184
2188
  /** What this recording shows, as declared for it: its own `scope`, else its attempt's, check's, part's or layer's. Absent when none declared one: its scope is unknown, not live, physical or audible. */
2185
2189
  readonly scope?: EvidenceScope;
2186
2190
  };
@@ -27,6 +27,7 @@ decided exactly as before ([the report contract](report-contract.md)). Evidence
27
27
  | `attempts` | check | Every try of a retried check, with its own status, detail, steps and attachments. A check that passed after failing is flaky. |
28
28
  | `tags`, `labels` | check, layer | Filters and groups (`smoke`, `flaky`, an owner, a browser). |
29
29
  | `tags` | attachment | Marks one recording or screenshot, for example `demo`: shown as a `#demo` chip, and searchable. See [Tagging artifacts and demos](#tagging-artifacts-and-demos). |
30
+ | `poster` (the `id` of an image in the same list) and `durationMs` | attachment (video) | Shown beside the recording in a watch playlist before anything is opened: the report carries them as the optional `poster` (a bundle key) and `durationMs` of each video reference, so no evidence is read per entry. Neither is ever a latency. |
30
31
  | `extensions['atlas.media']` | attachment (video) | Where the check's steps are in the recording, so a click on a step seeks the video. See [Place steps in a recording](#place-steps-in-a-recording). |
31
32
  | `scope` | attachment, attempt, check, part, layer | What a recording does and does not show: where it ran, whether data and dependencies were real, whether audio was captured. See [Evidence scope](#evidence-scope). |
32
33
  | `annotations`, `links` | check, step, attempt, layer | Notes, warnings and issue links next to the result. |
package/docs/ui.md CHANGED
@@ -104,6 +104,37 @@ when Watch is on screen.
104
104
  - The spec index searches titles, paths and declared domains, and can group by the domain from spec metadata (specs with
105
105
  none are listed under "No domain declared"; the Tests view reports domain problems).
106
106
 
107
+ ## Behaviors across the project
108
+
109
+ `/behaviors` (`evidence/behaviors-page.tsx`, **Behaviors** in the spec-type menu) lists every behavior of every Gherkin feature in one
110
+ place, so the question "what does this product promise?" needs no knowledge of which file holds the answer. It adds no source of
111
+ truth: `evidence/behavior-index.ts` reads each feature through the same `featureBehaviors` the feature page uses, so a behavior has the
112
+ same coverage state, revisions and recording in both.
113
+
114
+ - The list is searched with the shared matcher (`find/`: words, quoted phrases, `status:`, `domain:`, `tag:`, `has:video`), grouped by
115
+ the domain spec metadata declares ("No domain declared" when it declares none), or ranked best match first when words are typed.
116
+ A coverage state is a filter with its own count; `demonstrated` is always named. A long list is bounded with *Show more*.
117
+ - Features whose text has not arrived are counted ("3 of 12 read"), and features that cannot be parsed are named, so an empty answer is
118
+ never taken for "no such behavior" while it is still incomplete.
119
+ - The search words (`q`), the coverage filter (`coverage`), the selection (`behavior`) and the expanded state (`workspace`) are in the
120
+ address. Selecting shows the Read / Watch / Inspect panel beside the list (below it on narrow screens), and *Read and watch* in the
121
+ selected-behavior bar is reachable before the list in keyboard order.
122
+ - **Read and watch workspace.** Expanding a behavior replaces the index with a page that uses the width: a two-column
123
+ `SimpleGrid` (one column below `md`) with *What should happen* (the steps, an outline's example filled in) beside *Recording* (the
124
+ player, status and provenance, promise and evidence revisions, applicability), the scoped summary below, and Inspect behind a
125
+ disclosure that reads the technical record only when opened. Keyboard focus moves to the behavior's heading on arrival, and
126
+ *Previous behavior* / *Next behavior* move through the same search. A recording is labelled illustrative.
127
+ - **Returning** from the workspace restores the search words, the filter and the selection from the address, and the scroll position,
128
+ the number of rows shown and the focused row from `evidence/behavior-return.ts`, which remembers them for the session against the
129
+ scope they were taken in and gives them back once, only to that scope. Storage that is unavailable means the list opens at its top.
130
+ - **Scoped summary** (`evidence/scoped-summary.ts`, `scoped-summary-view.tsx`, on Watch and in the workspace) answers three questions
131
+ apart, for the check in the environment of the recording shown (environments are not pooled; with none named it says they are):
132
+ the **outcome** of the shown result (`outcomeHeadline`), **reliability** (`reports/reliability`: always with the independent
133
+ executions it rests on, "Insufficient evidence" below the minimum, first-try success only from recorded tries, the library's
134
+ warnings) and **performance** (`reports/latency`: only measurements named `product-latency`, percentiles withheld with the reason,
135
+ budget and breaches). Absent evidence is stated as unknown, never as good; the test's duration is not product latency, and the
136
+ summary says it describes the checks Atlas holds, not production. The full reliability and latency detail is one link away.
137
+
107
138
  ## Catalog health
108
139
 
109
140
  The **Catalog health** tab (`explorer/HealthView.tsx`, `?tab=health`; `@crvouga/atlas/reports/catalog-health`) explains why the Specs
@@ -209,7 +240,14 @@ over the same filtered set the page lists, in the same order (`explorer/playlist
209
240
  demonstrated. A set longer than the page's limit says it is only the newest results.
210
241
  - Only the selected clip is read: the list is the metadata the catalog already sent, the selected result's evidence is read
211
242
  when it is selected, and the one `<video>` uses `preload="metadata"`. Opening the playlist downloads no recordings in bulk.
212
- Posters and durations of unselected entries are not shown for that reason.
243
+ An unselected entry still shows a poster and a length when the catalog already carries them: the producer's declared
244
+ `Attachment.durationMs` and `Attachment.poster` (an image of the same list that the layer's bundle holds) travel as the
245
+ optional `durationMs` and `poster` of the result's video reference. The poster is an `<img loading="lazy">`, fetched when
246
+ its row nears the screen; an entry whose producer declared neither shows neither, and nothing is read to find out.
247
+ - The open playlist is in the address (`watch=true`, and `watchClip` for the entry playing), so a link restores it over
248
+ the same filters, run or collection. Opening it adds a history entry (Back closes it); moving between clips and closing
249
+ it replace the current one. A `watchClip` the set no longer holds opens the first watchable entry and the address then
250
+ names that one.
213
251
  - One player: selecting another clip replaces it, and the position of the clip left is kept for the session so returning
214
252
  resumes there. Previous/next and advancing on end move over the watchable entries; a result with nothing to play is
215
253
  selected from the list to read why.
@@ -350,6 +388,7 @@ The narrowly scoped exceptions are:
350
388
  | Project home | Tests/runs/artifacts/compare/catalog health tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
351
389
  | Test detail | Current outcomes, statistics, duration graph, product latency with budgets and baselines, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
352
390
  | Specs | All six existing kinds, overview/index with title search and folder or declared-domain grouping, Gherkin rules/scenarios/examples with a coverage summary and Read / Watch / Inspect per behavior, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Tabs, Table, CodeHighlightTabs, ResultRow |
391
+ | Behaviors | Project-wide behavior index with search, coverage filters and a selected behavior's Read / Watch / Inspect; an expanded read-and-watch workspace with a scoped outcome, reliability and performance summary that restores scope, scroll, selection and focus on return | Container, Grid, SimpleGrid, NavLink, TextInput, Button, Paper, Accordion, Tabs |
353
392
  | Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
354
393
  | Evidence | Steps, retries, annotations, facts, unknown extensions, safe downloads and previews | List, Badge, Accordion, Code, Image, native media |
355
394
  | Recordings | Pictures/video/timeline, actor selection, moments, failure jumps, timing, model-state differences | Tabs, Slider, Button, ScrollArea, Table, Progress |
package/llms-full.txt CHANGED
@@ -1016,6 +1016,7 @@ decided exactly as before ([the report contract](report-contract.md)). Evidence
1016
1016
  | `attempts` | check | Every try of a retried check, with its own status, detail, steps and attachments. A check that passed after failing is flaky. |
1017
1017
  | `tags`, `labels` | check, layer | Filters and groups (`smoke`, `flaky`, an owner, a browser). |
1018
1018
  | `tags` | attachment | Marks one recording or screenshot, for example `demo`: shown as a `#demo` chip, and searchable. See [Tagging artifacts and demos](#tagging-artifacts-and-demos). |
1019
+ | `poster` (the `id` of an image in the same list) and `durationMs` | attachment (video) | Shown beside the recording in a watch playlist before anything is opened: the report carries them as the optional `poster` (a bundle key) and `durationMs` of each video reference, so no evidence is read per entry. Neither is ever a latency. |
1019
1020
  | `extensions['atlas.media']` | attachment (video) | Where the check's steps are in the recording, so a click on a step seeks the video. See [Place steps in a recording](#place-steps-in-a-recording). |
1020
1021
  | `scope` | attachment, attempt, check, part, layer | What a recording does and does not show: where it ran, whether data and dependencies were real, whether audio was captured. See [Evidence scope](#evidence-scope). |
1021
1022
  | `annotations`, `links` | check, step, attempt, layer | Notes, warnings and issue links next to the result. |
@@ -4560,6 +4561,37 @@ when Watch is on screen.
4560
4561
  - The spec index searches titles, paths and declared domains, and can group by the domain from spec metadata (specs with
4561
4562
  none are listed under "No domain declared"; the Tests view reports domain problems).
4562
4563
 
4564
+ ## Behaviors across the project
4565
+
4566
+ `/behaviors` (`evidence/behaviors-page.tsx`, **Behaviors** in the spec-type menu) lists every behavior of every Gherkin feature in one
4567
+ place, so the question "what does this product promise?" needs no knowledge of which file holds the answer. It adds no source of
4568
+ truth: `evidence/behavior-index.ts` reads each feature through the same `featureBehaviors` the feature page uses, so a behavior has the
4569
+ same coverage state, revisions and recording in both.
4570
+
4571
+ - The list is searched with the shared matcher (`find/`: words, quoted phrases, `status:`, `domain:`, `tag:`, `has:video`), grouped by
4572
+ the domain spec metadata declares ("No domain declared" when it declares none), or ranked best match first when words are typed.
4573
+ A coverage state is a filter with its own count; `demonstrated` is always named. A long list is bounded with *Show more*.
4574
+ - Features whose text has not arrived are counted ("3 of 12 read"), and features that cannot be parsed are named, so an empty answer is
4575
+ never taken for "no such behavior" while it is still incomplete.
4576
+ - The search words (`q`), the coverage filter (`coverage`), the selection (`behavior`) and the expanded state (`workspace`) are in the
4577
+ address. Selecting shows the Read / Watch / Inspect panel beside the list (below it on narrow screens), and *Read and watch* in the
4578
+ selected-behavior bar is reachable before the list in keyboard order.
4579
+ - **Read and watch workspace.** Expanding a behavior replaces the index with a page that uses the width: a two-column
4580
+ `SimpleGrid` (one column below `md`) with *What should happen* (the steps, an outline's example filled in) beside *Recording* (the
4581
+ player, status and provenance, promise and evidence revisions, applicability), the scoped summary below, and Inspect behind a
4582
+ disclosure that reads the technical record only when opened. Keyboard focus moves to the behavior's heading on arrival, and
4583
+ *Previous behavior* / *Next behavior* move through the same search. A recording is labelled illustrative.
4584
+ - **Returning** from the workspace restores the search words, the filter and the selection from the address, and the scroll position,
4585
+ the number of rows shown and the focused row from `evidence/behavior-return.ts`, which remembers them for the session against the
4586
+ scope they were taken in and gives them back once, only to that scope. Storage that is unavailable means the list opens at its top.
4587
+ - **Scoped summary** (`evidence/scoped-summary.ts`, `scoped-summary-view.tsx`, on Watch and in the workspace) answers three questions
4588
+ apart, for the check in the environment of the recording shown (environments are not pooled; with none named it says they are):
4589
+ the **outcome** of the shown result (`outcomeHeadline`), **reliability** (`reports/reliability`: always with the independent
4590
+ executions it rests on, "Insufficient evidence" below the minimum, first-try success only from recorded tries, the library's
4591
+ warnings) and **performance** (`reports/latency`: only measurements named `product-latency`, percentiles withheld with the reason,
4592
+ budget and breaches). Absent evidence is stated as unknown, never as good; the test's duration is not product latency, and the
4593
+ summary says it describes the checks Atlas holds, not production. The full reliability and latency detail is one link away.
4594
+
4563
4595
  ## Catalog health
4564
4596
 
4565
4597
  The **Catalog health** tab (`explorer/HealthView.tsx`, `?tab=health`; `@crvouga/atlas/reports/catalog-health`) explains why the Specs
@@ -4665,7 +4697,14 @@ over the same filtered set the page lists, in the same order (`explorer/playlist
4665
4697
  demonstrated. A set longer than the page's limit says it is only the newest results.
4666
4698
  - Only the selected clip is read: the list is the metadata the catalog already sent, the selected result's evidence is read
4667
4699
  when it is selected, and the one `<video>` uses `preload="metadata"`. Opening the playlist downloads no recordings in bulk.
4668
- Posters and durations of unselected entries are not shown for that reason.
4700
+ An unselected entry still shows a poster and a length when the catalog already carries them: the producer's declared
4701
+ `Attachment.durationMs` and `Attachment.poster` (an image of the same list that the layer's bundle holds) travel as the
4702
+ optional `durationMs` and `poster` of the result's video reference. The poster is an `<img loading="lazy">`, fetched when
4703
+ its row nears the screen; an entry whose producer declared neither shows neither, and nothing is read to find out.
4704
+ - The open playlist is in the address (`watch=true`, and `watchClip` for the entry playing), so a link restores it over
4705
+ the same filters, run or collection. Opening it adds a history entry (Back closes it); moving between clips and closing
4706
+ it replace the current one. A `watchClip` the set no longer holds opens the first watchable entry and the address then
4707
+ names that one.
4669
4708
  - One player: selecting another clip replaces it, and the position of the clip left is kept for the session so returning
4670
4709
  resumes there. Previous/next and advancing on end move over the watchable entries; a result with nothing to play is
4671
4710
  selected from the list to read why.
@@ -4806,6 +4845,7 @@ The narrowly scoped exceptions are:
4806
4845
  | Project home | Tests/runs/artifacts/compare/catalog health tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
4807
4846
  | Test detail | Current outcomes, statistics, duration graph, product latency with budgets and baselines, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
4808
4847
  | Specs | All six existing kinds, overview/index with title search and folder or declared-domain grouping, Gherkin rules/scenarios/examples with a coverage summary and Read / Watch / Inspect per behavior, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Tabs, Table, CodeHighlightTabs, ResultRow |
4848
+ | Behaviors | Project-wide behavior index with search, coverage filters and a selected behavior's Read / Watch / Inspect; an expanded read-and-watch workspace with a scoped outcome, reliability and performance summary that restores scope, scroll, selection and focus on return | Container, Grid, SimpleGrid, NavLink, TextInput, Button, Paper, Accordion, Tabs |
4809
4849
  | Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
4810
4850
  | Evidence | Steps, retries, annotations, facts, unknown extensions, safe downloads and previews | List, Badge, Accordion, Code, Image, native media |
4811
4851
  | Recordings | Pictures/video/timeline, actor selection, moments, failure jumps, timing, model-state differences | Tabs, Slider, Button, ScrollArea, Table, Progress |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/atlas",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Atlas: specs of any kind, reported as layered evidence. Publish checks from any runner (JUnit, CTRF, a script), combine them across nodes, branches, environments and backends, gate CI on the result, and run statecharts against the real app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -268,6 +268,15 @@
268
268
  "description": "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",
269
269
  "type": "string"
270
270
  },
271
+ "durationMs": {
272
+ "description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
273
+ "type": "number",
274
+ "minimum": 0
275
+ },
276
+ "poster": {
277
+ "description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
278
+ "type": "string"
279
+ },
271
280
  "scope": {
272
281
  "description": "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",
273
282
  "type": "object",
@@ -751,6 +760,15 @@
751
760
  "description": "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",
752
761
  "type": "string"
753
762
  },
763
+ "durationMs": {
764
+ "description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
765
+ "type": "number",
766
+ "minimum": 0
767
+ },
768
+ "poster": {
769
+ "description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
770
+ "type": "string"
771
+ },
754
772
  "scope": {
755
773
  "description": "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",
756
774
  "type": "object",
@@ -268,6 +268,15 @@
268
268
  "description": "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",
269
269
  "type": "string"
270
270
  },
271
+ "durationMs": {
272
+ "description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
273
+ "type": "number",
274
+ "minimum": 0
275
+ },
276
+ "poster": {
277
+ "description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
278
+ "type": "string"
279
+ },
271
280
  "scope": {
272
281
  "description": "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",
273
282
  "type": "object",
@@ -751,6 +760,15 @@
751
760
  "description": "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",
752
761
  "type": "string"
753
762
  },
763
+ "durationMs": {
764
+ "description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
765
+ "type": "number",
766
+ "minimum": 0
767
+ },
768
+ "poster": {
769
+ "description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
770
+ "type": "string"
771
+ },
754
772
  "scope": {
755
773
  "description": "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",
756
774
  "type": "object",
@@ -255,6 +255,15 @@
255
255
  "description": "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",
256
256
  "type": "string"
257
257
  },
258
+ "durationMs": {
259
+ "description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
260
+ "type": "number",
261
+ "minimum": 0
262
+ },
263
+ "poster": {
264
+ "description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
265
+ "type": "string"
266
+ },
258
267
  "scope": {
259
268
  "description": "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",
260
269
  "type": "object",
@@ -259,6 +259,15 @@
259
259
  "description": "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",
260
260
  "type": "string"
261
261
  },
262
+ "durationMs": {
263
+ "description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
264
+ "type": "number",
265
+ "minimum": 0
266
+ },
267
+ "poster": {
268
+ "description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
269
+ "type": "string"
270
+ },
262
271
  "scope": {
263
272
  "description": "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",
264
273
  "type": "object",
@@ -384,6 +384,15 @@
384
384
  "description": "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",
385
385
  "type": "string"
386
386
  },
387
+ "durationMs": {
388
+ "description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
389
+ "type": "number",
390
+ "minimum": 0
391
+ },
392
+ "poster": {
393
+ "description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
394
+ "type": "string"
395
+ },
387
396
  "scope": {
388
397
  "description": "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",
389
398
  "type": "object",
@@ -200,6 +200,15 @@
200
200
  "description": "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",
201
201
  "type": "string"
202
202
  },
203
+ "durationMs": {
204
+ "description": "How long the recording is, in milliseconds, as its producer declared it: a hint to show before the file is opened, never a latency. Absent when none was declared, and in documents of older servers",
205
+ "type": "number",
206
+ "minimum": 0
207
+ },
208
+ "poster": {
209
+ "description": "The bundle key, in the same layer, of the image the producer named as this recording's poster: a hint to show before the file is opened. Absent when there is none or the layer's bundle does not hold it, and in documents of older servers",
210
+ "type": "string"
211
+ },
203
212
  "scope": {
204
213
  "description": "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",
205
214
  "type": "object",