@crvouga/atlas 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/dist/reports/src/artifact-query.d.ts +14 -2
  3. package/dist/reports/src/artifact-query.js +13 -4
  4. package/dist/reports/src/evidence-detail.js +7 -4
  5. package/dist/reports/src/evidence-types.d.ts +4 -0
  6. package/dist/reports/src/recording-receipt.d.ts +23 -0
  7. package/dist/reports/src/recording-receipt.js +16 -0
  8. package/dist/reports/src/recording-verify.d.ts +74 -0
  9. package/dist/reports/src/recording-verify.js +142 -0
  10. package/dist/reports/src/report-cli.d.ts +2 -2
  11. package/dist/reports/src/report-cli.js +53 -3
  12. package/dist/reports/src/test-catalog.d.ts +7 -1
  13. package/dist/reports/src/test-catalog.js +10 -4
  14. package/dist/schema/src/api.d.ts +84 -0
  15. package/dist/schema/src/api.js +11 -2
  16. package/dist/schema/src/cli-results.d.ts +51 -1
  17. package/dist/schema/src/cli-results.js +25 -1
  18. package/dist/server/src/openapi.js +1 -0
  19. package/docs/agents.md +1 -0
  20. package/docs/reference/cli.md +11 -2
  21. package/docs/reference/http-api.md +2 -0
  22. package/docs/reference/reports.md +185 -9
  23. package/docs/reference/schema.md +19 -1
  24. package/docs/rich-evidence.md +32 -5
  25. package/docs/server.md +1 -1
  26. package/llms-full.txt +47 -8
  27. package/package.json +1 -1
  28. package/packages/schema/schemas/effective-delta.schema.json +8 -0
  29. package/packages/schema/schemas/effective-evidence.schema.json +8 -0
  30. package/packages/schema/schemas/evidence-page.schema.json +4 -0
  31. package/packages/schema/schemas/layer-rows.schema.json +4 -0
  32. package/packages/schema/schemas/report-run.schema.json +74 -0
  33. package/packages/schema/schemas/report-runs.schema.json +86 -0
  34. package/packages/schema/schemas/review-collection.schema.json +4 -0
  35. package/packages/schema/schemas/review-manifest.schema.json +4 -0
  36. package/packages/schema/schemas/test-catalog.schema.json +4 -0
  37. package/packages/schema/schemas/test-history.schema.json +8 -0
  38. package/packages/schema/schemas/verify-recordings-result.schema.json +97 -0
  39. package/packages/server/openapi.json +252 -0
  40. package/packages/visualizer/src/explorer/ArtifactsView.tsx +3 -2
  41. package/packages/visualizer/src/explorer/RunsView.tsx +28 -3
  42. package/packages/visualizer/src/explorer/filters.tsx +11 -3
  43. package/packages/visualizer/src/explorer/search.ts +6 -2
@@ -10,7 +10,7 @@ Source: `packages/reports/src/artifact-query.ts`. Import: `@crvouga/atlas/report
10
10
 
11
11
  One shareable query for the runs of a project and for the recorded results that carry evidence (recordings,
12
12
  screenshots, traces): by artifact tag, label, media type, feature (domain, spec), environment or device, producer,
13
- branch, revision, outcome and time. The same rules answer `GET /api/reports/runs`, `GET /api/reports/artifacts`
13
+ branch, revision, outcome, time and (runs only) recording completeness. The same rules answer `GET /api/reports/runs`, `GET /api/reports/artifacts`
14
14
  and the viewer's Runs and Artifacts views, so a link means the same thing everywhere.
15
15
 
16
16
  The semantics are fixed, not inferred:
@@ -81,6 +81,12 @@ export type ArtifactFilter = {
81
81
  readonly domain?: readonly string[];
82
82
  /** Spec paths. Not applicable to runs. */
83
83
  readonly spec?: readonly string[];
84
+ /**
85
+ * Recording completeness of a run: `complete`, `incomplete`, `none-expected` or `unknown` (a run whose layers declared no
86
+ * recording expectations is `unknown`, never `complete`). Answered from the layers' receipts alone, without reading any
87
+ * recording. Applies to runs only; it is independent of `outcome`.
88
+ */
89
+ readonly recordings?: readonly string[];
84
90
  /** Only records at or after this time (epoch ms): a result's recording time, a run's end. */
85
91
  readonly since?: number;
86
92
  /** Only records at or before this time (epoch ms). */
@@ -124,6 +130,8 @@ export type FilterFields = {
124
130
  /** Absent when the dimension does not apply (runs have no domain). */
125
131
  readonly domains?: readonly string[];
126
132
  readonly specs?: readonly string[];
133
+ /** The recording completeness of a run (one value). Absent when the dimension does not apply (results). */
134
+ readonly recordings?: readonly string[];
127
135
  readonly at: number;
128
136
  };
129
137
  ```
