@crvouga/atlas 1.18.0 → 1.20.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 (56) hide show
  1. package/CHANGELOG.md +14 -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 +105 -0
  5. package/dist/schema/src/api.js +35 -0
  6. package/dist/server/src/app.js +3 -1
  7. package/dist/server/src/openapi.d.ts +1 -1
  8. package/dist/server/src/openapi.js +24 -2
  9. package/dist/server/src/recording-verification.d.ts +34 -0
  10. package/dist/server/src/recording-verification.js +118 -0
  11. package/docs/reference/http-api.md +52 -0
  12. package/docs/reference/reports.md +4 -0
  13. package/docs/reference/schema.md +41 -0
  14. package/docs/reference/server.md +39 -0
  15. package/docs/rich-evidence.md +13 -0
  16. package/docs/server.md +12 -0
  17. package/docs/ui.md +40 -1
  18. package/llms-full.txt +117 -1
  19. package/package.json +1 -1
  20. package/packages/schema/schemas/effective-delta.schema.json +18 -0
  21. package/packages/schema/schemas/effective-evidence.schema.json +18 -0
  22. package/packages/schema/schemas/evidence-page.schema.json +9 -0
  23. package/packages/schema/schemas/layer-rows.schema.json +9 -0
  24. package/packages/schema/schemas/report-run.schema.json +9 -0
  25. package/packages/schema/schemas/review-collection.schema.json +9 -0
  26. package/packages/schema/schemas/review-manifest.schema.json +9 -0
  27. package/packages/schema/schemas/test-catalog.schema.json +9 -0
  28. package/packages/schema/schemas/test-history.schema.json +18 -0
  29. package/packages/server/openapi.json +653 -1
  30. package/packages/visualizer/src/app/AppShell.tsx +2 -2
  31. package/packages/visualizer/src/app/router.tsx +25 -1
  32. package/packages/visualizer/src/components/Header.tsx +15 -4
  33. package/packages/visualizer/src/evidence/behavior-index.test.ts +119 -0
  34. package/packages/visualizer/src/evidence/behavior-index.ts +162 -0
  35. package/packages/visualizer/src/evidence/behavior-panel.tsx +36 -22
  36. package/packages/visualizer/src/evidence/behavior-return.test.ts +41 -0
  37. package/packages/visualizer/src/evidence/behavior-return.ts +59 -0
  38. package/packages/visualizer/src/evidence/behavior.ts +32 -1
  39. package/packages/visualizer/src/evidence/behaviors-page.tsx +465 -0
  40. package/packages/visualizer/src/evidence/feature-page.tsx +5 -26
  41. package/packages/visualizer/src/evidence/scoped-summary-view.tsx +110 -0
  42. package/packages/visualizer/src/evidence/scoped-summary.test.ts +62 -0
  43. package/packages/visualizer/src/evidence/scoped-summary.ts +118 -0
  44. package/packages/visualizer/src/evidence/spec-texts.ts +88 -0
  45. package/packages/visualizer/src/explorer/ArtifactsView.tsx +4 -4
  46. package/packages/visualizer/src/explorer/Explorer.tsx +1 -1
  47. package/packages/visualizer/src/explorer/RunsView.tsx +36 -10
  48. package/packages/visualizer/src/explorer/catalog.tsx +24 -1
  49. package/packages/visualizer/src/explorer/collections.tsx +5 -5
  50. package/packages/visualizer/src/explorer/navigate.ts +17 -0
  51. package/packages/visualizer/src/explorer/playlist-entries.test.ts +10 -1
  52. package/packages/visualizer/src/explorer/playlist-entries.ts +10 -0
  53. package/packages/visualizer/src/explorer/playlist.tsx +43 -8
  54. package/packages/visualizer/src/explorer/recording-verification.ts +58 -0
  55. package/packages/visualizer/src/explorer/search.ts +7 -1
  56. package/packages/visualizer/src/find/Finder.tsx +1 -62
