@crvouga/atlas 0.9.0 → 0.10.1

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 (91) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/playwright-reporter/src/evidence.d.ts +122 -0
  3. package/dist/playwright-reporter/src/evidence.js +327 -0
  4. package/dist/playwright-reporter/src/index.d.ts +114 -0
  5. package/dist/playwright-reporter/src/index.js +312 -0
  6. package/dist/reports/src/compose.js +7 -14
  7. package/dist/reports/src/recorded-specs.d.ts +33 -0
  8. package/dist/reports/src/recorded-specs.js +42 -0
  9. package/dist/reports/src/report-index.d.ts +6 -0
  10. package/dist/reports/src/report-index.js +14 -2
  11. package/dist/reports/src/test-catalog.d.ts +386 -0
  12. package/dist/reports/src/test-catalog.js +655 -0
  13. package/dist/schema/src/api.d.ts +585 -0
  14. package/dist/schema/src/api.js +105 -0
  15. package/dist/schema/src/common.d.ts +2 -2
  16. package/dist/schema/src/manifest.d.ts +4 -4
  17. package/dist/schema/src/spec.d.ts +6 -6
  18. package/dist/server/src/app.js +82 -4
  19. package/dist/server/src/llms.js +14 -0
  20. package/dist/server/src/openapi.d.ts +1 -1
  21. package/dist/server/src/openapi.js +60 -3
  22. package/docs/agents.md +1 -0
  23. package/docs/integrating.md +1 -0
  24. package/docs/reference/http-api.md +134 -2
  25. package/docs/reference/playwright-reporter.md +332 -0
  26. package/docs/reference/reports.md +632 -0
  27. package/docs/reference/schema.md +57 -0
  28. package/docs/rich-evidence.md +198 -0
  29. package/docs/server.md +41 -0
  30. package/llms-full.txt +378 -2
  31. package/llms.txt +2 -0
  32. package/package.json +8 -3
  33. package/packages/schema/schemas/check-detail.schema.json +15 -15
  34. package/packages/schema/schemas/checks-result.schema.json +2 -2
  35. package/packages/schema/schemas/command-error.schema.json +4 -4
  36. package/packages/schema/schemas/doctor-result.schema.json +4 -4
  37. package/packages/schema/schemas/effective-delta.schema.json +69 -95
  38. package/packages/schema/schemas/effective-evidence.schema.json +63 -89
  39. package/packages/schema/schemas/evidence-page.schema.json +41 -59
  40. package/packages/schema/schemas/init-plan.schema.json +58 -90
  41. package/packages/schema/schemas/layer-rows.schema.json +17 -29
  42. package/packages/schema/schemas/layer.schema.json +54 -70
  43. package/packages/schema/schemas/live-run-start.schema.json +1 -1
  44. package/packages/schema/schemas/live-run.schema.json +1 -1
  45. package/packages/schema/schemas/merge-result.schema.json +6 -6
  46. package/packages/schema/schemas/outline.schema.json +7 -11
  47. package/packages/schema/schemas/path-record.schema.json +12 -28
  48. package/packages/schema/schemas/problem.schema.json +9 -9
  49. package/packages/schema/schemas/publish-checks.schema.json +27 -43
  50. package/packages/schema/schemas/publish-result.schema.json +16 -24
  51. package/packages/schema/schemas/ready.schema.json +12 -12
  52. package/packages/schema/schemas/report-event.schema.json +22 -28
  53. package/packages/schema/schemas/report-run.schema.json +225 -0
  54. package/packages/schema/schemas/report-runs.schema.json +163 -0
  55. package/packages/schema/schemas/report-status.schema.json +30 -54
  56. package/packages/schema/schemas/run-info.schema.json +9 -21
  57. package/packages/schema/schemas/run-summary.schema.json +6 -14
  58. package/packages/schema/schemas/server-config.schema.json +21 -21
  59. package/packages/schema/schemas/server-editable-settings.schema.json +14 -14
  60. package/packages/schema/schemas/server-info.schema.json +15 -19
  61. package/packages/schema/schemas/server-settings-snapshot.schema.json +28 -28
  62. package/packages/schema/schemas/spec-set.schema.json +16 -24
  63. package/packages/schema/schemas/state-node.schema.json +16 -16
  64. package/packages/schema/schemas/state-record.schema.json +5 -9
  65. package/packages/schema/schemas/store-listing.schema.json +3 -7
  66. package/packages/schema/schemas/store-write-result.schema.json +2 -2
  67. package/packages/schema/schemas/stream-result.schema.json +13 -17
  68. package/packages/schema/schemas/test-catalog.schema.json +1135 -0
  69. package/packages/schema/schemas/test-history.schema.json +632 -0
  70. package/packages/schema/schemas/timeline-entry.schema.json +2 -2
  71. package/packages/schema/schemas/transition-record.schema.json +9 -21
  72. package/packages/server/openapi.json +4417 -1382
  73. package/packages/visualizer/src/app/AppShell.tsx +7 -3
  74. package/packages/visualizer/src/app/router.tsx +26 -3
  75. package/packages/visualizer/src/components/Header.tsx +2 -2
  76. package/packages/visualizer/src/evidence/evidence-context.tsx +10 -1
  77. package/packages/visualizer/src/explorer/Explorer.tsx +433 -0
  78. package/packages/visualizer/src/explorer/ExplorerIndex.tsx +120 -0
  79. package/packages/visualizer/src/explorer/RunsView.tsx +164 -0
  80. package/packages/visualizer/src/explorer/TestDetail.tsx +214 -0
  81. package/packages/visualizer/src/explorer/catalog.tsx +274 -0
  82. package/packages/visualizer/src/explorer/explorer.module.css +1238 -0
  83. package/packages/visualizer/src/explorer/groups.test.ts +87 -0
  84. package/packages/visualizer/src/explorer/groups.ts +126 -0
  85. package/packages/visualizer/src/explorer/navigate.ts +36 -0
  86. package/packages/visualizer/src/explorer/parts.tsx +59 -0
  87. package/packages/visualizer/src/explorer/search.ts +97 -0
  88. package/packages/visualizer/src/find/Finder.tsx +2 -0
  89. package/packages/visualizer/src/find/corpus.ts +13 -6
  90. package/packages/visualizer/src/find/rank.ts +2 -0
  91. package/packages/visualizer/src/evidence/project-home.tsx +0 -45