@@ -143,6 +151,8 @@ export type QueryFacets = {
143
151
  readonly branches: readonly QueryFacetValue[];
144
152
  readonly outcomes: readonly QueryFacetValue[];
145
153
  readonly domains?: readonly QueryFacetValue[];
154
+ /** Runs only: `complete`, `incomplete`, `none-expected` and `unknown`, with how many runs have each. */
155
+ readonly recordings?: readonly QueryFacetValue[];
146
156
  };
147
157
  ```
148
158
 
@@ -204,6 +214,14 @@ export function usedFilters(filter: ArtifactFilter): FilterName[]
204
214
 
205
215
  Which filters a query uses, by the names an address gives them.
206
216
 
217
+ #### `forArtifacts`
218
+
219
+ ```ts
220
+ export function forArtifacts(filter: ArtifactFilter): ArtifactFilter
221
+ ```
222
+
223
+ Every filter in `filter` that may be applied to a recorded result, as the artifacts query applies them (no recording completeness, which is a property of a run).
224
+
207
225
  #### `isFiltered`
208
226
 
209
227
  ```ts
@@ -218,7 +236,7 @@ Whether a query narrows anything.
218
236
  export function forRuns(filter: ArtifactFilter): ArtifactFilter
219
237
  ```
220
238
 
221
- Every filter in `filter` that may be applied to a record with these fields, as the runs query applies them (no domain or spec).
239
+ Every filter in `filter` that may be applied to a run, as the runs query applies them (no domain or spec).
222
240
 
223
241
  #### `matchesFilter`
224
242
 
@@ -2161,6 +2179,8 @@ export type VideoRef = {
2161
2179
  readonly recording?: string;
2162
2180
  /** False when the reference names no bytes at all (no bundle key, address or inline text), or a bundle key that the layer's own bundle does not hold, so it can never be played. */
2163
2181
  readonly available: boolean;
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
+ readonly reason?: 'not-in-bundle';
2164
2184
  /** 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. */
2165
2185
  readonly scope?: EvidenceScope;
2166
2186
  };
@@ -2180,6 +2200,8 @@ export type VideoCounts = {
2180
2200
  readonly recordings: number;
2181
2201
  /** References that name no bytes, or bytes the layer does not hold. A report that predates availability metadata has none of these and none of the others. */
2182
2202
  readonly unavailable: number;
2203
+ /** Of `unavailable`, those that name a file the layer's bundle does not hold (stored, then gone). Absent when there are none. */
2204
+ readonly missing?: number;
2183
2205
  };
2184
2206
  ```
2185
2207
 
@@ -6408,6 +6430,160 @@ skipped or unresolved; otherwise `unknown` when a contributing layer has no rece
6408
6430
  report with no results at all is unknown too); otherwise `complete` when something was expected and `none-expected` when
6409
6431
  nothing was. Gaps are sorted so the output never depends on read order.
6410
6432
 