@@ -694,6 +694,58 @@ Scoped to one project. The same project may be selected on the unprefixed API us
694
694
 
695
695
  Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `413` Request too large.; `422` Not a tag edit: a bad target, an empty or over-long tag, or nothing to change.
696
696
 
697
+ ### Which stored recordings the server last read back, and which were gone or damaged
698
+
699
+ `GET /api/reports/recordings/verification` · role: read
700
+
701
+ The newest read-back the server did of stored videos, per layer: how many it checked, how many passed and the `gaps` (a blob that is gone, bytes of another size or hash than were published, or a file that does not begin like its container), each naming the check that holds the video. `run` is the key of the run the layer belongs to (a `RunSummary.key`), so a viewer can mark that run's recordings as stored-then-unavailable beside its test results.
702
+
703
+ This route never reads a blob: it returns what a publish token's `POST` found earlier. A layer that was never read back is absent, which means "not verified", not "fine"; the result is kept in the server's memory only, so a restart empties it. Only layers a store still holds are listed (one removed by garbage collection drops out). No status, slot or winner depends on it. It checks stored bytes, not browser playback. Narrow with `?run=` (a run key) and `?layer=` (an id prefix of at least 7 hex digits, repeatable).
704
+
705
+ - `run` (query): Only layers of this run (a key from `GET /api/reports/runs`).
706
+ - `layer` (query): Only layers whose id starts with this prefix. Repeatable or comma-separated.
707
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
708
+
709
+ Responses: `200` The held read-backs.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
710
+
711
+ ### Which stored recordings the server last read back, and which were gone or damaged
712
+
713
+ `GET /api/projects/{project}/reports/recordings/verification` · role: read
714
+
715
+ The newest read-back the server did of stored videos, per layer: how many it checked, how many passed and the `gaps` (a blob that is gone, bytes of another size or hash than were published, or a file that does not begin like its container), each naming the check that holds the video. `run` is the key of the run the layer belongs to (a `RunSummary.key`), so a viewer can mark that run's recordings as stored-then-unavailable beside its test results.
716
+
717
+ This route never reads a blob: it returns what a publish token's `POST` found earlier. A layer that was never read back is absent, which means "not verified", not "fine"; the result is kept in the server's memory only, so a restart empties it. Only layers a store still holds are listed (one removed by garbage collection drops out). No status, slot or winner depends on it. It checks stored bytes, not browser playback. Narrow with `?run=` (a run key) and `?layer=` (an id prefix of at least 7 hex digits, repeatable).
718
+
719
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
720
+
721
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
722
+ - `run` (query): Only layers of this run (a key from `GET /api/reports/runs`).
723
+ - `layer` (query): Only layers whose id starts with this prefix. Repeatable or comma-separated.
724
+
725
+ Responses: `200` The held read-backs.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
726
+
727
+ ### Read stored recordings back and check them (publish token)
728
+
729
+ `POST /api/reports/recordings/verification` · role: publish
730
+
731
+ Reads the videos of the newest layers (`limit`, default 20, at most 200; or the layers `layers` names) back from storage and checks that each is still stored, still has the size and SHA-256 published, and still begins like its media type's container. This reads blobs, so it needs a publish (or admin) token in a Bearer header; a reader-only token, a browser sign-in box and an anonymous reader are refused. One read-back runs at a time (`409` otherwise). It writes nothing to storage and changes no status. The answer is the same document `GET` returns, with `verified`: how many layers this pass read back.
732
+
733
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
734
+
735
+ Responses: `200` The read-backs now held.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` A read-back is already running.; `413` Request too large.; `422` Not a verification request.; `502` A store could not be read.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
736
+
737
+ ### Read stored recordings back and check them (publish token)
738
+
739
+ `POST /api/projects/{project}/reports/recordings/verification` · role: publish
740
+
741
+ Reads the videos of the newest layers (`limit`, default 20, at most 200; or the layers `layers` names) back from storage and checks that each is still stored, still has the size and SHA-256 published, and still begins like its media type's container. This reads blobs, so it needs a publish (or admin) token in a Bearer header; a reader-only token, a browser sign-in box and an anonymous reader are refused. One read-back runs at a time (`409` otherwise). It writes nothing to storage and changes no status. The answer is the same document `GET` returns, with `verified`: how many layers this pass read back.
742
+
743
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
744
+
745
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
746
+
747
+ Responses: `200` The read-backs now held.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` A read-back is already running.; `413` Request too large.; `422` Not a verification request.; `502` A store could not be read.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
748
+
697
749
  ### Review collections: curated sets of recordings, newest first