package/llms-full.txt CHANGED
@@ -120,6 +120,7 @@ The complete public export map is generated from the package manifest (wildcards
120
120
  @crvouga/atlas/codecept
121
121
  @crvouga/atlas/jest
122
122
  @crvouga/atlas/node-test
123
+ @crvouga/atlas/playwright-reporter
123
124
  ```
124
125
  <!-- /generated:public-exports -->
125
126
 
@@ -605,6 +606,7 @@ bunx atlas report publish --producer e2e --junit out.xml --attach ci.log --attac
605
606
  Files are stored in the layer by content, shown by what they are (pictures, videos, page snapshots, text) and
606
607
  offered for download otherwise. [Evidence](extending.md) is the full model, and why a new kind of evidence never
607
608
  breaks anything.
609
+ [Send rich evidence](rich-evidence.md) lists what to send and what the viewer does with it, and covers the Playwright Test reporter.
608
610
 
609
611
  ### Statecharts
610
612
 
@@ -949,6 +951,207 @@ Run `atlas server` once for the projects the node hosts; point local and CI publ
949
951
  Native reporters send a heartbeat every 5 seconds. The server interrupts invocations after 30 seconds of silence, or after the configurable progress-stall timeout. It preserves finished results and never marks missing tests as passed. See [live test runs](server.md#live-test-runs-and-heartbeats).
950
952
  </doc>
951
953
 
954
+ <doc path="docs/rich-evidence.md">
955
+ # Send rich evidence
956
+
957
+ A check that says only its title and `passed` answers one question: did it pass? A check that also has its duration,
958
+ the device it ran on, its steps, its retries and its video answers the next ones too: how slow is it, where does it
959
+ fail, what happened, is it flaky. This guide covers which fields a producer should send, what the viewer does with
960
+ each, and two ways to send them: the Playwright Test reporter, which sends all of them for you, and `createReporter` /
961
+ `publishChecks` for any other producer.
962
+
963
+ Every field here is optional and none of them changes a result: a check's status, its slot and which result wins are
964
+ decided exactly as before ([the report contract](report-contract.md)). Evidence only adds information.
965
+ [Evidence](extending.md) is the full model, with the rules that keep it compatible.
966
+
967
+ - [What to send, and what it is for](#what-to-send-and-what-it-is-for)
968
+ - [Labels describe, parameters split](#labels-describe-parameters-split)
969
+ - [Playwright Test](#playwright-test)
970
+ - [Any other producer](#any-other-producer)
971
+
972
+ ## What to send, and what it is for
973
+
974
+ | Field | Where | What the viewer does with it |
975
+ | --- | --- | --- |
976
+ | `durationMs` | check, attempt, step | Averages, p95 and the slowest tests, per environment and over time. |
977
+ | `environment` | layer | Which client was checked (`desktop-web`, `mobile-web`, `ios`, `android`…). A check on two environments is two results, side by side. |
978
+ | `labels` (`browser`, `device`, `os`, `viewport`…) | check, layer | Shows each result's browser, device and system, and lets you filter by them. |
979
+ | `attachments` with a `role` | check, attempt, step, layer | Plays `video`, shows a `screenshot`, opens a `trace`, shows logs and offers anything else for download. |
980
+ | `steps` (`parentId`, `offsetMs`, `durationMs`, `status`, `kind`) | check, attempt | Draws the check's timeline: nested steps, when each started, how long it took, which one failed. |
981
+ | `attempts` | check | Every try of a retried check, with its own status, detail, steps and attachments. A check that passed after failing is flaky. |
982
+ | `tags`, `labels` | check, layer | Filters and groups (`smoke`, `flaky`, an owner, a browser). |
983
+ | `annotations`, `links` | check, step, attempt, layer | Notes, warnings and issue links next to the result. |
984
+ | `specPath` | check | The spec a check belongs to (a `.feature`, a test file), relative to the repository. Checks are grouped by it. |
985
+ | `specs[]` (`path`, `domain`, `kind`, `title`) | layer | Names each spec and its group (`domain`), so a server can group checks by spec with a title even when no [spec set](integrating.md#tell-atlas-which-spec-a-check-belongs-to) has been published. |
986
+ | `detail` | check, attempt, step | Why it failed: the error message and stack, as plain text. |
987
+
988
+ Send `sha`, `branch` and `runUrl` on every layer too. They never change a result, but they tie results to a commit
989
+ and to the CI run that produced them.
990
+
991
+ ## Labels describe, parameters split
992
+
993
+ A result's **slot** is its producer, check id, part kind, spec, environment and **parameters**. The newest completed
994
+ result of a slot wins. So:
995
+
996
+ - Put what describes a run in `labels`: browser, device name, viewport, operating system, Playwright version, shard.
997
+ Two runs that differ only in labels fill the same slot, and the newer one wins.
998
+ - Put what makes a run a different result in `parameters` (a locale, a tenant, a feature flag): each value is a slot
999
+ of its own, kept side by side.
1000
+ - Put the client in `environment`. It is a parameter that every view understands.
1001
+
1002
+ If you label a check `device: Pixel 7` on one run and `device: iPhone 15` on the next, both in `mobile-web`, the report
1003
+ shows the newer one. To see both, give them different environments or a parameter.
1004
+
1005
+ ## Playwright Test
1006
+
1007
+ `@crvouga/atlas/playwright-reporter` is a Playwright Test reporter. Add it beside your console reporter:
1008
+
1009
+ ```ts
1010
+ // playwright.config.ts
1011
+ import { defineConfig, devices } from '@playwright/test';
1012
+
1013
+ export default defineConfig({
1014
+ retries: process.env.CI ? 2 : 0,
1015
+ reporter: [['list'], ['@crvouga/atlas/playwright-reporter', { project: 'checkout', producer: 'e2e-web' }]],
1016
+ use: { video: 'retain-on-failure', trace: 'retain-on-failure', screenshot: 'only-on-failure' },
1017
+ projects: [
1018
+ { name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } },
1019
+ { name: 'Pixel 7', use: { ...devices['Pixel 7'] } }
1020
+ ]
1021
+ });
1022
+ ```
1023
+
1024
+ Then run your tests as usual, with the destination in the environment:
1025
+
1026
+ ```sh
1027
+ ATLAS_SERVER_URL=https://atlas.example.com ATLAS_TOKEN=$ATLAS_PUBLISH_TOKEN ATLAS_PROJECT=checkout npx playwright test
1028
+ # … Playwright's own output …
1029
+ # Atlas: 3512 checks in 140 layers to https://atlas.example.com
1030
+ ```
1031
+
1032
+ What it sends:
1033
+
1034
+ - Each test as `running` when it starts, then with its result when its last try ends, every few seconds, so the
1035
+ report follows the run as it happens.
1036
+ - One stream of layers per Playwright project. The environment is `mobile-web` for a project that emulates a mobile
1037
+ device (`use.isMobile`), else `desktop-web`. Every check is labelled `browser`, `device` (the project name),
1038
+ `viewport`, `os` and `playwright` (its version), and `shard` when the run is sharded.
1039
+ - A check id of `<specPath>#<describe titles> > <test title>`, the same in every run.
1040
+ - `passed` for a test that got the result it expected, `failed` for a failure, a timeout or an interruption,
1041
+ `not-run` for a skipped test or one the run ended before it finished. A test that passed on a retry is `passed`,
1042
+ tagged and labelled `flaky`, with one attempt per try.
1043
+ - The failure message and stack without terminal colours, the duration, `test.tags` (without `@`) and annotation
1044
+ types as tags, annotations, links (annotations whose description is a URL), and Playwright's steps as the timeline
1045
+ (`hook`, `fixture`, `action` for `pw:api`, `assertion` for `expect`, `step` for `test.step`).
1046
+ - Every attachment, bytes included: `video`, `screenshot` and `trace` take that role, other attachments their own
1047
+ name, and standard output and error are `log`s. Attachments of earlier tries go with their attempt.
1048
+ - The project's spec files in `specs[]`, so the server can group checks by spec.
1049
+
1050
+ It never fails the run. A destination that is not set, a server that is down or a file that cannot be read is said
1051
+ once, and the summary line says how many checks could not be sent.
1052
+
1053
+ ### Options
1054
+
1055
+ | Option | Default | |
1056
+ | --- | --- | --- |
1057
+ | `url` | `ATLAS_SERVER_URL`, else `ATLAS_STORE` | The Atlas server, or any store location (`./atlas-reports`, `s3://bucket/prefix`). |
1058
+ | `token` | `ATLAS_PUBLISH_TOKEN`, else `ATLAS_TOKEN` (or `ATLAS_TOKEN_FILE`) | A publish token. A token is never sent over plain `http` beyond this machine. |
1059
+ | `project` | `ATLAS_PROJECT` | The project on a shared Atlas server. |
1060
+ | `store` | | Publish to this `ObjectStore` instead of a URL (a directory store, an in-memory store in tests). |
1061
+ | `producer` | `playwright` | A stable name for this suite. |
1062
+ | `kind` | `checks` | The part kind of every layer. |
1063
+ | `environment` | `mobile-web` or `desktop-web` | A string, or `(project) => string`. |
1064
+ | `parameters` | `{}` | Choices that make a result its own slot. An object, or `(project) => object`. |
1065
+ | `labels` | | More labels for every check. An object, or `(project) => object`. |
1066
+ | `sha`, `branch`, `runUrl` | `ATLAS_SHA`, `ATLAS_BRANCH`, then the CI's variables (`GITHUB_SHA`, `GITHUB_HEAD_REF` / `GITHUB_REF_NAME`, the GitHub Actions run URL…), then git | Where the run came from. |
1067
+ | `specPath` | the `.feature` of a playwright-bdd test, else the test file | `(test) => string`, relative to the repository. |
1068
+ | `rootDir` | the git root above Playwright's `rootDir` | Paths are relative to it. |
1069
+ | `attachments` | `all` | `all`, `failures` (failed checks and failed tries only) or `none`. |
1070
+ | `maxAttachmentBytes` | 50 MiB | A larger file is not uploaded; the check gets an `attachment-skipped` annotation. |
1071
+ | `maxSteps` | 500 | Timeline steps kept per try; the rest are counted in a `steps-truncated` annotation. |
1072
+ | `specs` | `true` | Record the project's spec files in each layer. |
1073
+ | `flushMs`, `batchSize`, `attempts` | 5000, 50, 5 | How often to send, how many checks to send at once, how many tries per layer at the end. |
1074
+ | `quiet` | `false` | Print nothing, not even the summary. |
1075
+
1076
+ Playwright decides what there is to upload: set `video`, `trace` and `screenshot` in `use` (`retain-on-failure` and
1077
+ `only-on-failure` keep uploads small), and anything you `testInfo.attach()` is uploaded too. Each file is stored once
1078
+ by its content, in chunks, so a server's object size limit does not cap a video.
1079
+
1080
+ ### playwright-bdd
1081
+
1082
+ Tests generated by [playwright-bdd](https://github.com/vitalets/playwright-bdd) belong to their `.feature` file: a
1083
+ test in `.features-gen/<path>.feature.spec.js` gets the `specPath` of `<path>.feature`, found beside `.features-gen`
1084
+ (a per-project output folder in between is skipped). The scenario's title is the check's title (an outline's example
1085
+ is `<outline>: Example #1`), feature tags are tags, `Given` / `When` / `Then` steps are timeline steps of that kind,
1086
+ and the feature's name is the spec's title in `specs[]`. If your features are elsewhere, give `specPath`:
1087
+
1088
+ ```ts
1089
+ ['@crvouga/atlas/playwright-reporter', { specPath: (test) => test.location.file.replace(/^.*\/generated\//, 'features/').replace(/\.spec\.js$/, '') }]
1090
+ ```
1091
+
1092
+ ## Any other producer
1093
+
1094
+ `createReporter` streams checks as they finish, one layer per flush, to a server or any store
1095
+ ([streaming a run](server.md#streaming-a-run-as-it-happens)); `publishChecks` publishes one finished layer. Both take
1096
+ the same check. Attachment bytes go in as `data` (a `Uint8Array`); read files yourself (only the command line reads a
1097
+ `path`). Here is a check that carries everything:
1098
+
1099
+ ```ts
1100
+ import { readFile } from 'node:fs/promises';
1101
+ import { createReporter } from '@crvouga/atlas/reports/reporter';
1102
+ import { httpStore } from '@crvouga/atlas/reports/http-store';
1103
+
1104
+ const reporter = createReporter(
1105
+ {
1106
+ producer: 'e2e-web',
1107
+ environment: 'mobile-web',
1108
+ sha: process.env.GITHUB_SHA ?? null,
1109
+ branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || null,
1110
+ runUrl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`,
1111
+ // Names and groups each spec, so checks are grouped by spec without a published spec set.
1112
+ specs: [{ path: 'features/checkout/pay.feature', domain: 'checkout', kind: 'feature', title: 'Paying for an order' }]
1113
+ },
1114
+ { store: httpStore({ url: 'https://atlas.example.com', project: 'shop', token: process.env.ATLAS_TOKEN }) }
1115
+ );
1116
+
1117
+ reporter.check({ id: 'features/checkout/pay.feature#Pays by card', title: 'Pays by card', status: 'running', specPath: 'features/checkout/pay.feature' });
1118
+
1119
+ reporter.check({
1120
+ id: 'features/checkout/pay.feature#Pays by card',
1121
+ title: 'Pays by card',
1122
+ status: 'passed',
1123
+ detail: 'Passed on retry 1 (flaky)',
1124
+ specPath: 'features/checkout/pay.feature',
1125
+ durationMs: 4180,
1126
+ tags: ['smoke', 'flaky'],
1127
+ labels: { browser: 'chromium', device: 'Pixel 7', os: 'linux', viewport: '412x839' },
1128
+ steps: [
1129
+ { id: 's1', name: 'Given a cart with one item', kind: 'given', status: 'passed', offsetMs: 0, durationMs: 900 },
1130
+ { id: 's2', name: 'When I pay by card', kind: 'when', status: 'passed', offsetMs: 900, durationMs: 2600 },
1131
+ { id: 's3', parentId: 's2', name: 'click #pay', kind: 'action', status: 'passed', offsetMs: 1200, durationMs: 80 },
1132
+ { id: 's4', name: 'Then I see the receipt', kind: 'then', status: 'passed', offsetMs: 3500, durationMs: 680 }
1133
+ ],
1134
+ attempts: [
1135
+ { index: 0, status: 'failed', durationMs: 30000, startedAtMs: Date.now() - 40000, detail: 'Timeout 30000ms exceeded',
1136
+ attachments: [{ name: 'retry-0.png', role: 'screenshot', contentType: 'image/png', data: await readFile('test-results/retry-0.png') }] },
1137
+ { index: 1, status: 'passed', durationMs: 4180, startedAtMs: Date.now() - 5000 }
1138
+ ],
1139
+ attachments: [
1140
+ { name: 'video.webm', role: 'video', contentType: 'video/webm', data: await readFile('test-results/video.webm') },
1141
+ { name: 'trace.zip', role: 'trace', contentType: 'application/zip', data: await readFile('test-results/trace.zip') }
1142
+ ]
1143
+ });
1144
+
1145
+ const { checks, layers, lost } = await reporter.close(); // never throws; `lost` counts what never arrived
1146
+ ```
1147
+
1148
+ With `publishChecks`, pass the same identity and checks with `recordedAtMs`, once the run is over. From a checks file
1149
+ or the command line, an attachment may name a `path` instead; `atlas report publish` reads it
1150
+ ([attach evidence](extending.md#attach-evidence)). A CTRF report already carries most of this, and
1151
+ `atlas report publish --ctrf` keeps it ([native runner reporters](runners.md) cover CodeceptJS, Mocha, Jest and Node's
1152
+ test runner).
1153
+ </doc>
1154
+
952
1155
  <doc path="docs/server.md">
953
1156
  # The Atlas server
954
1157
 
@@ -1195,6 +1398,39 @@ report the server would have sent whole (`applyEffectiveDelta`, tested against e
1195
1398
  runs); a version the server does not remember, or an older server, answers with the whole report, which a client
1196
1399
  takes as it always did. A result streamed into a 100 000-check report costs a client a few kilobytes.
1197
1400
 
1401
+ ## Reading the report as an agent
1402
+
1403
+ The effective report says what each check's result is now. To explore a project, or to answer "what is tested,
1404
+ where, how long does it take, what is flaky", read the tests instead. Every endpoint here answers JSON, or Markdown
1405
+ with `format=markdown` (or `Accept: text/markdown`), and takes the project prefix like every other
1406
+ (`/api/projects/<slug>/reports/tests`).
1407
+
1408
+ | Ask | Endpoint |
1409
+ | --- | --- |
1410
+ | Every test, a summary (by status, environment, producer, domain), insights (failing, flaky, slowest) and facets | `GET /api/reports/tests` |
1411
+ | 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` |
1412
+ | One test: every recorded result (status, duration, environment, branch, commit, CI run), newest first | `GET /api/reports/tests/history?key=<key>` |
1413
+ | The runs that recorded the report (by CI run URL, else branch and commit) | `GET /api/reports/runs` |
1414
+ | Every test one run recorded | `GET /api/reports/runs/detail?key=<run key>` |
1415
+
1416
+ ```sh
1417
+ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdown&limit=50"
1418
+ ```
1419
+
1420
+ - A **test** is a check identity, its spec path and id, across producers and environments. `outcomes` holds its
1421
+ current result on each environment (the report's winners, so a test's status here is always the gate's);
1422
+ `stats` describes its recorded history: pass rate, flips, whether it is flaky and why (it passed and failed on the
1423
+ same commit, passed only after a retry, or its producer said so), durations (avg, p50, p95, min, max, last), runs
1424
+ and branches.
1425
+ - The server keeps the newest 100 results of every test (progress reported as `running` is not history) and every run's last result per test, in memory, as layers
1426
+ arrive; it is rebuilt from the layers on restart. History never changes a result.
1427
+ - Without a published spec set, tests are grouped under the specs their layers recorded (`specs[]`: path, domain,
1428
+ title), and the gate and the outline do the same, so a project needs no spec set to be browsable. A path no layer
1429
+ described is titled from its file name and grouped by its folder under `specs/`.
1430
+ - Statistics are only as rich as what producers send: durations need `durationMs`, devices need an environment and
1431
+ labels (`browser`, `device`, `os`), videos and screenshots need attachments. See
1432
+ [Send rich evidence](rich-evidence.md).
1433
+
1198
1434
  ## API reference
1199
1435
 
1200
1436
  <!-- generated:server-operations -->
@@ -1235,6 +1471,14 @@ takes as it always did. A result streamed into a 100 000-check report costs a cl
1235
1471
  | `PUT /api/projects/{project}/store/{key}` | publish | Upload an object. |
1236
1472
  | `DELETE /api/store/{key}` | publish | Remove a scratch object. |
1237
1473
  | `DELETE /api/projects/{project}/store/{key}` | publish | Remove a scratch object. |
1474
+ | `GET /api/reports/tests` | read | Every test, with insights, facets and history statistics. |
1475
+ | `GET /api/projects/{project}/reports/tests` | read | Every test, with insights, facets and history statistics. |
1476
+ | `GET /api/reports/tests/history` | read | One test and every result recorded for it. |
1477
+ | `GET /api/projects/{project}/reports/tests/history` | read | One test and every result recorded for it. |
1478
+ | `GET /api/reports/runs` | read | The runs that recorded the report. |
1479
+ | `GET /api/projects/{project}/reports/runs` | read | The runs that recorded the report. |
1480
+ | `GET /api/reports/runs/detail` | read | Every test one run recorded. |
1481
+ | `GET /api/projects/{project}/reports/runs/detail` | read | Every test one run recorded. |
1238
1482
  | `GET /api/reports/outline` | read | The specs a report is laid over. |
1239
1483
  | `GET /api/projects/{project}/reports/outline` | read | The specs a report is laid over. |
1240
1484
  | `GET /api/reports/outline/spec` | read | The text of one spec. |
@@ -4201,11 +4445,143 @@ Scoped to one project. The same project may be selected on the unprefixed API us
4201
4445
 
4202
4446
  Responses: `204` Removed (or it was not there).; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` Not a scratch object, or the token may not.
4203
4447
 
4448
+ ### Every test, with insights, facets and history statistics
4449
+
4450
+ `GET /api/reports/tests` · role: read
4451
+
4452
+ The report as a list of tests, for exploring it or for an agent to read in one request. A test is a check identity (its spec path and id) across producers and environments; `outcomes` holds its current result on each environment, and `stats` how it went over its recorded history (pass rate, flips, flaky, durations avg/p50/p95, runs, branches).
4453
+
4454
+ `summary` and `facets` cover every test (counts by status, by environment, by producer and by domain, the slowest, flakiest and failing tests); `tests` is the page the query matched. Without a published spec set, specs come from the `specs[]` the layers recorded.
4455
+
4456
+ Send `format=markdown` (or `Accept: text/markdown`) for the same as a Markdown report. The current status of a test is always the report's winner; history never changes it.
4457
+
4458
+ - `q` (query): Words that must all appear in the title, id, spec, domain, tags, labels, environments or producers. Case does not matter.
4459
+ - `status` (query): Current status, or `flaky`. Repeatable or comma-separated.
4460
+ - `environment` (query): Only tests with a result on this environment. Repeatable or comma-separated.
4461
+ - `producer` (query): Only tests from this producer. Repeatable or comma-separated.
4462
+ - `domain` (query): Only tests whose spec is in this domain (`(no spec)` for none). Repeatable or comma-separated.
4463
+ - `spec` (query): Only tests of this spec path. Repeatable or comma-separated.
4464
+ - `tag` (query): Only tests with this tag. Repeatable or comma-separated.
4465
+ - `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
4466
+ - `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
4467
+ - `offset` (query): Skip this many matching tests.
4468
+ - `limit` (query): At most this many tests (default 500, at most 20000).
4469
+ - `format` (query): `markdown` for a Markdown report instead of JSON.
4470
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4471
+
4472
+ Responses: `200` The catalog.; `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`.
4473
+
4474
+ ### Every test, with insights, facets and history statistics
4475
+
4476
+ `GET /api/projects/{project}/reports/tests` · role: read
4477
+
4478
+ The report as a list of tests, for exploring it or for an agent to read in one request. A test is a check identity (its spec path and id) across producers and environments; `outcomes` holds its current result on each environment, and `stats` how it went over its recorded history (pass rate, flips, flaky, durations avg/p50/p95, runs, branches).
4479
+
4480
+ `summary` and `facets` cover every test (counts by status, by environment, by producer and by domain, the slowest, flakiest and failing tests); `tests` is the page the query matched. Without a published spec set, specs come from the `specs[]` the layers recorded.
4481
+
4482
+ Send `format=markdown` (or `Accept: text/markdown`) for the same as a Markdown report. The current status of a test is always the report's winner; history never changes it.
4483
+
4484
+ 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.
4485
+
4486
+ - `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.
4487
+ - `q` (query): Words that must all appear in the title, id, spec, domain, tags, labels, environments or producers. Case does not matter.
4488
+ - `status` (query): Current status, or `flaky`. Repeatable or comma-separated.
4489
+ - `environment` (query): Only tests with a result on this environment. Repeatable or comma-separated.
4490
+ - `producer` (query): Only tests from this producer. Repeatable or comma-separated.
4491
+ - `domain` (query): Only tests whose spec is in this domain (`(no spec)` for none). Repeatable or comma-separated.
4492
+ - `spec` (query): Only tests of this spec path. Repeatable or comma-separated.
4493
+ - `tag` (query): Only tests with this tag. Repeatable or comma-separated.
4494
+ - `run` (query): Only tests recorded in this run (a `key` from `/api/reports/runs`).
4495
+ - `sort` (query): The order: `spec` (default: domain, spec, title), `title`, `recent`, `slowest`, `failures`, `flaky` or `runs`.
4496
+ - `offset` (query): Skip this many matching tests.
4497
+ - `limit` (query): At most this many tests (default 500, at most 20000).
4498
+ - `format` (query): `markdown` for a Markdown report instead of JSON.
4499
+
4500
+ Responses: `200` The catalog.; `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`.
4501
+
4502
+ ### One test and every result recorded for it
4503
+
4504
+ `GET /api/reports/tests/history` · role: read
4505
+
4506
+ The test (as `/api/reports/tests` lists it) and up to 100 recorded results, newest first: status, duration, environment, producer, branch, commit, CI run, layer and the first 500 characters of the detail. Open a result's evidence (attachments, steps, retries) with `/api/reports/evidence/detail/{backend}/{id}?check=<id>`. Send `format=markdown` for Markdown.
4507
+
4508
+ - `key` (query, required): The test's `key` from `/api/reports/tests`: `JSON.stringify([specPath, id])`, URL-encoded.
4509
+ - `format` (query): `markdown` for Markdown instead of JSON.
4510
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4511
+
4512
+ Responses: `200` The test and its history.; `304` Unchanged since the `If-None-Match` ETag.; `400` No `key`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No test in the report has that key.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
4513
+
4514
+ ### One test and every result recorded for it
4515
+
4516
+ `GET /api/projects/{project}/reports/tests/history` · role: read
4517
+
4518
+ The test (as `/api/reports/tests` lists it) and up to 100 recorded results, newest first: status, duration, environment, producer, branch, commit, CI run, layer and the first 500 characters of the detail. Open a result's evidence (attachments, steps, retries) with `/api/reports/evidence/detail/{backend}/{id}?check=<id>`. Send `format=markdown` for Markdown.
4519
+
4520
+ 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.
4521
+
4522
+ - `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.
4523
+ - `key` (query, required): The test's `key` from `/api/reports/tests`: `JSON.stringify([specPath, id])`, URL-encoded.
4524
+ - `format` (query): `markdown` for Markdown instead of JSON.
4525
+
4526
+ Responses: `200` The test and its history.; `304` Unchanged since the `If-None-Match` ETag.; `400` No `key`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No test in the report has that key.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
4527
+
4528
+ ### The runs that recorded the report
4529
+
4530
+ `GET /api/reports/runs` · role: read
4531
+
4532
+ 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.
4533
+
4534
+ - `limit` (query): At most this many runs (default 100).
4535
+ - `format` (query): `markdown` for Markdown instead of JSON.
4536
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4537
+
4538
+ 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`.
4539
+
4540
+ ### The runs that recorded the report
4541
+
4542
+ `GET /api/projects/{project}/reports/runs` · role: read
4543
+
4544
+ 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.
4545
+
4546
+ 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.
4547
+
4548
+ - `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.
4549
+ - `limit` (query): At most this many runs (default 100).
4550
+ - `format` (query): `markdown` for Markdown instead of JSON.
4551
+
4552
+ 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`.
4553
+
4554
+ ### Every test one run recorded
4555
+
4556
+ `GET /api/reports/runs/detail` · role: read
4557
+
4558
+ One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.
4559
+
4560
+ - `key` (query, required): The run's `key` from `/api/reports/runs`, URL-encoded.
4561
+ - `format` (query): `markdown` for Markdown instead of JSON.
4562
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4563
+
4564
+ Responses: `200` The run and its results.; `304` Unchanged since the `If-None-Match` ETag.; `400` No `key`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No run has that key.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
4565
+
4566
+ ### Every test one run recorded
4567
+
4568
+ `GET /api/projects/{project}/reports/runs/detail` · role: read
4569
+
4570
+ One run and its last result for every test, environment and producer it recorded, failures first. Send `format=markdown` for Markdown.
4571
+
4572
+ 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.
4573
+
4574
+ - `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.
4575
+ - `key` (query, required): The run's `key` from `/api/reports/runs`, URL-encoded.
4576
+ - `format` (query): `markdown` for Markdown instead of JSON.
4577
+
4578
+ Responses: `200` The run and its results.; `304` Unchanged since the `If-None-Match` ETag.; `400` No `key`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No run has that key.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
4579
+
4204
4580
  ### The specs a report is laid over
4205
4581
 
4206
4582
  `GET /api/reports/outline` · role: read
4207
4583
 
4208
- The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. A spec here with no result shows as having none.
4584
+ The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. Without a spec set, the specs the layers recorded (`specs[]`), newest per path. A spec here with no result shows as having none.
4209
4585
 
4210
4586
  - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
4211
4587
 
@@ -4215,7 +4591,7 @@ Responses: `200` The outline.; `401` No token, or an unknown one. Send `Authoriz
4215
4591
 
4216
4592
  `GET /api/projects/{project}/reports/outline` · role: read
4217
4593
 
4218
- The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. A spec here with no result shows as having none.
4594
+ The specs of the newest committed spec set (of `main`, else the newest), so the report needs no checkout. Without a spec set, the specs the layers recorded (`specs[]`), newest per path. A spec here with no result shows as having none.
4219
4595
 
4220
4596
  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.
4221
4597
 
package/llms.txt CHANGED
@@ -10,6 +10,7 @@ Given only the repository link, start at docs/agents.md: install `@crvouga/atlas
10
10
  - [Verify Atlas locally](https://raw.githubusercontent.com/crvouga/atlas/main/docs/quickstart.md): Prove that the installed Atlas can publish, combine and gate evidence before connecting your project's real suites.
11
11
  - [Integrating Atlas into a project](https://raw.githubusercontent.com/crvouga/atlas/main/docs/integrating.md): This guide takes a project from nothing to a layered, gated report of its specs.
12
12
  - [Native test runner reporters](https://raw.githubusercontent.com/crvouga/atlas/main/docs/runners.md): Projects upload results to a shared Atlas node under their own project slug.
13
+ - [Send rich evidence](https://raw.githubusercontent.com/crvouga/atlas/main/docs/rich-evidence.md): A check that says only its title and passed answers one question:
13
14
 
14
15
  ## Run and use the server
15
16
 
@@ -43,6 +44,7 @@ Given only the repository link, start at docs/agents.md: install `@crvouga/atlas
43
44
  - [TypeScript API: detox](https://raw.githubusercontent.com/crvouga/atlas/main/docs/reference/detox.md): The Detox driver for statechart runs of React Native apps.
44
45
  - [TypeScript API: runners](https://raw.githubusercontent.com/crvouga/atlas/main/docs/reference/runners.md): Native reporters that upload to the project server.
45
46
  - [TypeScript API: vitest](https://raw.githubusercontent.com/crvouga/atlas/main/docs/reference/vitest.md): Run a statechart plan as Vitest tests.
47
+ - [TypeScript API: playwright-reporter](https://raw.githubusercontent.com/crvouga/atlas/main/docs/reference/playwright-reporter.md): A Playwright Test reporter that streams every test to Atlas with its duration, device labels, steps, retries, videos, screenshots and traces.
46
48
  - [JSON Schemas](https://unpkg.com/@crvouga/atlas/packages/schema/schemas/): the layer format, checks file, report status, command results, server responses, server configuration.
47
49
 
48
50
  ## Optional
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/atlas",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
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",
@@ -111,6 +111,10 @@
111
111
  "./node-test": {
112
112
  "types": "./dist/runners/src/node-test.d.ts",
113
113
  "default": "./dist/runners/src/node-test.js"
114
+ },
115
+ "./playwright-reporter": {
116
+ "types": "./dist/playwright-reporter/src/index.d.ts",
117
+ "default": "./dist/playwright-reporter/src/index.js"
114
118
  }
115
119
  },
116
120
  "publishConfig": {
@@ -141,6 +145,7 @@
141
145
  "docs/deploying.md",
142
146
  "docs/server.md",
143
147
  "docs/extending.md",
148
+ "docs/rich-evidence.md",
144
149
  "docs/statecharts.md",
145
150
  "docs/report-contract.md",
146
151
  "docs/report-backends.md",
@@ -202,10 +207,10 @@
202
207
  "react-dom": "^19.3.0",
203
208
  "solid-js": "^1.9.15",
204
209
  "tsx": "4.23.13",
205
- "vite": "7.3.6",
210
+ "vite": "7.3.7",
206
211
  "xstate": "5.33.2",
207
212
  "yaml": "^2.9.0",
208
- "zod": "^3.25.76",
213
+ "zod": "^4.6.7",
209
214
  "zustand": "^5.0.15"
210
215
  },
211
216
  "peerDependencies": {
@@ -13,9 +13,9 @@
13
13
  "type": "object",
14
14
  "properties": {
15
15
  "id": {
16
- "description": "The check's stable identity within its producer. The same check must keep the same id in every layer, or layers cannot replace one another",
17
16
  "type": "string",
18
- "minLength": 1
17
+ "minLength": 1,
18
+ "description": "The check's stable identity within its producer. The same check must keep the same id in every layer, or layers cannot replace one another"
19
19
  },
20
20
  "title": {
21
21
  "type": "string"
@@ -30,8 +30,8 @@
30
30
  ]
31
31
  },
32
32
  "detail": {
33
- "description": "Why it failed, or what was measured",
34
33
  "default": "",
34
+ "description": "Why it failed, or what was measured",
35
35
  "type": "string"
36
36
  },
37
37
  "specPath": {
@@ -49,8 +49,8 @@
49
49
  "minLength": 1
50
50
  },
51
51
  "value": {
52
- "description": "A finite number",
53
- "type": "number"
52
+ "type": "number",
53
+ "description": "A finite number"
54
54
  },
55
55
  "unit": {
56
56
  "type": "string"
@@ -215,9 +215,9 @@
215
215
  "type": "object",
216
216
  "properties": {
217
217
  "type": {
218
- "description": "An open vocabulary: note, warning, deprecation, issue, accessibility, coverage…",
219
218
  "type": "string",
220
- "minLength": 1
219
+ "minLength": 1,
220
+ "description": "An open vocabulary: note, warning, deprecation, issue, accessibility, coverage…"
221
221
  },
222
222
  "message": {
223
223
  "type": "string"
@@ -443,9 +443,9 @@
443
443
  "type": "object",
444
444
  "properties": {
445
445
  "type": {
446
- "description": "An open vocabulary: note, warning, deprecation, issue, accessibility, coverage…",
447
446
  "type": "string",
448
- "minLength": 1
447
+ "minLength": 1,
448
+ "description": "An open vocabulary: note, warning, deprecation, issue, accessibility, coverage…"
449
449
  },
450
450
  "message": {
451
451
  "type": "string"
@@ -708,9 +708,9 @@
708
708
  "type": "object",
709
709
  "properties": {
710
710
  "type": {
711
- "description": "An open vocabulary: note, warning, deprecation, issue, accessibility, coverage…",
712
711
  "type": "string",
713
- "minLength": 1
712
+ "minLength": 1,
713
+ "description": "An open vocabulary: note, warning, deprecation, issue, accessibility, coverage…"
714
714
  },
715
715
  "message": {
716
716
  "type": "string"
@@ -908,9 +908,9 @@
908
908
  "type": "object",
909
909
  "properties": {
910
910
  "type": {
911
- "description": "An open vocabulary: note, warning, deprecation, issue, accessibility, coverage…",
912
911
  "type": "string",
913
- "minLength": 1
912
+ "minLength": 1,
913
+ "description": "An open vocabulary: note, warning, deprecation, issue, accessibility, coverage…"
914
914
  },
915
915
  "message": {
916
916
  "type": "string"
@@ -1016,10 +1016,10 @@
1016
1016
  "additionalProperties": {}
1017
1017
  },
1018
1018
  "run": {
1019
- "description": "What the run as a whole carries: its logs, links, labels, annotations and extensions",
1020
1019
  "type": "object",
1021
1020
  "properties": {},
1022
- "additionalProperties": {}
1021
+ "additionalProperties": {},
1022
+ "description": "What the run as a whole carries: its logs, links, labels, annotations and extensions"
1023
1023
  }
1024
1024
  },
1025
1025
  "required": [
@@ -8,8 +8,8 @@
8
8
  "const": true
9
9
  },
10
10
  "id": {
11
- "description": "The layer id",
12
- "type": "string"
11
+ "type": "string",
12
+ "description": "The layer id"
13
13
  },
14
14
  "checks": {
15
15
  "type": "integer",
@@ -11,13 +11,13 @@
11
11
  "type": "object",
12
12
  "properties": {
13
13
  "message": {
14
- "description": "What is wrong, and usually what to do about it",
15
- "type": "string"
14
+ "type": "string",
15
+ "description": "What is wrong, and usually what to do about it"
16
16
  },
17
17
  "code": {
18
- "description": "`usage`: the command or its inputs were wrong. Nothing was written",
19
18
  "type": "string",
20
- "const": "usage"
19
+ "const": "usage",
20
+ "description": "`usage`: the command or its inputs were wrong. Nothing was written"
21
21
  }
22
22
  },
23
23
  "required": [
@@ -4,8 +4,8 @@
4
4
  "type": "object",
5
5
  "properties": {
6
6
  "ok": {
7
- "description": "True when every check of every store passed (otherwise the exit code is 1)",
8
- "type": "boolean"
7
+ "type": "boolean",
8
+ "description": "True when every check of every store passed (otherwise the exit code is 1)"
9
9
  },
10
10
  "stores": {
11
11
  "type": "array",
@@ -21,8 +21,8 @@
21
21
  "type": "object",
22
22
  "properties": {
23
23
  "name": {
24
- "description": "`store`, `list`, `contents`, `write`, …",
25
- "type": "string"
24
+ "type": "string",
25
+ "description": "`store`, `list`, `contents`, `write`, …"
26
26
  },
27
27
  "ok": {
28
28
  "type": "boolean"