6433
+ #### `RunRecordings`
6434
+
6435
+ ```ts
6436
+ export type RunRecordings = {
6437
+ /** `incomplete`: a recording is skipped or unresolved. `unknown`: no gap is known, but a layer declared no expectations. `complete`: everything expected was published. `none-expected`: every layer declared expectations and none required a recording. */
6438
+ readonly state: 'complete' | 'incomplete' | 'none-expected' | 'unknown';
6439
+ /** Recordings the layers expected. */
6440
+ readonly expected: number;
6441
+ /** Of those, uploaded or referenced by address. */
6442
+ readonly published: number;
6443
+ readonly skipped: number;
6444
+ readonly unresolved: number;
6445
+ /** Intentionally unrecorded checks: neither expected nor missing. */
6446
+ readonly notApplicable: number;
6447
+ /** Layers of the run that declared no expectations: nothing is claimed for them. */
6448
+ readonly layersWithoutReceipt: number;
6449
+ };
6450
+ ```
6451
+
6452
+ A run's recording completeness, from the receipts of its layers (see {@link runRecordings}).
6453
+
6454
+ #### `runRecordings`
6455
+
6456
+ ```ts
6457
+ export function runRecordings(layers: readonly (RecordingCounts | undefined)[]): RunRecordings | undefined
6458
+ ```
6459
+
6460
+ The recording completeness of a run, from the `recordings` counts of each of its layers (`undefined` for a layer that
6461
+ has no receipt). `undefined` when no layer has a receipt: the run claims nothing, which a reader treats as `unknown`.
6462
+ A gap always wins (`incomplete`); otherwise a layer without a receipt keeps the run `unknown`, never `complete`. Reads
6463
+ only the counts, never a recording, and never changes a status.
6464
+
6465
+ ### Recording verification
6466
+
6467
+ Source: `packages/reports/src/recording-verify.ts`. Import: `@crvouga/atlas/reports/recording-verify`
6468
+
6469
+ Answers "is the recording we published still there, and does it still look like the video we published?" apart from
6470
+ "did the tests pass?" and apart from "was it uploaded?" (the receipt, see `recording-receipt.ts`). `verifyLayerRecordings`
6471
+ reads each video a layer keeps in its bundle back from the store and checks, in order, that the layer's bundle still
6472
+ lists its file, that the blob it lives in is still stored, that its size and SHA-256 are the ones recorded at upload,
6473
+ and that its first bytes are the container header its media type promises. A problem is a gap in the evidence, named
6474
+ with the check that holds the video; it never changes a status, a slot or a winner.
6475
+
6476
+ Trap: this is a stored-bytes check, not a playback test. A file with a valid header can still fail to play in a given
6477
+ browser (codec, truncation), and acknowledging an upload is not playback either. Videos given only by `uri` are counted
6478
+ as `referenced`, not checked: Atlas holds no bytes to read back.
6479
+
6480
+ See docs/rich-evidence.md#recording-completeness
6481
+
6482
+ #### `RecordingProblem`
6483
+
6484
+ ```ts
6485
+ export type RecordingProblem =
6486
+ /** The layer names the file but its bundle does not list it: stored, then gone (or never arrived). */
6487
+ | 'not-in-bundle'
6488
+ /** The bundle lists the file but a blob holding its bytes is no longer in the store (a 404 when played). */
6489
+ | 'blob-missing'
6490
+ /** The store could not be read just now. Not proof the bytes are gone, and not a pass. */
6491
+ | 'unreadable'
6492
+ /** The stored bytes are not as many as were recorded. */
6493
+ | 'size-mismatch'
6494
+ /** The stored bytes do not hash to the SHA-256 recorded at upload. */
6495
+ | 'checksum-mismatch'
6496
+ /** The stored bytes do not begin like the container their media type names, or are empty: incompatible or corrupt. */
6497
+ | 'undecodable';
6498
+ ```
6499
+
6500
+ What is wrong with a stored recording. An open vocabulary: a reader treats an unknown problem as a gap.
6501
+
6502
+ #### `RecordingGap`
6503
+
6504
+ ```ts
6505
+ export type RecordingGap = {
6506
+ /** The check that holds the video, or `null` for a video attached to the layer or a part. */
6507
+ readonly check: string | null;
6508
+ /** The attachment's name. */
6509
+ readonly name: string;
6510
+ readonly bundleKey: string;
6511
+ readonly problem: RecordingProblem;
6512
+ /** A short, non-secret explanation: sizes or the container found. */
6513
+ readonly reason: string;
6514
+ };
6515
+ ```
6516
+
6517
+ One video of a layer that failed verification.
6518
+
6519
+ #### `LayerRecordingVerification`
6520
+
6521
+ ```ts
6522
+ export type LayerRecordingVerification = {
6523
+ readonly layer: string;
6524
+ /** Videos read back from the store: those that name a bundle key. */
6525
+ readonly checked: number;
6526
+ /** Of those, the ones that passed every check. */
6527
+ readonly ok: number;
6528
+ /** Videos given only by `uri`, which Atlas cannot read back. */
6529
+ readonly referenced: number;
6530
+ readonly gaps: readonly RecordingGap[];
6531
+ };
6532
+ ```
6533
+
6534
+ What verifying one layer found.
6535
+
6536
+ #### `containerProblem`
6537
+
6538
+ ```ts
6539
+ export function containerProblem(bytes: Uint8Array, contentType: string): string | null
6540
+ ```
6541
+
6542
+ Whether the first bytes of a stored video are the container its media type names, as a reason it is not, or `null` when
6543
+ it looks right or the type is one this check has no signature for (then nothing is claimed). Recognizes WebM and
6544
+ Matroska (EBML), MP4, QuickTime and 3GPP (an ISO box), Ogg and AVI. An empty file is never a video.
6545
+
6546
+ #### `verifyLayerRecordings`
6547
+
6548
+ ```ts
6549
+ export async function verifyLayerRecordings(store: ObjectStore, layer: ReportLayer): Promise<LayerRecordingVerification>
6550
+ ```
6551
+
6552
+ Reads back every video of `layer` that is kept in its bundle and reports the ones that are no longer retrievable or no
6553
+ longer look like what was published (see {@link RecordingProblem}). Each blob is read once however many checks
6554
+ reference it. A store that fails to answer is `unreadable`, never a pass. It only reads: nothing is written, and no
6555
+ status changes.
6556
+
6557
+ **Example**
6558
+
6559
+ ```ts
6560
+ const layer = await readLayer(store, id);
6561
+ const { gaps } = await verifyLayerRecordings(store, layer!);
6562
+ gaps.map(gap => `${gap.check}: ${gap.problem}`);
6563
+ ```
6564
+
6565
+ #### `StoreRecordingVerification`
6566
+
6567
+ ```ts
6568
+ export type StoreRecordingVerification = {
6569
+ readonly layers: readonly LayerRecordingVerification[];
6570
+ /** Layers that could not be read at all (their manifest is unreadable or gone): their recordings are unknown, not verified. */
6571
+ readonly notices: readonly string[];
6572
+ };
6573
+ ```
6574
+
6575
+ What verifying the layers of one store found.
6576
+
6577
+ #### `verifyStoreRecordings`
6578
+
6579
+ ```ts
6580
+ export async function verifyStoreRecordings(store: ObjectStore, options: { readonly ids?: readonly string[]; readonly limit?: number; readonly include?: (layer: ReportLayer) => boolean } = {}): Promise<StoreRecordingVerification>
6581
+ ```
6582
+
6583
+ Verifies the recordings of the layers a store lists, newest first. `ids` narrows to layers whose id starts with one of
6584
+ them (at least 7 hex digits); `limit` keeps only that many of the newest. A layer that cannot be read is a notice, never
6585
+ skipped silently and never counted as verified. A store that cannot be listed rejects.
6586
+
6411
6587
  ### Reliability of a test