698
750
 
699
751
  `GET /api/reports/review-collections` · role: read
@@ -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
  };
@@ -233,6 +233,47 @@ export const ArtifactTagsSchema = …
233
233
 
234
234
  `GET /api/reports/artifact-tags`: every recorded edit, oldest first. Fold them with `foldArtifactTags`.
235
235
 
236
+ #### `StoredRecordingGapSchema`
237
+
238
+ ```ts
239
+ export const StoredRecordingGapSchema = …
240
+ ```
241
+
242
+ One stored video that failed the server's read-back: which check holds it and what is wrong with it.
243
+
244
+ #### `LayerRecordingCheckSchema`
245
+
246
+ ```ts
247
+ export const LayerRecordingCheckSchema = …
248
+ ```
249
+
250
+ What reading back the stored videos of one layer found.
251
+
252
+ #### `RecordingVerificationSchema`
253
+
254
+ ```ts
255
+ export const RecordingVerificationSchema = …
256
+ ```
257
+
258
+ `GET` and `POST /api/reports/recordings/verification`: the newest read-back the server did of stored videos, per layer.
259
+ A layer that was never read back is absent, which means "not verified", never "fine".
260
+
261
+ #### `RecordingVerification`
262
+
263
+ ```ts
264
+ export type RecordingVerification = z.infer<typeof RecordingVerificationSchema>;
265
+ ```
266
+
267
+ The document of {@link RecordingVerificationSchema}, as a type.
268
+
269
+ #### `RecordingVerifyRequestSchema`
270
+
271
+ ```ts
272
+ export const RecordingVerifyRequestSchema = …
273
+ ```
274
+
275
+ The body of `POST /api/reports/recordings/verification`. Both fields are optional; an empty body reads back the newest layers.
276
+
236
277
  #### `ReviewSourceSchema`
237
278
 
