@crvouga/atlas 0.15.0 → 0.17.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 (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/reports/src/attachment-files.d.ts +3 -1
  3. package/dist/reports/src/attachment-files.js +14 -4
  4. package/dist/reports/src/evidence-detail.d.ts +19 -2
  5. package/dist/reports/src/evidence-detail.js +63 -14
  6. package/dist/reports/src/evidence-types.d.ts +53 -0
  7. package/dist/reports/src/layer-merge.js +7 -2
  8. package/dist/reports/src/layer-types.d.ts +1 -1
  9. package/dist/reports/src/publish-checks.js +11 -3
  10. package/dist/reports/src/recording-receipt.d.ts +48 -0
  11. package/dist/reports/src/recording-receipt.js +103 -0
  12. package/dist/reports/src/report-cli.d.ts +1 -1
  13. package/dist/reports/src/report-cli.js +20 -2
  14. package/dist/reports/src/report-status.d.ts +2 -0
  15. package/dist/reports/src/report-status.js +17 -1
  16. package/dist/reports/src/reports-explorer-route.d.ts +8 -0
  17. package/dist/reports/src/reports-route.d.ts +8 -0
  18. package/dist/reports/src/test-catalog.d.ts +24 -3
  19. package/dist/reports/src/test-catalog.js +37 -11
  20. package/dist/schema/src/api.d.ts +232 -0
  21. package/dist/schema/src/api.js +35 -6
  22. package/dist/schema/src/cli-results.d.ts +107 -2
  23. package/dist/schema/src/cli-results.js +11 -3
  24. package/dist/schema/src/layer.d.ts +165 -0
  25. package/dist/schema/src/layer.js +72 -1
  26. package/dist/server/src/app.js +3 -2
  27. package/dist/server/src/llms.js +2 -1
  28. package/dist/server/src/openapi.js +2 -1
  29. package/docs/reference/cli.md +5 -0
  30. package/docs/reference/http-api.md +4 -2
  31. package/docs/reference/reports.md +204 -5
  32. package/docs/reference/schema.md +75 -0
  33. package/docs/rich-evidence.md +29 -0
  34. package/docs/server.md +1 -1
  35. package/llms-full.txt +39 -3
  36. package/package.json +1 -1
  37. package/packages/schema/schemas/check-detail.schema.json +20 -0
  38. package/packages/schema/schemas/effective-delta.schema.json +143 -0
  39. package/packages/schema/schemas/effective-evidence.schema.json +143 -0
  40. package/packages/schema/schemas/evidence-page.schema.json +86 -0
  41. package/packages/schema/schemas/layer-rows.schema.json +57 -0
  42. package/packages/schema/schemas/layer.schema.json +153 -0
  43. package/packages/schema/schemas/publish-checks.schema.json +24 -0
  44. package/packages/schema/schemas/publish-result.schema.json +161 -1
  45. package/packages/schema/schemas/report-run.schema.json +71 -0
  46. package/packages/schema/schemas/report-runs.schema.json +38 -1
  47. package/packages/schema/schemas/report-status.schema.json +104 -0
  48. package/packages/schema/schemas/test-catalog.schema.json +41 -0
  49. package/packages/schema/schemas/test-history.schema.json +82 -0
  50. package/packages/server/openapi.json +830 -4
  51. package/packages/visualizer/src/evidence/marks.tsx +16 -0
  52. package/packages/visualizer/src/explorer/RunsView.tsx +128 -10
  53. package/packages/visualizer/src/explorer/catalog.tsx +16 -6
  54. package/packages/visualizer/src/explorer/search.ts +4 -1
@@ -91,6 +91,7 @@ atlas report — layered evidence for any kind of spec
91
91
  [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]
92
92
  [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...
93
93
  [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]
94
+ [--require-recordings]
94
95
  atlas report stream [--store <dir|s3://…|https://atlas.example.com>] --producer <name> [--environment <name>]
95
96
  [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...
96
97
  [--run-id <id>] [--run-title <t>]
@@ -127,6 +128,10 @@ atlas report — layered evidence for any kind of spec
127
128
  Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own
128
129
  folder and each --attach-root, and never from a hidden file or a link out: a report is data from a
129
130
  test run, not instructions. One that is missing or not allowed is skipped with a warning.
131
+ --require-recordings exit 1 when a check that declared recordingExpectation "required" has no
132
+ uploaded video (absent, left out as too large or not allowed, or refused). The layer is
133
+ still written and the checks keep their results; only this command fails. The receipt of
134
+ what was uploaded (counts, sizes, checksums) is in the JSON result under layer.recordings.
130
135
  From a checkout with uncommitted changes the layer is marked "modified".
131
136
  stream Send checks as they finish: one JSON object per line on standard input (the fields of one
132
137
  check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server
@@ -616,9 +616,10 @@ Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `A
616
616
 
617
617
  `GET /api/reports/runs` · role: read
618
618
 
619
- Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned and up to twenty failures. Send `format=markdown` for Markdown.
619
+ Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures and their video counts (references, distinct playable recordings, unavailable). Send `format=markdown` for Markdown.
620
620
 
621
621
  - `limit` (query): At most this many runs (default 100).
622
+ - `video` (query): `true` to list only runs that have a playable video recording. Applied to every run before `limit`; `total` counts the matches. A server that does not know it answers with every run.
622
623
  - `format` (query): `markdown` for Markdown instead of JSON.
623
624
  - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
624
625
 
@@ -628,12 +629,13 @@ Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `40
628
629
 
629
630
  `GET /api/projects/{project}/reports/runs` · role: read
630
631
 
631
- Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned and up to twenty failures. Send `format=markdown` for Markdown.
632
+ Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures and their video counts (references, distinct playable recordings, unavailable). Send `format=markdown` for Markdown.
632
633
 
633
634
  Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
634
635
 
635
636
  - `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
636
637
  - `limit` (query): At most this many runs (default 100).
638
+ - `video` (query): `true` to list only runs that have a playable video recording. Applied to every run before `limit`; `total` counts the matches. A server that does not know it answers with every run.
637
639
  - `format` (query): `markdown` for Markdown instead of JSON.
638
640
 
639
641
  Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
@@ -276,7 +276,9 @@ recursively), and hands their bytes over as `data` with a `contentType` guessed
276
276
  attachment gave none.
277
277
 
278
278
  A file that is missing, not allowed or too large is left out with a warning: evidence that could not be
279
- found never stops the results from being published. Attachments without a `path` are kept untouched.
279
+ found never stops the results from being published. The exception in kind is a video of a check that declared
280
+ `recordingExpectation: "required"`: it stays as an attachment without bytes whose `omitted` says why (`not-found`,
281
+ `not-allowed`, `too-large`, `over-budget`), so the recording receipt can name the gap. Attachments without a `path` are kept untouched.
280
282
 
281
283
  - `holder`: A check, step, attempt or any object that may have `attachments`, `steps` and `attempts`.
282
284
 
@@ -1426,7 +1428,36 @@ read when someone asks for it. Undefined when there is none.
1426
1428
  Several holders (for example a check and its layer) are summed: `attachments` also counts those on steps and
1427
1429
  attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
1428
1430
  label the later holder wins. `artifactTags` are the attachments' own normalized tags and are never inherited. `roles` are the attachments' roles (or, without one, what their media type or file
1429
- name implies), sorted and distinct.
1431
+ name implies), sorted and distinct. `videos` lists every attachment whose effective media type is video, with the
1432
+ check, retry attempt or step that holds it (see {@link isVideoAttachment}).
1433
+
1434
+ #### `summarizeLayerEvidence`
1435
+
1436
+ ```ts
1437
+ export function summarizeLayerEvidence(...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined
1438
+ ```
1439
+
1440
+ Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
1441
+ part is owned by the `layer`, not by a check.
1442
+
1443
+ #### `isVideoAttachment`
1444
+
1445
+ ```ts
1446
+ export function isVideoAttachment(attachment: Pick<Attachment, 'name' | 'role' | 'contentType'>): boolean
1447
+ ```
1448
+
1449
+ Whether an attachment is a video, by its effective media type: the producer's `contentType`, else the one its file
1450
+ name implies. A role never decides this on its own (`demo` on a PNG is not a video), except that a `video` role
1451
+ settles a file whose type nothing says (`application/octet-stream`).
1452
+
1453
+ #### `videoCounts`
1454
+
1455
+ ```ts
1456
+ export function videoCounts(videos: readonly VideoRef[] | undefined): VideoCounts | undefined
1457
+ ```
1458
+
1459
+ Counts references, distinct playable recordings and unplayable references. Undefined when there is no reference, so
1460
+ "no video" and "nothing known about video" look the same on the wire.
1430
1461
 
1431
1462
  #### `roleOfType`
1432
1463
 
@@ -1609,6 +1640,51 @@ export type StatusCounts = Readonly<Record<EvidenceStatus, number>>;
1609
1640
 
1610
1641
  How many results have each status (`passed`, `failed`, `running`, `not-run`).
1611
1642
 
1643
+ #### `VideoOwner`
1644
+
1645
+ ```ts
1646
+ export type VideoOwner = 'layer' | 'check' | 'attempt' | 'step';
1647
+ ```
1648
+
1649
+ Who holds a recording: the whole layer (or one of its parts), one check, one retry attempt of a check, or one step.
1650
+
1651
+ #### `VideoRef`
1652
+
1653
+ ```ts
1654
+ export type VideoRef = {
1655
+ /** The attachment's name, for a person. */
1656
+ readonly name: string;
1657
+ readonly owner: VideoOwner;
1658
+ /** The try that holds it, when `owner` is `attempt` or the step is inside one. */
1659
+ readonly attempt?: number;
1660
+ /** The name of the step that holds it, when `owner` is `step`. */
1661
+ readonly step?: string;
1662
+ /** What identifies the bytes (digest, bundle key or address); references with the same one are the same recording. Absent when `available` is false. */
1663
+ readonly recording?: string;
1664
+ /** False when the reference names no bytes at all (no bundle key, address or inline text), so it can never be played. */
1665
+ readonly available: boolean;
1666
+ };
1667
+ ```
1668
+
1669
+ One reference to a video, found by its effective media type (the producer's `contentType`, else the one the file
1670
+ name implies; a `video` role alone counts only when nothing else says what the file is). A reference is not a
1671
+ recording: the same bytes referenced from three checks are three references and one recording.
1672
+
1673
+ #### `VideoCounts`
1674
+
1675
+ ```ts
1676
+ export type VideoCounts = {
1677
+ /** Every reference to a video, playable or not. */
1678
+ readonly references: number;
1679
+ /** Distinct playable recordings: identical bytes referenced twice are one. */
1680
+ readonly recordings: number;
1681
+ /** References that name no bytes. A report that predates availability metadata has none of these and none of the others. */
1682
+ readonly unavailable: number;
1683
+ };
1684
+ ```
1685
+
1686
+ Video availability as counts: references, distinct playable recordings (deduplicated), and references that cannot be played.
1687
+
1612
1688
  #### `EvidenceSummary`
1613
1689
 
1614
1690
  ```ts
@@ -1626,6 +1702,8 @@ export type EvidenceSummary = {
1626
1702
  readonly labels: Readonly<Record<string, string>>;
1627
1703
  /** The distinct roles of its attachments (or what their media types say), to show what kind of evidence there is. */
1628
1704
  readonly roles: readonly string[];
1705
+ /** Its references to videos, wherever they are held (layer, check, attempt, step). Only there when it has any. */
1706
+ readonly videos?: readonly VideoRef[];
1629
1707
  };
1630
1708
  ```
1631
1709
 
@@ -1671,11 +1749,34 @@ export type LayerHeader = {
1671
1749
  readonly execution: string;
1672
1750
  /** What the run as a whole carries: its logs, links and labels. */
1673
1751
  readonly evidence?: EvidenceSummary;
1752
+ /** Counts from the layer's recording receipt: how many expected recordings were published. Absent when the layer declared no expectations: its completeness is unknown, not complete. Never changes a status. */
1753
+ readonly recordings?: RecordingCounts;
1674
1754
  };
1675
1755
  ```
1676
1756
 
1677
1757
  A report layer's origin and totals. Its checks are read separately, once.
1678
1758
 
1759
+ #### `RecordingCounts`
1760
+
1761
+ ```ts
1762
+ export type RecordingCounts = {
1763
+ /** Checks that required a recording: `uploaded + referenced + skipped + unresolved`. */
1764
+ readonly expected: number;
1765
+ /** Stored in the layer, with a checksum. */
1766
+ readonly uploaded: number;
1767
+ /** Given only by `uri`: Atlas cannot vouch for it. */
1768
+ readonly referenced: number;
1769
+ /** Captured but left out, with a reason (too large, not allowed…). */
1770
+ readonly skipped: number;
1771
+ /** No video was attached at all. */
1772
+ readonly unresolved: number;
1773
+ /** Intentionally unrecorded checks: neither expected nor missing. */
1774
+ readonly notApplicable: number;
1775
+ };
1776
+ ```
1777
+
1778
+ How many recordings a layer expected and what became of them (see `RecordingReceipt`).
1779
+
1679
1780
  #### `LayerRow`
1680
1781
 
1681
1782
  ```ts
@@ -1699,6 +1800,8 @@ export type LayerRow = {
1699
1800
  readonly durationMs: number | null;
1700
1801
  /** What this result carries beyond its outcome. Absent when nothing. */
1701
1802
  readonly evidence?: EvidenceSummary;
1803
+ /** What became of this check's expected recording: `uploaded`, `referenced`, `skipped`, `unresolved` or `not-applicable`, with a reason when it is not uploaded. Absent when the check declared no expectation. Independent of `status`. */
1804
+ readonly recording?: { readonly state: string; readonly reason?: string };
1702
1805
  };
1703
1806
  ```
1704
1807
 
@@ -3907,7 +4010,7 @@ String choices (such as `{ device: 'iphone-15' }`) that make a result its own sl
3907
4010
  #### `LayerExtras`
3908
4011
 
3909
4012
  ```ts
3910
- export type LayerExtras = Partial<Pick<import('../../schema/src/layer').LayerDocument, 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>>;
4013
+ export type LayerExtras = Partial<Pick<import('../../schema/src/layer').LayerDocument, 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions' | 'recordings'>>;
3911
4014
  ```
3912
4015
 
3913
4016
  What a layer may carry as a whole. All optional and open: see docs/extending.md.
@@ -5305,6 +5408,69 @@ export function contentTypeForMedia(media: string): string
5305
5408
 
5306
5409
  The media type for a snapshot media kind (`html`, `png`, `json`, `text`, `bytes`, `video`); an unknown kind is `application/octet-stream`.
5307
5410
 
5411
+ ### Recording receipts
5412
+
5413
+ Source: `packages/reports/src/recording-receipt.ts`. Import: `@crvouga/atlas/reports/recording-receipt`
5414
+
5415
+ Answers "did the recordings we expected get published?" apart from "did the tests pass?". `recordingReceipt` reads the
5416
+ checks a producer is about to publish (after their files were stored) and describes, per check that declared a
5417
+ `recordingExpectation`, whether its video was uploaded, only referenced, left out (with a reason) or never attached.
5418
+ `recordingCompleteness` reduces the winning results of a report to the figures `atlas report status` prints. Neither
5419
+ reads a clock or the environment, and neither changes a status, a slot or a winner: a receipt describes publication, it
5420
+ does not decide a result. A layer with no receipt is Unknown, never Complete.
5421
+
5422
+ See docs/rich-evidence.md#recording-completeness
5423
+
5424
+ #### `isVideoAttachment`
5425
+
5426
+ ```ts
5427
+ export function isVideoAttachment(attachment: Loose): boolean
5428
+ ```
5429
+
5430
+ Is this attachment a screen recording: a role of `video`, or a `video/*` media type?
5431
+
5432
+ #### `recordingReceipt`
5433
+
5434
+ ```ts
5435
+ export function recordingReceipt(checks: readonly Loose[]): RecordingReceipt | undefined
5436
+ ```
5437
+
5438
+ The receipt for the checks of one layer, or `undefined` when no check declares a `recordingExpectation` (the layer then
5439
+ claims nothing). Call it with checks whose attachment bytes are already stored (`bundleKey`, `bytes` and `sha256` set).
5440
+
5441
+ A `required` check is `uploaded` when any video of it has stored bytes, `referenced` when one has only a `uri`,
5442
+ `skipped` when its videos were left out (the attachment's `omitted` is the reason) and `unresolved` when it has no
5443
+ video at all. A `not-applicable` check is listed but is neither expected nor missing. Items are sorted by check id so the
5444
+ same input is always the same receipt.
5445
+
5446
+ #### `RecordingResult`
5447
+
5448
+ ```ts
5449
+ export type RecordingResult = {
5450
+ readonly spec: string | null;
5451
+ readonly check: string;
5452
+ readonly environment: string | null;
5453
+ readonly layer: string;
5454
+ /** Whether the layer the result came from carried a receipt. */
5455
+ readonly hasReceipt: boolean;
5456
+ /** The result's item in that receipt, when its check declared an expectation. */
5457
+ readonly recording?: { readonly state: string; readonly reason?: string };
5458
+ };
5459
+ ```
5460
+
5461
+ One result as {@link recordingCompleteness} reads it.
5462
+
5463
+ #### `recordingCompleteness`
5464
+
5465
+ ```ts
5466
+ export function recordingCompleteness(results: readonly RecordingResult[]): RecordingCompleteness
5467
+ ```
5468
+
5469
+ Reduces the winning results of a report to its recording completeness. `incomplete` when any expected recording is
5470
+ skipped or unresolved; otherwise `unknown` when a contributing layer has no receipt (nothing is claimed for it, and a
5471
+ report with no results at all is unknown too); otherwise `complete` when something was expected and `none-expected` when
5472
+ nothing was. Gaps are sorted so the output never depends on read order.
5473
+
5308
5474
  ### Remote commit report
5309
5475
 
5310
5476
  Source: `packages/reports/src/remote-report.ts`. Import: `@crvouga/atlas/reports/remote-report`
@@ -5680,6 +5846,8 @@ checks (or none completed), failed checks, running checks, specs without results
5680
5846
  each required environment with no checks (the name is normalised, so `iOS` means `ios`), then every notice of the
5681
5847
  report (an unreadable layer or backend). `failing` lists each failed or running winner, in a fixed order that does
5682
5848
  not depend on read order; diagnostic `performance` checks are left out unless nothing else was recorded.
5849
+ `recordings` reports recording completeness of the same results apart from `ok`: an incomplete or unknown
5850
+ completeness never changes the gate (use `atlas report publish --require-recordings` to fail on a missing video).
5683
5851
 
5684
5852
  **Example**
5685
5853
 
@@ -7930,20 +8098,39 @@ export type RunSummary = {
7930
8098
  readonly totalDurationMs: number | null;
7931
8099
  /** Up to twenty tests that failed in it. */
7932
8100
  readonly failing: readonly { readonly key: string; readonly title: string; readonly environment: string | null; readonly detail: string }[];
8101
+ /**
8102
+ * Its videos, from the run's layers and from the last result of each test: references, distinct playable recordings
8103
+ * and references that cannot be played. Absent when it has none, and in a report that carries no video information.
8104
+ */
8105
+ readonly videos?: VideoCounts;
7933
8106
  };
7934
8107
  ```
7935
8108
 
7936
8109
  A run: the layers that name the same execution (`run.id`), else share a CI run URL, else a branch and commit. When,
7937
8110
  where, and how its tests went.
7938
8111
 
8112
+ #### `RunQuery`
8113
+
8114
+ ```ts
8115
+ export type RunQuery = {
8116
+ /** Only runs that have at least one playable video recording. `total` then counts only those. */
8117
+ readonly video?: boolean;
8118
+ };
8119
+ ```
8120
+
8121
+ What `runList` can be asked.
8122
+
7939
8123
  #### `RunList`
7940
8124
 
7941
8125
  ```ts
7942
8126
  export type RunList = {
7943
8127
  readonly schemaVersion: typeof TEST_CATALOG_VERSION;
7944
8128
  readonly version: string;
8129
+ /** Runs that matched, before `limit`. */
7945
8130
  readonly total: number;
7946
8131
  readonly runs: readonly RunSummary[];
8132
+ /** How many of all runs (whatever was asked, before `limit`) have a playable video recording. */
8133
+ readonly withVideo?: number;
7947
8134
  };
7948
8135
  ```
7949
8136
 
@@ -7964,6 +8151,8 @@ export type RunResult = {
7964
8151
  readonly at: number;
7965
8152
  readonly layer: string;
7966
8153
  readonly detail: string;
8154
+ /** Every video its evidence references, with who holds it (check, retry attempt or step). Absent when there is none. */
8155
+ readonly videos?: readonly VideoRef[];
7967
8156
  };
7968
8157
  ```
7969
8158
 
@@ -8114,13 +8303,23 @@ export function testHistory(model: CatalogModel, key: string): TestHistoryDocume
8114
8303
 
8115
8304
  One test and its recorded results, or null when the report has no test with that key.
8116
8305
 
8306
+ #### `hasVideo`
8307
+
8308
+ ```ts
8309
+ export function hasVideo(run: Pick<RunSummary, 'videos'>): boolean
8310
+ ```
8311
+
8312
+ Whether a run has at least one playable video recording.
8313
+
8117
8314
  #### `runList`
8118
8315
 
8119
8316
  ```ts
8120
- export function runList(history: TestHistory, version: string, limit = 100): RunList
8317
+ export function runList(history: TestHistory, version: string, limit = 100, query: RunQuery = {}): RunList
8121
8318
  ```
8122
8319
 
8123
- Every run the history knows of, newest first, `limit` at most (default 100).
8320
+ Every run the history knows of, newest first, `limit` at most (default 100). With `query.video` only the runs that
8321
+ have a playable video recording: the filter and `total` apply to every run before `limit`, and nothing is read from
8322
+ any recording's bytes. `withVideo` always counts every run that has one.
8124
8323
 
8125
8324
  #### `runDetail`
8126
8325
 
@@ -69,6 +69,22 @@ export const BackendInfoSchema = …
69
69
 
70
70
  A backend as the report shows it: where results came from, and whether it could be read.
71
71
 
72
+ #### `VideoRefSchema`
73
+
74
+ ```ts
75
+ export const VideoRefSchema = …
76
+ ```
77
+
78
+ One reference to a video held by a layer, check, retry attempt or step, found by the attachment's effective media type.
79
+
80
+ #### `VideoCountsSchema`
81
+
82
+ ```ts
83
+ export const VideoCountsSchema = …
84
+ ```
85
+
86
+ Video references as counts: how many are referenced, how many distinct recordings can be played, how many cannot.
87
+
72
88
  #### `EvidenceSummarySchema`
73
89
 
74
90
  ```ts
@@ -748,6 +764,47 @@ export type Attachment = z.output<typeof AttachmentSchema>;
748
764
 
749
765
  An attachment as stored in a layer, parsed from `AttachmentSchema`.
750
766
 
767
+ #### `RECORDING_EXPECTATIONS`
768
+
769
+ ```ts
770
+ export const RECORDING_EXPECTATIONS = …
771
+ ```
772
+
773
+ Whether a check is expected to have a screen recording (a video). `required`: the producer expects one, and the layer's
774
+ recording receipt says whether it arrived. `not-applicable`: the check is intentionally unrecorded (an API-only check),
775
+ so it counts as neither a recorded pass nor a missing video. An open string: a reader treats any other value, and its
776
+ absence, as no expectation.
777
+
778
+ #### `RecordingItemSchema`
779
+
780
+ ```ts
781
+ export const RecordingItemSchema = …
782
+ ```
783
+
784
+ The state of one check's recording in a receipt, in an open vocabulary. Conventions: `uploaded` (the video's bytes are in
785
+ the layer, with a checksum), `referenced` (only a `uri` was given: Atlas cannot vouch for it), `skipped` (the video was
786
+ captured but its bytes were left out, with a reason), `unresolved` (no video was attached at all), `not-applicable`
787
+ (intentionally unrecorded). A reader shows a state it does not know as it is and never counts it as uploaded.
788
+
789
+ #### `RecordingReceiptSchema`
790
+
791
+ ```ts
792
+ export const RecordingReceiptSchema = …
793
+ ```
794
+
795
+ What a layer's producer published about the recordings it expected: counts and one item per check that expected a
796
+ recording (or declared itself not applicable). Written by `publishChecks` when any check declares a
797
+ `recordingExpectation`; a layer without it says nothing, which a reader reports as Unknown, never Complete. It only
798
+ describes publication: it never changes a check's status, its slot or which result wins.
799
+
800
+ #### `RecordingReceipt`
801
+
802
+ ```ts
803
+ export type RecordingReceipt = z.output<typeof RecordingReceiptSchema>;
804
+ ```
805
+
806
+ A recording receipt as stored in a layer, parsed from `RecordingReceiptSchema`.
807
+
751
808
  #### `AnnotationSchema`
752
809
 
753
810
  ```ts
@@ -946,6 +1003,24 @@ export type PublishChecksInput = z.input<typeof PublishChecksSchema>;
946
1003
 
947
1004
  What a caller passes to `publishChecks`: the input type of `PublishChecksSchema`, before defaults.
948
1005
 
1006
+ #### `RecordingCompletenessSchema`
1007
+
1008
+ ```ts
1009
+ export const RecordingCompletenessSchema = …
1010
+ ```
1011
+
1012
+ Recording completeness across the winning results of a report: `complete` (every expected recording was published),
1013
+ `incomplete` (at least one is skipped or unresolved), `none-expected` (every layer declared its expectations, and none
1014
+ required a recording) or `unknown` (a layer has no receipt, so nothing is claimed). Independent of the checks' outcomes.
1015
+
1016
+ #### `RecordingCompleteness`
1017
+
1018
+ ```ts
1019
+ export type RecordingCompleteness = z.output<typeof RecordingCompletenessSchema>;
1020
+ ```
1021
+
1022
+ A parsed `RecordingCompletenessSchema` value.
1023
+
949
1024
  #### `REPORT_STATUS_SCHEMA_VERSION`
950
1025
 
951
1026
  ```ts
@@ -224,3 +224,32 @@ or the command line, an attachment may name a `path` instead; `atlas report publ
224
224
  ([attach evidence](extending.md#attach-evidence)). A CTRF report already carries most of this, and
225
225
  `atlas report publish --ctrf` keeps it ([native runner reporters](runners.md) cover CodeceptJS, Mocha, Jest and Node's
226
226
  test runner).
227
+
228
+ ## Recording completeness
229
+
230
+ A green run does not say whether the videos you expected were kept and published. Atlas answers that separately from
231
+ the checks' outcomes. Mark a check with `recordingExpectation`: `"required"` (expect a video of it) or
232
+ `"not-applicable"` (an API-only or intentionally unrecorded check). A check with neither is not part of the answer.
233
+
234
+ ```json
235
+ { "id": "checkout#pays", "title": "Pays", "status": "passed", "recordingExpectation": "required",
236
+ "attachments": [{ "name": "pays.webm", "role": "video", "path": "test-results/pays.webm" }] }
237
+ ```
238
+
239
+ When any check carries an expectation, `publishChecks` and `atlas report publish` write a receipt into the layer
240
+ (`recordings`): counts of expected, uploaded, referenced (only a `uri`, which Atlas cannot vouch for), skipped and
241
+ unresolved recordings, and for each expected check its state, a reason when it is not uploaded, and the size and SHA-256
242
+ of every stored video. A video over the size limit, or a file that is missing or not allowed, is kept as an attachment
243
+ without bytes whose `omitted` says why (`too-large`, `not-found`, `not-allowed`, `over-budget`) instead of failing the
244
+ publish; it never contains a path or a token.
245
+
246
+ - `atlas report publish --format json` prints the receipt under `layer.recordings` and a summary under `recordings`.
247
+ Add `--require-recordings` to exit 1 when an expected recording is skipped or unresolved. The layer and its test
248
+ results are still written and are not changed; only the command fails, so a CI step can report the evidence gap and
249
+ retry (publishing again reuses the stored files).
250
+ - `atlas report status --format json` has `recordings`: `state` is `complete`, `incomplete`, `none-expected` or
251
+ `unknown`, with counts and each gap. It never changes `ok`. A layer without a receipt is `unknown`, never `complete`.
252
+ - The viewer shows `Recordings 2 of 3: incomplete` beside the run's origin, apart from its green or red outcomes.
253
+
254
+ This is a statement about publication. Atlas does not check that a stored video plays in a browser, and does not yet
255
+ re-check later that a stored file is still readable.
package/docs/server.md CHANGED
@@ -276,7 +276,7 @@ with `format=markdown` (or `Accept: text/markdown`), and takes the project prefi
276
276
  | Only some: `q` (all words), `status` (or `flaky`), `environment`, `producer`, `domain`, `spec`, `tag`, `run`; `sort`, `offset`, `limit` | `GET /api/reports/tests?status=failed&environment=ios` |
277
277
  | One test: every recorded result (status, duration, environment, branch, commit, CI run), newest first | `GET /api/reports/tests/history?key=<key>` |
278
278
  | Why Specs, domains and counts disagree: ambiguous spec paths, shared domains, gaps, a reconciliation of the totals | `GET /api/reports/tests/health` |
279
- | The runs that recorded the report (by CI run URL, else branch and commit) | `GET /api/reports/runs` |
279
+ | The runs that recorded the report (by CI run URL, else branch and commit); `?video=true` keeps those with a playable video | `GET /api/reports/runs` |
280
280
  | Every test one run recorded | `GET /api/reports/runs/detail?key=<run key>` |
281
281
 
282
282
  ```sh
package/llms-full.txt CHANGED
@@ -1211,6 +1211,35 @@ or the command line, an attachment may name a `path` instead; `atlas report publ
1211
1211
  ([attach evidence](extending.md#attach-evidence)). A CTRF report already carries most of this, and
1212
1212
  `atlas report publish --ctrf` keeps it ([native runner reporters](runners.md) cover CodeceptJS, Mocha, Jest and Node's
1213
1213
  test runner).
1214
+
1215
+ ## Recording completeness
1216
+
1217
+ A green run does not say whether the videos you expected were kept and published. Atlas answers that separately from
1218
+ the checks' outcomes. Mark a check with `recordingExpectation`: `"required"` (expect a video of it) or
1219
+ `"not-applicable"` (an API-only or intentionally unrecorded check). A check with neither is not part of the answer.
1220
+
1221
+ ```json
1222
+ { "id": "checkout#pays", "title": "Pays", "status": "passed", "recordingExpectation": "required",
1223
+ "attachments": [{ "name": "pays.webm", "role": "video", "path": "test-results/pays.webm" }] }
1224
+ ```
1225
+
1226
+ When any check carries an expectation, `publishChecks` and `atlas report publish` write a receipt into the layer
1227
+ (`recordings`): counts of expected, uploaded, referenced (only a `uri`, which Atlas cannot vouch for), skipped and
1228
+ unresolved recordings, and for each expected check its state, a reason when it is not uploaded, and the size and SHA-256
1229
+ of every stored video. A video over the size limit, or a file that is missing or not allowed, is kept as an attachment
1230
+ without bytes whose `omitted` says why (`too-large`, `not-found`, `not-allowed`, `over-budget`) instead of failing the
1231
+ publish; it never contains a path or a token.
1232
+
1233
+ - `atlas report publish --format json` prints the receipt under `layer.recordings` and a summary under `recordings`.
1234
+ Add `--require-recordings` to exit 1 when an expected recording is skipped or unresolved. The layer and its test
1235
+ results are still written and are not changed; only the command fails, so a CI step can report the evidence gap and
1236
+ retry (publishing again reuses the stored files).
1237
+ - `atlas report status --format json` has `recordings`: `state` is `complete`, `incomplete`, `none-expected` or
1238
+ `unknown`, with counts and each gap. It never changes `ok`. A layer without a receipt is `unknown`, never `complete`.
1239
+ - The viewer shows `Recordings 2 of 3: incomplete` beside the run's origin, apart from its green or red outcomes.
1240
+
1241
+ This is a statement about publication. Atlas does not check that a stored video plays in a browser, and does not yet
1242
+ re-check later that a stored file is still readable.
1214
1243
  </doc>
1215
1244
 
1216
1245
  <doc path="docs/server.md">
@@ -1492,7 +1521,7 @@ with `format=markdown` (or `Accept: text/markdown`), and takes the project prefi
1492
1521
  | Only some: `q` (all words), `status` (or `flaky`), `environment`, `producer`, `domain`, `spec`, `tag`, `run`; `sort`, `offset`, `limit` | `GET /api/reports/tests?status=failed&environment=ios` |
1493
1522
  | One test: every recorded result (status, duration, environment, branch, commit, CI run), newest first | `GET /api/reports/tests/history?key=<key>` |
1494
1523
  | Why Specs, domains and counts disagree: ambiguous spec paths, shared domains, gaps, a reconciliation of the totals | `GET /api/reports/tests/health` |
1495
- | The runs that recorded the report (by CI run URL, else branch and commit) | `GET /api/reports/runs` |
1524
+ | The runs that recorded the report (by CI run URL, else branch and commit); `?video=true` keeps those with a playable video | `GET /api/reports/runs` |
1496
1525
  | Every test one run recorded | `GET /api/reports/runs/detail?key=<run key>` |
1497
1526
 
1498
1527
  ```sh
@@ -4315,6 +4344,7 @@ atlas report — layered evidence for any kind of spec
4315
4344
  [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]
4316
4345
  [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...
4317
4346
  [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]
4347
+ [--require-recordings]
4318
4348
  atlas report stream [--store <dir|s3://…|https://atlas.example.com>] --producer <name> [--environment <name>]
4319
4349
  [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...
4320
4350
  [--run-id <id>] [--run-title <t>]
@@ -4351,6 +4381,10 @@ atlas report — layered evidence for any kind of spec
4351
4381
  Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own
4352
4382
  folder and each --attach-root, and never from a hidden file or a link out: a report is data from a
4353
4383
  test run, not instructions. One that is missing or not allowed is skipped with a warning.
4384
+ --require-recordings exit 1 when a check that declared recordingExpectation "required" has no
4385
+ uploaded video (absent, left out as too large or not allowed, or refused). The layer is
4386
+ still written and the checks keep their results; only this command fails. The receipt of
4387
+ what was uploaded (counts, sizes, checksums) is in the JSON result under layer.recordings.
4354
4388
  From a checkout with uncommitted changes the layer is marked "modified".
4355
4389
  stream Send checks as they finish: one JSON object per line on standard input (the fields of one
4356
4390
  check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server
@@ -4998,9 +5032,10 @@ Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `A
4998
5032
 
4999
5033
  `GET /api/reports/runs` · role: read
5000
5034
 
5001
- Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned and up to twenty failures. Send `format=markdown` for Markdown.
5035
+ Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures and their video counts (references, distinct playable recordings, unavailable). Send `format=markdown` for Markdown.
5002
5036
 
5003
5037
  - `limit` (query): At most this many runs (default 100).
5038
+ - `video` (query): `true` to list only runs that have a playable video recording. Applied to every run before `limit`; `total` counts the matches. A server that does not know it answers with every run.
5004
5039
  - `format` (query): `markdown` for Markdown instead of JSON.
5005
5040
  - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
5006
5041
 
@@ -5010,12 +5045,13 @@ Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `40
5010
5045
 
5011
5046
  `GET /api/projects/{project}/reports/runs` · role: read
5012
5047
 
5013
- Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned and up to twenty failures. Send `format=markdown` for Markdown.
5048
+ Every run, newest first: layers grouped by their CI run URL (else branch and commit), with their environments, producers, how many tests they recorded, their results by status, the time they spanned, up to twenty failures and their video counts (references, distinct playable recordings, unavailable). Send `format=markdown` for Markdown.
5014
5049
 
5015
5050
  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.
5016
5051
 
5017
5052
  - `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.
5018
5053
  - `limit` (query): At most this many runs (default 100).
5054
+ - `video` (query): `true` to list only runs that have a playable video recording. Applied to every run before `limit`; `total` counts the matches. A server that does not know it answers with every run.
5019
5055
  - `format` (query): `markdown` for Markdown instead of JSON.
5020
5056
 
5021
5057
  Responses: `200` The runs.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/atlas",
3
- "version": "0.15.0",
3
+ "version": "0.17.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",
@@ -73,6 +73,10 @@
73
73
  "type": "number",
74
74
  "minimum": 0
75
75
  },
76
+ "recordingExpectation": {
77
+ "description": "`required`: expect a video of this check, and report whether it was published. `not-applicable`: intentionally unrecorded. Absent: no expectation. Never changes the check's status",
78
+ "type": "string"
79
+ },
76
80
  "tags": {
77
81
  "description": "Free labels to filter and group by: smoke, slow, flaky…",
78
82
  "type": "array",
@@ -169,6 +173,10 @@
169
173
  "description": {
170
174
  "type": "string"
171
175
  },
176
+ "omitted": {
177
+ "description": "Why the producer did not include its bytes, in an open vocabulary: too-large, not-found, not-allowed, over-budget, policy… Present on a metadata-only attachment, so a reader can tell \"left out\" from \"never recorded\"",
178
+ "type": "string"
179
+ },
172
180
  "tags": {
173
181
  "description": "Free labels of this one attachment, such as `demo`: shown as `#demo`, searchable, and normalized by `normalizeTag` (a leading `#` and letter case never matter). Unlike the tags of a check they say nothing about its result",
174
182
  "type": "array",
@@ -404,6 +412,10 @@
404
412
  "description": {
405
413
  "type": "string"
406
414
  },
415
+ "omitted": {
416
+ "description": "Why the producer did not include its bytes, in an open vocabulary: too-large, not-found, not-allowed, over-budget, policy… Present on a metadata-only attachment, so a reader can tell \"left out\" from \"never recorded\"",
417
+ "type": "string"
418
+ },
407
419
  "tags": {
408
420
  "description": "Free labels of this one attachment, such as `demo`: shown as `#demo`, searchable, and normalized by `normalizeTag` (a leading `#` and letter case never matter). Unlike the tags of a check they say nothing about its result",
409
421
  "type": "array",
@@ -676,6 +688,10 @@
676
688
  "description": {
677
689
  "type": "string"
678
690
  },
691
+ "omitted": {
692
+ "description": "Why the producer did not include its bytes, in an open vocabulary: too-large, not-found, not-allowed, over-budget, policy… Present on a metadata-only attachment, so a reader can tell \"left out\" from \"never recorded\"",
693
+ "type": "string"
694
+ },
679
695
  "tags": {
680
696
  "description": "Free labels of this one attachment, such as `demo`: shown as `#demo`, searchable, and normalized by `normalizeTag` (a leading `#` and letter case never matter). Unlike the tags of a check they say nothing about its result",
681
697
  "type": "array",
@@ -883,6 +899,10 @@
883
899
  "description": {
884
900
  "type": "string"
885
901
  },
902
+ "omitted": {
903
+ "description": "Why the producer did not include its bytes, in an open vocabulary: too-large, not-found, not-allowed, over-budget, policy… Present on a metadata-only attachment, so a reader can tell \"left out\" from \"never recorded\"",
904
+ "type": "string"
905
+ },
886
906
  "tags": {
887
907
  "description": "Free labels of this one attachment, such as `demo`: shown as `#demo`, searchable, and normalized by `normalizeTag` (a leading `#` and letter case never matter). Unlike the tags of a check they say nothing about its result",
888
908
  "type": "array",