6412
6588
 
6413
6589
  Source: `packages/reports/src/reliability.ts`. Import: `@crvouga/atlas/reports/reliability`
@@ -6767,7 +6943,7 @@ Call this inside a Solid root: it creates signals and a store.
6767
6943
 
6768
6944
  Source: `packages/reports/src/report-cli.ts`. Import: `@crvouga/atlas/reports/report-cli`
6769
6945
 
6770
- `reportMain` is `atlas report publish | stream | status | merge | doctor | policy`, written so it can be called from code and
6946
+ `reportMain` is `atlas report publish | stream | status | verify-recordings | merge | doctor | policy`, written so it can be called from code and
6771
6947
  tests as well as a terminal: it takes its arguments, environment, working directory, clock, git and standard input as
6772
6948
  parameters and returns the exit code. Every command can print a JSON document (`--format json`) instead of prose, and a
6773
6949
  command that is wrong throws, which `runReportCommand` turns into a message (or a JSON error) and exit code 2.
@@ -9942,14 +10118,14 @@ export type RunSummary = {
9942
10118
  * and references that cannot be played. Absent when it has none, and in a report that carries no video information.
9943
10119
  */
9944
10120
  readonly videos?: VideoCounts;
10121
+ /**
10122
+ * Whether the recordings its layers expected were published, apart from its outcomes (see `runRecordings`). Absent when
10123
+ * none of its layers declared a recording expectation: its completeness is unknown, never complete. Never changes a status.
10124
+ */
10125
+ readonly recordings?: RunRecordings;
9945
10126
  /**
9946
10127
  * Files attached to the run itself, in its layers (and their parts), not to any one check: review movies, archives,
9947
10128
  * summaries. All kinds, videos included. Absent when none, and in a report that carries no such information.
9948
- */
9949
- readonly attachments?: number;
9950
- /** The normalized tags of the attachments of its results and layers, sorted and distinct. Absent when none is tagged. */
9951
- readonly artifactTags?: readonly string[];
9952
- /** The tags of its checks and layers, which its attachments inherit (what `inherited=1` also matches), sorted and distinct. Absent when none. */
9953
10129
  …
9954
10130
  ```
9955
10131
 
@@ -10281,7 +10457,7 @@ What a query looks at in a run: see {@link ArtifactFilter}. A run's outcome is `
10281
10457
  #### `artifactList`
10282
10458
 
10283
10459
  ```ts
10284
- export function artifactList(history: TestHistory, version: string, query: ArtifactFilter = {}, page: { readonly limit?: number; readonly offset?: number; readonly domainOf?: (specPath: string) => string | undefined } = {}): ArtifactList
10460
+ export function artifactList(history: TestHistory, version: string, asked: ArtifactFilter = {}, page: { readonly limit?: number; readonly offset?: number; readonly domainOf?: (specPath: string) => string | undefined } = {}): ArtifactList
10285
10461
  ```
10286
10462
 
10287
10463
  The recorded results that carry evidence and match a query, newest first, across the whole history (`limit` default
@@ -535,7 +535,7 @@ of every layer. States are in id order and cells in profile order, so the same l
535
535
 
536
536
  Source: `packages/schema/src/cli-results.ts`
537
537
 
538
- What `atlas report publish`, `stream`, `merge` and `doctor` print on standard output with `--format json` (alias
538
+ What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings` print on standard output with `--format json` (alias
539
539
  `--json`), and what any `atlas` command prints on standard error for a command that was wrong: one JSON document,
540
540
  so an agent or a script never has to parse prose. (`status` and `policy` already print documents:
541
541
  `ReportStatusSchema`, and an IAM policy.) Each is also published as a JSON Schema.
@@ -594,6 +594,24 @@ export const DoctorResultSchema = …
594
594
 
595
595
  What `atlas report doctor --format json` prints.
596
596
 
597
+ #### `RecordingGapSchema`
598
+
599
+ ```ts
600
+ export const RecordingGapSchema = …
601
+ ```
602
+
603
+ A published video that could not be read back from the store as published (see `atlas report verify-recordings`).
604
+
605
+ #### `VerifyRecordingsResultSchema`
606
+
607
+ ```ts
608
+ export const VerifyRecordingsResultSchema = …
609
+ ```
610
+
611
+ What `atlas report verify-recordings --format json` prints: whether the videos the layers published can still be read
612
+ back from the store as they were published. It describes stored bytes, never playback in a browser, and it never changes
613
+ a check\'s result.
614
+
597
615
  #### `CommandErrorSchema`
598
616
 
599
617
  ```ts
@@ -208,7 +208,8 @@ curl "$ATLAS_SERVER_URL/api/reports/artifacts?artifactTag=demo&type=video&domain
208
208
  - **Dimensions.** `artifactTag`, `type` (the media type as a role: `video`, `image`, `audio`, `snapshot`, `text`,
209
209
  `file`; `video` means a playable recording), `label` (`key=value`, or `key`), `domain` and `spec` (artifacts only),
210
210
  `environment` (the device or client), `producer`, `branch`, `sha` (seven or more characters), `outcome` (`passed`,
211
- `failed`, `running`, `not-run`), `since` and `until` (epoch milliseconds or an ISO date), and `q` (words).
211
+ `failed`, `running`, `not-run`), `recordings` (runs only: `complete`, `incomplete`, `none-expected` or `unknown`, see
212
+ [recording completeness](#recording-completeness)), `since` and `until` (epoch milliseconds or an ISO date), and `q` (words).
212
213
  - **Semantics.** Filters combine with AND across dimensions and OR within one (`environment=ios,android`). With both
213
214
  `artifactTag` and `type`, one attachment must be both where the report says which video carries which tag: a tagged
214
215
  screenshot beside an untagged video is not a tagged video. `inherited=1` also counts the tags an attachment inherits
@@ -475,7 +476,33 @@ publish; it never contains a path or a token.
475
476
  retry (publishing again reuses the stored files).
476
477
  - `atlas report status --format json` has `recordings`: `state` is `complete`, `incomplete`, `none-expected` or
477
478
  `unknown`, with counts and each gap. It never changes `ok`. A layer without a receipt is `unknown`, never `complete`.
478
- - The viewer shows `Recordings 2 of 3: incomplete` beside the run's origin, apart from its green or red outcomes.
479
-
480
- This is a statement about publication. Atlas does not check that a stored video plays in a browser, and does not yet
481
- re-check later that a stored file is still readable.
479
+ - The viewer shows `Recordings 2 of 3: incomplete` beside the run's origin and in each row of the Runs tab, apart from
480
+ its green or red outcomes.
481
+ - **Find runs by completeness.** `GET /api/reports/runs?recordings=incomplete` (and the Runs tab's *Recordings* filter,
482
+ `?tab=runs&recordings=incomplete`) keep the runs whose layers have a skipped or unresolved expected recording. The
483
+ values are `complete`, `incomplete`, `none-expected` and `unknown`, comma-separated for OR, and they combine with the
484
+ other filters by AND. A run whose layers declared no expectations is `unknown`, never `complete`, and a run with any
485
+ layer without a receipt cannot be `complete`. It is answered from the layers' receipts, before the page limit, and
486
+ never reads a recording; the facet counts say how many runs have each state. It is a property of a run, so the
487
+ artifacts list does not take it. A server that predates it ignores the parameter and does not name it in `filters`, and
488
+ the viewer then says the list is not narrowed by it.
489
+
490
+ ### Is the recording still there?
491
+
492
+ A receipt says a file was published. Whether it is still retrievable later is a separate question, and Atlas answers it
493
+ two ways without touching a check's result:
494
+
495
+ - **Layer bundle.** A video that names a `bundleKey` the layer's bundle does not list (the file was acknowledged and is
496
+ gone, or never arrived) is not offered to play. The report marks it `available: false` with `reason: "not-in-bundle"`,
497
+ the run counts it in `videos.unavailable` and `videos.missing`, and the Runs tab explains that the run stored the file
498
+ but no longer holds it, which is different from a reference that never named any bytes.
499
+ - **`atlas report verify-recordings`** reads the videos of the stored layers back and checks, for each, that the bundle
500
+ lists it, that its blob is still in the store, that its size and SHA-256 are the recorded ones, and that it begins like
501
+ the container its media type names (WebM, MP4, QuickTime, Ogg, AVI). It exits 1 and names the layer and check for
502
+ every gap (`not-in-bundle`, `blob-missing`, `unreadable`, `size-mismatch`, `checksum-mismatch`, `undecodable`);
503
+ `--format json` prints the `verify-recordings-result` document. `--layer <id>` and `--limit <n>` narrow the work, since
504
+ it reads blobs. A store that cannot answer is `unreadable`, never verified. Test results are unchanged either way.
505
+
506
+ Both are statements about stored bytes. Atlas does not check that a stored video plays in a given browser (a valid header
507
+ can still have an unsupported codec or be truncated), and acknowledging an upload is not playback. Videos given only by
508
+ `uri` are counted as referenced and not checked.
package/docs/server.md CHANGED
@@ -410,7 +410,7 @@ with `format=markdown` (or `Accept: text/markdown`), and takes the project prefi
410
410
  | One test: every recorded result (status, duration, environment, branch, commit, CI run), newest first | `GET /api/reports/tests/history?key=<key>` |
411
411
  | How reliable one test is in a scope (environment, producer, parameters, branch, commit, spec revision, time window): independent executions, first-try and eventual success, what was left out and why | `GET /api/reports/tests/reliability?key=<key>&environment=phone` |
412
412
  | Why Specs, domains and counts disagree: ambiguous spec paths, shared domains, gaps, a reconciliation of the totals | `GET /api/reports/tests/health` |
413
- | The runs that recorded the report (by CI run URL, else branch and commit); `?video=true` keeps those with a playable video; `artifactTag`, `type`, `label`, `environment`, `branch`, `outcome`, `since`, `until` narrow it ([filtering](rich-evidence.md#finding-runs-and-artifacts)) | `GET /api/reports/runs` |
413
+ | The runs that recorded the report (by CI run URL, else branch and commit); `?video=true` keeps those with a playable video; `artifactTag`, `type`, `label`, `environment`, `branch`, `outcome`, `recordings` (completeness of the recordings the run's layers expected), `since`, `until` narrow it ([filtering](rich-evidence.md#finding-runs-and-artifacts)) | `GET /api/reports/runs` |
414
414
  | Recorded results that carry evidence, across the whole history, found by tag, media type, label, feature, environment, branch, revision, outcome and time | `GET /api/reports/artifacts?artifactTag=demo&type=video` |
415
415
  | Every test one run recorded | `GET /api/reports/runs/detail?key=<run key>` |
416
416
 
package/llms-full.txt CHANGED
@@ -172,6 +172,7 @@ JSON Schema (`@crvouga/atlas/schemas/<name>.schema.json`, draft 2020-12):
172
172
  | `atlas report merge` | `merge-result` |
173
173
  | `atlas report doctor` | `doctor-result` |
174
174
  | `atlas report status` | `report-status` |
175
+ | `atlas report verify-recordings` | `verify-recordings-result` |
175
176
  | `a command that was wrong (standard error)` | `command-error` |
176
177
  <!-- /generated:json-command-schemas -->
177
178
 
@@ -1196,7 +1197,8 @@ curl "$ATLAS_SERVER_URL/api/reports/artifacts?artifactTag=demo&type=video&domain
1196
1197
  - **Dimensions.** `artifactTag`, `type` (the media type as a role: `video`, `image`, `audio`, `snapshot`, `text`,
1197
1198
  `file`; `video` means a playable recording), `label` (`key=value`, or `key`), `domain` and `spec` (artifacts only),
1198
1199
  `environment` (the device or client), `producer`, `branch`, `sha` (seven or more characters), `outcome` (`passed`,
1199
- `failed`, `running`, `not-run`), `since` and `until` (epoch milliseconds or an ISO date), and `q` (words).
1200
+ `failed`, `running`, `not-run`), `recordings` (runs only: `complete`, `incomplete`, `none-expected` or `unknown`, see
1201
+ [recording completeness](#recording-completeness)), `since` and `until` (epoch milliseconds or an ISO date), and `q` (words).
1200
1202
  - **Semantics.** Filters combine with AND across dimensions and OR within one (`environment=ios,android`). With both
1201
1203
  `artifactTag` and `type`, one attachment must be both where the report says which video carries which tag: a tagged
1202
1204
  screenshot beside an untagged video is not a tagged video. `inherited=1` also counts the tags an attachment inherits
@@ -1463,10 +1465,36 @@ publish; it never contains a path or a token.
1463
1465
  retry (publishing again reuses the stored files).
1464
1466
  - `atlas report status --format json` has `recordings`: `state` is `complete`, `incomplete`, `none-expected` or
1465
1467
  `unknown`, with counts and each gap. It never changes `ok`. A layer without a receipt is `unknown`, never `complete`.
1466
- - The viewer shows `Recordings 2 of 3: incomplete` beside the run's origin, apart from its green or red outcomes.
1467
-
1468
- This is a statement about publication. Atlas does not check that a stored video plays in a browser, and does not yet
1469
- re-check later that a stored file is still readable.
1468
+ - The viewer shows `Recordings 2 of 3: incomplete` beside the run's origin and in each row of the Runs tab, apart from
1469
+ its green or red outcomes.
1470
+ - **Find runs by completeness.** `GET /api/reports/runs?recordings=incomplete` (and the Runs tab's *Recordings* filter,
1471
+ `?tab=runs&recordings=incomplete`) keep the runs whose layers have a skipped or unresolved expected recording. The
1472
+ values are `complete`, `incomplete`, `none-expected` and `unknown`, comma-separated for OR, and they combine with the
1473
+ other filters by AND. A run whose layers declared no expectations is `unknown`, never `complete`, and a run with any
1474
+ layer without a receipt cannot be `complete`. It is answered from the layers' receipts, before the page limit, and
1475
+ never reads a recording; the facet counts say how many runs have each state. It is a property of a run, so the
1476
+ artifacts list does not take it. A server that predates it ignores the parameter and does not name it in `filters`, and
1477
+ the viewer then says the list is not narrowed by it.
1478
+
1479
+ ### Is the recording still there?
1480
+
1481
+ A receipt says a file was published. Whether it is still retrievable later is a separate question, and Atlas answers it
1482
+ two ways without touching a check's result:
1483
+
1484
+ - **Layer bundle.** A video that names a `bundleKey` the layer's bundle does not list (the file was acknowledged and is
1485
+ gone, or never arrived) is not offered to play. The report marks it `available: false` with `reason: "not-in-bundle"`,
1486
+ the run counts it in `videos.unavailable` and `videos.missing`, and the Runs tab explains that the run stored the file
1487
+ but no longer holds it, which is different from a reference that never named any bytes.
1488
+ - **`atlas report verify-recordings`** reads the videos of the stored layers back and checks, for each, that the bundle
1489
+ lists it, that its blob is still in the store, that its size and SHA-256 are the recorded ones, and that it begins like
1490
+ the container its media type names (WebM, MP4, QuickTime, Ogg, AVI). It exits 1 and names the layer and check for
1491
+ every gap (`not-in-bundle`, `blob-missing`, `unreadable`, `size-mismatch`, `checksum-mismatch`, `undecodable`);
1492
+ `--format json` prints the `verify-recordings-result` document. `--layer <id>` and `--limit <n>` narrow the work, since
1493
+ it reads blobs. A store that cannot answer is `unreadable`, never verified. Test results are unchanged either way.
1494
+
1495
+ Both are statements about stored bytes. Atlas does not check that a stored video plays in a given browser (a valid header
1496
+ can still have an unsupported codec or be truncated), and acknowledging an upload is not playback. Videos given only by
1497
+ `uri` are counted as referenced and not checked.
1470
1498
  </doc>
1471
1499
 
1472
1500
  <doc path="docs/ui-catalog.md">
@@ -2058,7 +2086,7 @@ with `format=markdown` (or `Accept: text/markdown`), and takes the project prefi
2058
2086
  | One test: every recorded result (status, duration, environment, branch, commit, CI run), newest first | `GET /api/reports/tests/history?key=<key>` |
2059
2087
  | How reliable one test is in a scope (environment, producer, parameters, branch, commit, spec revision, time window): independent executions, first-try and eventual success, what was left out and why | `GET /api/reports/tests/reliability?key=<key>&environment=phone` |
2060
2088
  | Why Specs, domains and counts disagree: ambiguous spec paths, shared domains, gaps, a reconciliation of the totals | `GET /api/reports/tests/health` |
2061
- | The runs that recorded the report (by CI run URL, else branch and commit); `?video=true` keeps those with a playable video; `artifactTag`, `type`, `label`, `environment`, `branch`, `outcome`, `since`, `until` narrow it ([filtering](rich-evidence.md#finding-runs-and-artifacts)) | `GET /api/reports/runs` |
2089
+ | The runs that recorded the report (by CI run URL, else branch and commit); `?video=true` keeps those with a playable video; `artifactTag`, `type`, `label`, `environment`, `branch`, `outcome`, `recordings` (completeness of the recordings the run's layers expected), `since`, `until` narrow it ([filtering](rich-evidence.md#finding-runs-and-artifacts)) | `GET /api/reports/runs` |
2062
2090
  | Recorded results that carry evidence, across the whole history, found by tag, media type, label, feature, environment, branch, revision, outcome and time | `GET /api/reports/artifacts?artifactTag=demo&type=video` |
2063
2091
  | Every test one run recorded | `GET /api/reports/runs/detail?key=<run key>` |
2064
2092
 
@@ -5252,6 +5280,8 @@ atlas report — layered evidence for any kind of spec
5252
5280
  [--specs <dir>] [--format text|json|markdown]
5253
5281
  [--require-environments a,b] [--require-all-specs]
5254
5282
  [--branch <name>]... [--ignore-modified]
5283
+ atlas report verify-recordings [--store <dir|s3://…|id=url>]... [--backends <module>] [--layer <id>]...
5284
+ [--limit <n>] [--branch <name>]... [--ignore-modified] [--format text|json]
5255
5285
  atlas report merge [--store …]... [--backends <module>] --into <dir|s3://…>
5256
5286
  atlas report doctor [--store …]... [--role publisher|reader|both]
5257
5287
  atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]
@@ -5295,6 +5325,13 @@ atlas report — layered evidence for any kind of spec
5295
5325
  --require-all-specs fail while any spec has no results
5296
5326
  --branch <name> only layers recorded on this branch (repeatable)
5297
5327
  --ignore-modified leave out layers from modified checkouts and unknown origins
5328
+ verify-recordings Read back the videos the layers published and check they are still there as published: the
5329
+ layer still lists the file, its blob is still stored, its size and SHA-256 are the recorded ones, and it
5330
+ begins like the container its media type names. Exit 1 when any video is missing, unreadable, corrupt or
5331
+ not that container, naming the layer and check; test results are never changed. This checks stored bytes,
5332
+ not playback in a browser. Videos given only by address are counted, not checked.
5333
+ --layer <id> only the layer whose id starts with this (repeatable; at least 7 digits)
5334
+ --limit <n> only the newest n layers
5298
5335
  merge Copy the layers of several stores into one.
5299
5336
  doctor Check that a store works: credentials, listing, writing, and what it holds.
5300
5337
  policy Print the least-privilege IAM policy for publishers, readers, or both.
@@ -5303,8 +5340,8 @@ atlas report — layered evidence for any kind of spec
5303
5340
  { id, label?, store } (or a function returning it): a device lab, anything
5304
5341
  that implements ObjectStore.
5305
5342
 
5306
- --format json (alias --json) publish, stream, merge, doctor and status print one JSON document instead of prose
5307
- (schemas: publish-result, stream-result, merge-result, doctor-result, report-status). A command
5343
+ --format json (alias --json) publish, stream, merge, doctor, status and verify-recordings print one JSON document instead of prose
5344
+ (schemas: publish-result, stream-result, merge-result, doctor-result, report-status, verify-recordings-result). A command
5308
5345
  that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
5309
5346
  A store may be an Atlas server (https://…): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.
5310
5347
 
@@ -6148,6 +6185,7 @@ Every run, newest first: layers grouped by their CI run URL (else branch and com
6148
6185
  - `branch` (query): Branches.
6149
6186
  - `sha` (query): Revisions: a full commit id or a prefix of at least seven characters.
6150
6187
  - `outcome` (query): Statuses: `passed`, `failed`, `running`, `not-run`. A run's outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`.
6188
+ - `recordings` (query): Recording completeness of the run, apart from its outcome: `complete`, `incomplete`, `none-expected` or `unknown` (a run whose layers declared no recording expectations is `unknown`, never `complete`). Repeatable or comma-separated. Answered from the layers' receipts; no recording is read. A server that does not know it ignores it and does not name it in `filters`.
6151
6189
  - `q` (query): Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.
6152
6190
  - `since` (query): Only records at or after this time: epoch milliseconds or an ISO date (a date alone is UTC). A result's recording time; a run's end.
6153
6191
  - `until` (query): Only records at or before this time (same forms as `since`).
@@ -6176,6 +6214,7 @@ Scoped to one project. The same project may be selected on the unprefixed API us
6176
6214
  - `branch` (query): Branches.
6177
6215
  - `sha` (query): Revisions: a full commit id or a prefix of at least seven characters.
6178
6216
  - `outcome` (query): Statuses: `passed`, `failed`, `running`, `not-run`. A run's outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`.
6217
+ - `recordings` (query): Recording completeness of the run, apart from its outcome: `complete`, `incomplete`, `none-expected` or `unknown` (a run whose layers declared no recording expectations is `unknown`, never `complete`). Repeatable or comma-separated. Answered from the layers' receipts; no recording is read. A server that does not know it ignores it and does not name it in `filters`.
6179
6218
  - `q` (query): Words that must all appear in the title, id, spec, branch, producer, environment, tags or labels.
6180
6219
  - `since` (query): Only records at or after this time: epoch milliseconds or an ISO date (a date alone is UTC). A result's recording time; a run's end.
6181
6220
  - `until` (query): Only records at or before this time (same forms as `since`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/atlas",
3
- "version": "1.17.0",
3
+ "version": "1.18.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",
@@ -264,6 +264,10 @@
264
264
  "type": "boolean",
265
265
  "description": "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"
266
266
  },
267
+ "reason": {
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
+ "type": "string"
270
+ },
267
271
  "scope": {
268
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",
269
273
  "type": "object",
@@ -743,6 +747,10 @@
743
747
  "type": "boolean",
744
748
  "description": "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"
745
749
  },
750
+ "reason": {
751
+ "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
+ "type": "string"
753
+ },
746
754
  "scope": {
747
755
  "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",
748
756
  "type": "object",
@@ -264,6 +264,10 @@
264
264
  "type": "boolean",
265
265
  "description": "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"
266
266
  },
267
+ "reason": {
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
+ "type": "string"
270
+ },
267
271
  "scope": {
268
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",
269
273
  "type": "object",
@@ -743,6 +747,10 @@
743
747
  "type": "boolean",
744
748
  "description": "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"
745
749
  },
750
+ "reason": {
751
+ "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
+ "type": "string"
753
+ },
746
754
  "scope": {
747
755
  "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",
748
756
  "type": "object",
@@ -251,6 +251,10 @@
251
251
  "type": "boolean",
252
252
  "description": "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"
253
253
  },
254
+ "reason": {
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
+ "type": "string"
257
+ },
254
258
  "scope": {
255
259
  "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",
256
260
  "type": "object",
@@ -255,6 +255,10 @@
255
255
  "type": "boolean",
256
256
  "description": "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"
257
257
  },
258
+ "reason": {
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
+ "type": "string"
261
+ },
258
262
  "scope": {
259
263
  "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
264
  "type": "object",