238
279
  ```ts
@@ -627,6 +627,45 @@ export function projectServer(options: AtlasServerOptions, create: (options: Atl
627
627
 
628
628
  Wrap the single-project protocol in project selection. Auth, upload validation and composition stay identical.
629
629
 
630
+ ### Recording verification routes
631
+
632
+ Source: `packages/server/src/recording-verification.ts`
633
+
634
+ `/api/reports/recordings/verification`: whether the videos a run published can still be read back from storage as
635
+ they were published, apart from whether the run's tests passed. A viewer reads the newest result with a read token
636
+ (`GET`, which never touches a blob). A publish token starts a read-back (`POST`), which does read blobs and so is
637
+ bounded: the newest 20 layers unless asked for more, at most 200, one pass at a time. The result is kept in this
638
+ process only (it is derived, not state: a restart means "not verified yet", never "fine"), and only for layers the
639
+ store still holds. It adds nothing to any layer and changes no status, slot or winner. This is the same check as
640
+ `atlas report verify-recordings`, and as that command it checks stored bytes, not browser playback.
641
+
642
+ See docs/rich-evidence.md#recording-completeness
643
+
644
+ #### `RecordingVerificationDeps`
645
+
646
+ ```ts
647
+ export type RecordingVerificationDeps = {
648
+ /** Every backend's store: a layer is read back from the store that holds it. */
649
+ readonly stores: readonly ObjectStore[];
650
+ readonly auth: Auth;
651
+ readonly index: ReportIndex;
652
+ readonly now: () => number;
653
+ readonly unauthorized: (verdict: Extract<ReturnType<Auth['check']>, { ok: false }>) => Response;
654
+ };
655
+ ```
656
+
657
+ What the verification routes need from the server around them.
658
+
659
+ #### `mountRecordingVerification`
660
+
661
+ ```ts
662
+ export function mountRecordingVerification(app: Hono, deps: RecordingVerificationDeps): void
663
+ ```
664
+
665
+ Registers `GET` and `POST /api/reports/recordings/verification` on `app`. The result of the last read-back of each
666
+ layer is held in memory, replaced by a newer one, and served only for layers a store still holds. A layer held by
667
+ two backends keeps the reading with more gaps, so a copy that lost its blobs is never hidden by one that did not.
668
+
630
669
  ### Review collection routes
631
670
 
632
671
  Source: `packages/server/src/review-collections.ts`
@@ -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. |
@@ -503,6 +504,18 @@ two ways without touching a check's result:
503
504
  `--format json` prints the `verify-recordings-result` document. `--layer <id>` and `--limit <n>` narrow the work, since
504
505
  it reads blobs. A store that cannot answer is `unreadable`, never verified. Test results are unchanged either way.
505
506
 
507
+ - **In the viewer.** A publish token starts the same read-back on the server with
508
+ `POST /api/reports/recordings/verification` (body optional: `{ "layers": ["<id prefix>"], "limit": 20 }`; the newest
509
+ 20 layers by default, at most 200, one read-back at a time; a reader token, a browser sign-in box or an anonymous
510
+ caller is refused because it reads blobs). Anyone who can read the report then sees the result with
511
+ `GET /api/reports/recordings/verification` (which reads no blob): per layer, its run, how many videos were read back, how
512
+ many passed and each gap. The Runs tab and an opened run show `Recording unavailable: 1 of 3 stored videos failed the
513
+ read-back (1 file gone from storage)` beside the green outcomes, or `3 stored videos verified`, and a run whose stored
514
+ videos all failed offers no Watch action. A run whose layers were never read back shows nothing: unknown is not
515
+ verified. The result lives in the server's memory only, so a restart empties it, and a layer that garbage collection
516
+ removed drops out. An older server has no such route and the viewer then shows nothing. No status, slot or winner is
517
+ affected; `capabilities.recordingVerification` in `GET /api/server` says a server offers it.
518
+
506
519
  Both are statements about stored bytes. Atlas does not check that a stored video plays in a given browser (a valid header
507
520
  can still have an unsupported codec or be truncated), and acknowledging an upload is not playback. Videos given only by
508
521
  `uri` are counted as referenced and not checked.
package/docs/server.md CHANGED
@@ -338,6 +338,14 @@ edit, and the server's time. Nothing about the layer, its results or the report'
338
338
  See [Tagging artifacts and demos](rich-evidence.md#tagging-artifacts-and-demos) for the tag rules. Reviewer tags are
339
339
  shown in the viewer's evidence; the tests API (`artifactTag`) searches the producers' tags.
340
340
 
341
+ ## Reading recordings back
342
+
343
+ `POST /api/reports/recordings/verification` (publish, `Authorization: Bearer`) reads the stored videos of the newest layers
344
+ back and checks them; `GET` (read) returns the newest result per layer without reading any blob, so the viewer can mark a
345
+ run whose recording was stored and then lost, apart from its test results. The result is in memory only and layers
346
+ that were never read back are absent (unknown, never "fine"). See
347
+ [Is the recording still there?](rich-evidence.md#is-the-recording-still-there) for what is checked and what is not.
348
+
341
349
  ## Review collections
342
350
 
343
351
  A team can publish a named, curated review of a few behaviors: recordings from several explicit executions, notes on
@@ -517,6 +525,10 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
517
525
  | `GET /api/projects/{project}/reports/artifact-tags` | read | Reviewer tags of attachments: every recorded edit. |
518
526
  | `POST /api/reports/artifact-tags` | publish | Tag or untag one attachment after upload. |
519
527
  | `POST /api/projects/{project}/reports/artifact-tags` | publish | Tag or untag one attachment after upload. |
528
+ | `GET /api/reports/recordings/verification` | read | Which stored recordings the server last read back, and which were gone or damaged. |
529
+ | `GET /api/projects/{project}/reports/recordings/verification` | read | Which stored recordings the server last read back, and which were gone or damaged. |
530
+ | `POST /api/reports/recordings/verification` | publish | Read stored recordings back and check them (publish token). |
531
+ | `POST /api/projects/{project}/reports/recordings/verification` | publish | Read stored recordings back and check them (publish token). |
520
532
  | `GET /api/reports/review-collections` | read | Review collections: curated sets of recordings, newest first. |
521
533
  | `GET /api/projects/{project}/reports/review-collections` | read | Review collections: curated sets of recordings, newest first. |
522
534
  | `POST /api/reports/review-collections` | publish | Publish an immutable review collection (or a new revision of one). |
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. |
@@ -1492,6 +1493,18 @@ two ways without touching a check's result:
1492
1493
  `--format json` prints the `verify-recordings-result` document. `--layer <id>` and `--limit <n>` narrow the work, since
1493
1494
  it reads blobs. A store that cannot answer is `unreadable`, never verified. Test results are unchanged either way.
1494
1495
 
1496
+ - **In the viewer.** A publish token starts the same read-back on the server with
1497
+ `POST /api/reports/recordings/verification` (body optional: `{ "layers": ["<id prefix>"], "limit": 20 }`; the newest
1498
+ 20 layers by default, at most 200, one read-back at a time; a reader token, a browser sign-in box or an anonymous
1499
+ caller is refused because it reads blobs). Anyone who can read the report then sees the result with
1500
+ `GET /api/reports/recordings/verification` (which reads no blob): per layer, its run, how many videos were read back, how
1501
+ many passed and each gap. The Runs tab and an opened run show `Recording unavailable: 1 of 3 stored videos failed the
1502
+ read-back (1 file gone from storage)` beside the green outcomes, or `3 stored videos verified`, and a run whose stored
1503
+ videos all failed offers no Watch action. A run whose layers were never read back shows nothing: unknown is not
1504
+ verified. The result lives in the server's memory only, so a restart empties it, and a layer that garbage collection
1505
+ removed drops out. An older server has no such route and the viewer then shows nothing. No status, slot or winner is
1506
+ affected; `capabilities.recordingVerification` in `GET /api/server` says a server offers it.
1507
+
1495
1508
  Both are statements about stored bytes. Atlas does not check that a stored video plays in a given browser (a valid header
1496
1509
  can still have an unsupported codec or be truncated), and acknowledging an upload is not playback. Videos given only by
1497
1510
  `uri` are counted as referenced and not checked.
@@ -2014,6 +2027,14 @@ edit, and the server's time. Nothing about the layer, its results or the report'
2014
2027
  See [Tagging artifacts and demos](rich-evidence.md#tagging-artifacts-and-demos) for the tag rules. Reviewer tags are
2015
2028
  shown in the viewer's evidence; the tests API (`artifactTag`) searches the producers' tags.
2016
2029
 
2030
+ ## Reading recordings back
2031
+
2032
+ `POST /api/reports/recordings/verification` (publish, `Authorization: Bearer`) reads the stored videos of the newest layers
2033
+ back and checks them; `GET` (read) returns the newest result per layer without reading any blob, so the viewer can mark a
2034
+ run whose recording was stored and then lost, apart from its test results. The result is in memory only and layers
2035
+ that were never read back are absent (unknown, never "fine"). See
2036
+ [Is the recording still there?](rich-evidence.md#is-the-recording-still-there) for what is checked and what is not.
2037
+
2017
2038
  ## Review collections
2018
2039
 
2019
2040
  A team can publish a named, curated review of a few behaviors: recordings from several explicit executions, notes on
@@ -2193,6 +2214,10 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
2193
2214
  | `GET /api/projects/{project}/reports/artifact-tags` | read | Reviewer tags of attachments: every recorded edit. |
2194
2215
  | `POST /api/reports/artifact-tags` | publish | Tag or untag one attachment after upload. |
2195
2216
  | `POST /api/projects/{project}/reports/artifact-tags` | publish | Tag or untag one attachment after upload. |
2217
+ | `GET /api/reports/recordings/verification` | read | Which stored recordings the server last read back, and which were gone or damaged. |
2218
+ | `GET /api/projects/{project}/reports/recordings/verification` | read | Which stored recordings the server last read back, and which were gone or damaged. |
2219
+ | `POST /api/reports/recordings/verification` | publish | Read stored recordings back and check them (publish token). |
2220
+ | `POST /api/projects/{project}/reports/recordings/verification` | publish | Read stored recordings back and check them (publish token). |
2196
2221
  | `GET /api/reports/review-collections` | read | Review collections: curated sets of recordings, newest first. |
2197
2222
  | `GET /api/projects/{project}/reports/review-collections` | read | Review collections: curated sets of recordings, newest first. |
2198
2223
  | `POST /api/reports/review-collections` | publish | Publish an immutable review collection (or a new revision of one). |
@@ -4560,6 +4585,37 @@ when Watch is on screen.
4560
4585
  - The spec index searches titles, paths and declared domains, and can group by the domain from spec metadata (specs with
4561
4586
  none are listed under "No domain declared"; the Tests view reports domain problems).
4562
4587
 
4588
+ ## Behaviors across the project
4589
+
4590
+ `/behaviors` (`evidence/behaviors-page.tsx`, **Behaviors** in the spec-type menu) lists every behavior of every Gherkin feature in one
4591
+ place, so the question "what does this product promise?" needs no knowledge of which file holds the answer. It adds no source of
4592
+ truth: `evidence/behavior-index.ts` reads each feature through the same `featureBehaviors` the feature page uses, so a behavior has the
4593
+ same coverage state, revisions and recording in both.
4594
+
4595
+ - The list is searched with the shared matcher (`find/`: words, quoted phrases, `status:`, `domain:`, `tag:`, `has:video`), grouped by
4596
+ the domain spec metadata declares ("No domain declared" when it declares none), or ranked best match first when words are typed.
4597
+ A coverage state is a filter with its own count; `demonstrated` is always named. A long list is bounded with *Show more*.
4598
+ - 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
4599
+ never taken for "no such behavior" while it is still incomplete.
4600
+ - The search words (`q`), the coverage filter (`coverage`), the selection (`behavior`) and the expanded state (`workspace`) are in the
4601
+ address. Selecting shows the Read / Watch / Inspect panel beside the list (below it on narrow screens), and *Read and watch* in the
4602
+ selected-behavior bar is reachable before the list in keyboard order.
4603
+ - **Read and watch workspace.** Expanding a behavior replaces the index with a page that uses the width: a two-column
4604
+ `SimpleGrid` (one column below `md`) with *What should happen* (the steps, an outline's example filled in) beside *Recording* (the
4605
+ player, status and provenance, promise and evidence revisions, applicability), the scoped summary below, and Inspect behind a
4606
+ disclosure that reads the technical record only when opened. Keyboard focus moves to the behavior's heading on arrival, and
4607
+ *Previous behavior* / *Next behavior* move through the same search. A recording is labelled illustrative.
4608
+ - **Returning** from the workspace restores the search words, the filter and the selection from the address, and the scroll position,
4609
+ the number of rows shown and the focused row from `evidence/behavior-return.ts`, which remembers them for the session against the
4610
+ 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.
4611
+ - **Scoped summary** (`evidence/scoped-summary.ts`, `scoped-summary-view.tsx`, on Watch and in the workspace) answers three questions
4612
+ apart, for the check in the environment of the recording shown (environments are not pooled; with none named it says they are):
4613
+ the **outcome** of the shown result (`outcomeHeadline`), **reliability** (`reports/reliability`: always with the independent
4614
+ executions it rests on, "Insufficient evidence" below the minimum, first-try success only from recorded tries, the library's
4615
+ warnings) and **performance** (`reports/latency`: only measurements named `product-latency`, percentiles withheld with the reason,
4616
+ budget and breaches). Absent evidence is stated as unknown, never as good; the test's duration is not product latency, and the
4617
+ summary says it describes the checks Atlas holds, not production. The full reliability and latency detail is one link away.
4618
+
4563
4619
  ## Catalog health
4564
4620
 
4565
4621
  The **Catalog health** tab (`explorer/HealthView.tsx`, `?tab=health`; `@crvouga/atlas/reports/catalog-health`) explains why the Specs
@@ -4665,7 +4721,14 @@ over the same filtered set the page lists, in the same order (`explorer/playlist
4665
4721
  demonstrated. A set longer than the page's limit says it is only the newest results.
4666
4722
  - Only the selected clip is read: the list is the metadata the catalog already sent, the selected result's evidence is read
4667
4723
  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.
4724
+ An unselected entry still shows a poster and a length when the catalog already carries them: the producer's declared
4725
+ `Attachment.durationMs` and `Attachment.poster` (an image of the same list that the layer's bundle holds) travel as the
4726
+ optional `durationMs` and `poster` of the result's video reference. The poster is an `<img loading="lazy">`, fetched when
4727
+ its row nears the screen; an entry whose producer declared neither shows neither, and nothing is read to find out.
4728
+ - The open playlist is in the address (`watch=true`, and `watchClip` for the entry playing), so a link restores it over
4729
+ the same filters, run or collection. Opening it adds a history entry (Back closes it); moving between clips and closing
4730
+ it replace the current one. A `watchClip` the set no longer holds opens the first watchable entry and the address then
4731
+ names that one.
4669
4732
  - One player: selecting another clip replaces it, and the position of the clip left is kept for the session so returning
4670
4733
  resumes there. Previous/next and advancing on end move over the watchable entries; a result with nothing to play is
4671
4734
  selected from the list to read why.
@@ -4806,6 +4869,7 @@ The narrowly scoped exceptions are:
4806
4869
  | 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
4870
  | 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
4871
  | 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 |
4872
+ | 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
4873
  | Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
4810
4874
  | Evidence | Steps, retries, annotations, facts, unknown extensions, safe downloads and previews | List, Badge, Accordion, Code, Image, native media |
4811
4875
  | Recordings | Pictures/video/timeline, actor selection, moments, failure jumps, timing, model-state differences | Tabs, Slider, Button, ScrollArea, Table, Progress |
@@ -6046,6 +6110,58 @@ Scoped to one project. The same project may be selected on the unprefixed API us
6046
6110
 
6047
6111
  Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `413` Request too large.; `422` Not a tag edit: a bad target, an empty or over-long tag, or nothing to change.
6048
6112
 
6113
+ ### Which stored recordings the server last read back, and which were gone or damaged
6114
+
6115
+ `GET /api/reports/recordings/verification` · role: read
6116
+
6117
+ The newest read-back the server did of stored videos, per layer: how many it checked, how many passed and the `gaps` (a blob that is gone, bytes of another size or hash than were published, or a file that does not begin like its container), each naming the check that holds the video. `run` is the key of the run the layer belongs to (a `RunSummary.key`), so a viewer can mark that run's recordings as stored-then-unavailable beside its test results.
6118
+
6119
+ This route never reads a blob: it returns what a publish token's `POST` found earlier. A layer that was never read back is absent, which means "not verified", not "fine"; the result is kept in the server's memory only, so a restart empties it. Only layers a store still holds are listed (one removed by garbage collection drops out). No status, slot or winner depends on it. It checks stored bytes, not browser playback. Narrow with `?run=` (a run key) and `?layer=` (an id prefix of at least 7 hex digits, repeatable).
6120
+
6121
+ - `run` (query): Only layers of this run (a key from `GET /api/reports/runs`).
6122
+ - `layer` (query): Only layers whose id starts with this prefix. Repeatable or comma-separated.
6123
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
6124
+
6125
+ Responses: `200` The held read-backs.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
6126
+
6127
+ ### Which stored recordings the server last read back, and which were gone or damaged
6128
+
6129
+ `GET /api/projects/{project}/reports/recordings/verification` · role: read
6130
+
6131
+ The newest read-back the server did of stored videos, per layer: how many it checked, how many passed and the `gaps` (a blob that is gone, bytes of another size or hash than were published, or a file that does not begin like its container), each naming the check that holds the video. `run` is the key of the run the layer belongs to (a `RunSummary.key`), so a viewer can mark that run's recordings as stored-then-unavailable beside its test results.
6132
+
6133
+ This route never reads a blob: it returns what a publish token's `POST` found earlier. A layer that was never read back is absent, which means "not verified", not "fine"; the result is kept in the server's memory only, so a restart empties it. Only layers a store still holds are listed (one removed by garbage collection drops out). No status, slot or winner depends on it. It checks stored bytes, not browser playback. Narrow with `?run=` (a run key) and `?layer=` (an id prefix of at least 7 hex digits, repeatable).
6134
+
6135
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
6136
+
6137
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
6138
+ - `run` (query): Only layers of this run (a key from `GET /api/reports/runs`).
6139
+ - `layer` (query): Only layers whose id starts with this prefix. Repeatable or comma-separated.
6140
+
6141
+ Responses: `200` The held read-backs.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
6142
+
6143
+ ### Read stored recordings back and check them (publish token)
6144
+
6145
+ `POST /api/reports/recordings/verification` · role: publish
6146
+
6147
+ Reads the videos of the newest layers (`limit`, default 20, at most 200; or the layers `layers` names) back from storage and checks that each is still stored, still has the size and SHA-256 published, and still begins like its media type's container. This reads blobs, so it needs a publish (or admin) token in a Bearer header; a reader-only token, a browser sign-in box and an anonymous reader are refused. One read-back runs at a time (`409` otherwise). It writes nothing to storage and changes no status. The answer is the same document `GET` returns, with `verified`: how many layers this pass read back.
6148
+
6149
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
6150
+
6151
+ Responses: `200` The read-backs now held.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` A read-back is already running.; `413` Request too large.; `422` Not a verification request.; `502` A store could not be read.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
6152
+
6153
+ ### Read stored recordings back and check them (publish token)
6154
+
6155
+ `POST /api/projects/{project}/reports/recordings/verification` · role: publish
6156
+
6157
+ Reads the videos of the newest layers (`limit`, default 20, at most 200; or the layers `layers` names) back from storage and checks that each is still stored, still has the size and SHA-256 published, and still begins like its media type's container. This reads blobs, so it needs a publish (or admin) token in a Bearer header; a reader-only token, a browser sign-in box and an anonymous reader are refused. One read-back runs at a time (`409` otherwise). It writes nothing to storage and changes no status. The answer is the same document `GET` returns, with `verified`: how many layers this pass read back.
6158
+
6159
+ Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
6160
+
6161
+ - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
6162
+
6163
+ Responses: `200` The read-backs now held.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` A read-back is already running.; `413` Request too large.; `422` Not a verification request.; `502` A store could not be read.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
6164
+
6049
6165
  ### Review collections: curated sets of recordings, newest first
6050
6166
 
6051
6167
  `GET /api/reports/review-collections` · role: read
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/atlas",
3
- "version": "1.18.0",
3
+ "version": "1.20.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",