@crvouga/atlas 0.9.0 → 0.10.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 (53) hide show
  1. package/CHANGELOG.md +6 -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/server/src/app.js +82 -4
  16. package/dist/server/src/llms.js +14 -0
  17. package/dist/server/src/openapi.d.ts +1 -1
  18. package/dist/server/src/openapi.js +60 -3
  19. package/docs/agents.md +1 -0
  20. package/docs/integrating.md +1 -0
  21. package/docs/reference/http-api.md +134 -2
  22. package/docs/reference/playwright-reporter.md +332 -0
  23. package/docs/reference/reports.md +632 -0
  24. package/docs/reference/schema.md +57 -0
  25. package/docs/rich-evidence.md +198 -0
  26. package/docs/server.md +41 -0
  27. package/llms-full.txt +378 -2
  28. package/llms.txt +2 -0
  29. package/package.json +6 -1
  30. package/packages/schema/schemas/report-run.schema.json +251 -0
  31. package/packages/schema/schemas/report-runs.schema.json +177 -0
  32. package/packages/schema/schemas/test-catalog.schema.json +1177 -0
  33. package/packages/schema/schemas/test-history.schema.json +696 -0
  34. package/packages/server/openapi.json +8676 -5351
  35. package/packages/visualizer/src/app/AppShell.tsx +7 -3
  36. package/packages/visualizer/src/app/router.tsx +26 -3
  37. package/packages/visualizer/src/components/Header.tsx +2 -2
  38. package/packages/visualizer/src/evidence/evidence-context.tsx +10 -1
  39. package/packages/visualizer/src/explorer/Explorer.tsx +433 -0
  40. package/packages/visualizer/src/explorer/ExplorerIndex.tsx +120 -0
  41. package/packages/visualizer/src/explorer/RunsView.tsx +164 -0
  42. package/packages/visualizer/src/explorer/TestDetail.tsx +214 -0
  43. package/packages/visualizer/src/explorer/catalog.tsx +274 -0
  44. package/packages/visualizer/src/explorer/explorer.module.css +1238 -0
  45. package/packages/visualizer/src/explorer/groups.test.ts +87 -0
  46. package/packages/visualizer/src/explorer/groups.ts +126 -0
  47. package/packages/visualizer/src/explorer/navigate.ts +36 -0
  48. package/packages/visualizer/src/explorer/parts.tsx +59 -0
  49. package/packages/visualizer/src/explorer/search.ts +97 -0
  50. package/packages/visualizer/src/find/Finder.tsx +2 -0
  51. package/packages/visualizer/src/find/corpus.ts +13 -6
  52. package/packages/visualizer/src/find/rank.ts +2 -0
  53. package/packages/visualizer/src/evidence/project-home.tsx +0 -45
@@ -0,0 +1,386 @@
1
+ /**
2
+ * @module Test catalog, history and runs
3
+ *
4
+ * What a person or an agent asks of a report once it holds more than a handful of checks: every test with where it ran,
5
+ * how it went over time and how long it takes; the specs, domains, environments and producers they fall under; the CI
6
+ * runs that produced them and which tests each one ran.
7
+ *
8
+ * - `TestHistory` keeps every recorded result of every test (bounded per test) and every run's results, folded in one
9
+ * layer at a time, in any order, with the same outcome. It never decides a status: the current status of a test is
10
+ * always the report's winner (`combineEvidence`), and history only describes how it got there.
11
+ * - `testCatalog` turns the report's winners (plus a history, when there is one) into a `TestCatalog`: a summary with
12
+ * insights (slowest, flaky, failing), facets to filter by, and the tests that match a `TestQuery`.
13
+ * - `testHistory`, `runList` and `runDetail` answer for one test, every run, and one run.
14
+ * - `catalogMarkdown`, `historyMarkdown`, `runsMarkdown` and `runMarkdown` say the same in Markdown, for an agent or a
15
+ * terminal.
16
+ *
17
+ * A test is a check identity (its spec path and id) regardless of producer, environment and parameters; each of those
18
+ * combinations is one of its results. A result that is not `passed` is never counted as a pass, and a test with no
19
+ * completed result has no pass rate.
20
+ *
21
+ * @see docs/server.md#reading-the-report-as-an-agent
22
+ */
23
+ import type { EvidenceStatus, SpecEntry } from './explorer-types.js';
24
+ import type { EvidenceSummary, LayerEvidence, LayerHeader, LayerRow, StatusCounts } from './evidence-types.js';
25
+ import { RecordedSpecs } from './recorded-specs.js';
26
+ /** The version of the documents this module writes (`TestCatalog`, `TestHistoryDocument`, `RunList`, `RunDetail`). Only optional fields are added within it. */
27
+ export declare const TEST_CATALOG_VERSION = 1;
28
+ /** Recorded results kept per test, newest first. Older ones are counted in no statistic. */
29
+ export declare const HISTORY_PER_TEST = 100;
30
+ /** One recorded result of a test: when, where, how it went and how long it took, and the layer and run it came from. */
31
+ export type TestResultEntry = {
32
+ /** When it was recorded, epoch ms. */
33
+ readonly at: number;
34
+ readonly status: EvidenceStatus;
35
+ /** How long it took, ms, or null when the producer did not measure it. */
36
+ readonly durationMs: number | null;
37
+ /** The client it checked (`desktop-web`, `api`, `ios`…), or null. */
38
+ readonly environment: string | null;
39
+ readonly producer: string;
40
+ /** The kind of the part that held it (`checks`, `behavior`, `performance`…). */
41
+ readonly kind: string;
42
+ /** Other choices that made it its own result (a device size, a locale…). */
43
+ readonly parameters: Readonly<Record<string, string>>;
44
+ readonly branch: string | null;
45
+ readonly sha: string | null;
46
+ readonly runUrl: string | null;
47
+ /** The key of the run it belongs to (see `runKey`). */
48
+ readonly run: string;
49
+ /** The id of the layer that recorded it. */
50
+ readonly layer: string;
51
+ readonly backend: string;
52
+ /** Why it has its status: the first 500 characters of the check's detail. */
53
+ readonly detail: string;
54
+ /** What it carried beyond its outcome (attachments, steps, retries, tags, labels), when anything. */
55
+ readonly evidence?: EvidenceSummary;
56
+ };
57
+ /** The current result of a test in one producer, environment and parameter set: the report's winner for that slot. */
58
+ export type TestOutcome = TestResultEntry;
59
+ /** Durations of a set of results, ms. Only results that recorded one are counted. */
60
+ export type DurationStats = {
61
+ readonly count: number;
62
+ readonly totalMs: number;
63
+ readonly avgMs: number;
64
+ readonly p50Ms: number;
65
+ readonly p95Ms: number;
66
+ readonly minMs: number;
67
+ readonly maxMs: number;
68
+ /** The newest recorded duration. */
69
+ readonly lastMs: number;
70
+ };
71
+ /** How a test has gone across its recorded history (at most `HISTORY_PER_TEST` results). */
72
+ export type TestStats = {
73
+ /** Recorded results: passed, failed and not-run (progress reported as `running` is not history). */
74
+ readonly results: number;
75
+ readonly passed: number;
76
+ readonly failed: number;
77
+ /** passed / (passed + failed), or null with no completed result. */
78
+ readonly passRate: number | null;
79
+ /** True when the same code both passed and failed, a retry was needed to pass, or the producer said so. */
80
+ readonly flaky: boolean;
81
+ /** Why it is flaky, for a person; empty when it is not. */
82
+ readonly flakyReason: string;
83
+ /** How many times the completed outcome changed, per result, newest history first. */
84
+ readonly flips: number;
85
+ readonly durations: DurationStats | null;
86
+ readonly firstSeenMs: number | null;
87
+ readonly lastSeenMs: number | null;
88
+ readonly lastPassedAtMs: number | null;
89
+ readonly lastFailedAtMs: number | null;
90
+ /** Distinct runs it was recorded in. */
91
+ readonly runs: number;
92
+ /** Branches it was recorded on, most recent first, at most ten. */
93
+ readonly branches: readonly string[];
94
+ };
95
+ /** A spec as the catalog shows it: recorded by a layer or published in a spec set, or derived from the path alone. */
96
+ export type CatalogSpec = {
97
+ readonly path: string;
98
+ readonly title: string;
99
+ /** The group it belongs to: the spec's own domain, else the folder under `specs/`. */
100
+ readonly domain: string;
101
+ readonly kind: string;
102
+ };
103
+ /** One test: what it is, its current status per environment, and how it has gone. */
104
+ export type TestSummary = {
105
+ /** The check identity, `JSON.stringify([specPath, id])`. Stable; use it to ask for the test's history. */
106
+ readonly key: string;
107
+ readonly id: string;
108
+ readonly title: string;
109
+ readonly specPath: string | null;
110
+ readonly spec: CatalogSpec | null;
111
+ /** The worst current outcome across its results (failed, then running, then not-run, then passed). */
112
+ readonly status: EvidenceStatus;
113
+ readonly environments: readonly string[];
114
+ readonly producers: readonly string[];
115
+ /** The current result per producer, environment and parameter set, newest first. */
116
+ readonly outcomes: readonly TestOutcome[];
117
+ readonly stats: TestStats;
118
+ readonly tags: readonly string[];
119
+ readonly labels: Readonly<Record<string, string>>;
120
+ /** What its current results carry: attachments, steps, retries, and the kinds of attachment. */
121
+ readonly evidence: {
122
+ readonly attachments: number;
123
+ readonly steps: number;
124
+ readonly attempts: number;
125
+ readonly roles: readonly string[];
126
+ };
127
+ /** The newest time any of its results was recorded. */
128
+ readonly lastRecordedAtMs: number;
129
+ /** True while one of its results comes from a run still in progress. */
130
+ readonly live: boolean;
131
+ };
132
+ /** A test named in an insight list, with the number that put it there. */
133
+ export type TestRef = {
134
+ readonly key: string;
135
+ readonly title: string;
136
+ readonly specPath: string | null;
137
+ readonly status: EvidenceStatus;
138
+ /** Average duration (ms) for `slowest`, flips for `flaky`, failures for `failing`. */
139
+ readonly value: number;
140
+ };
141
+ /** A group in the summary (an environment, a producer, a domain, a spec): how many tests, by status (for an environment, by how they went on it), and their timing. */
142
+ export type CatalogGroup = {
143
+ readonly name: string;
144
+ readonly tests: number;
145
+ readonly counts: StatusCounts;
146
+ /** Average of its tests' average durations, ms, or null when none was measured. */
147
+ readonly avgMs: number | null;
148
+ };
149
+ /** The report at a glance: counts, timing and insights over every test (not only those a query matched). */
150
+ export type CatalogSummary = {
151
+ readonly tests: number;
152
+ /** Tests by their current status. */
153
+ readonly counts: StatusCounts;
154
+ /** Current results (a test checked on two environments has two). */
155
+ readonly outcomes: number;
156
+ readonly flaky: number;
157
+ /** Over each test's average duration. */
158
+ readonly durations: DurationStats | null;
159
+ /** Tests with at least one measured duration. */
160
+ readonly measured: number;
161
+ readonly withAttachments: number;
162
+ readonly environments: readonly CatalogGroup[];
163
+ readonly producers: readonly CatalogGroup[];
164
+ readonly domains: readonly CatalogGroup[];
165
+ readonly slowest: readonly TestRef[];
166
+ readonly flakiest: readonly TestRef[];
167
+ readonly failing: readonly TestRef[];
168
+ /** Runs the history knows of. */
169
+ readonly runs: number;
170
+ readonly lastRecordedAtMs: number | null;
171
+ /** True when statistics come from recorded history, false when only the current results were available. */
172
+ readonly history: boolean;
173
+ };
174
+ /** A value to filter by, and how many tests have it. */
175
+ export type FacetValue = {
176
+ readonly value: string;
177
+ readonly count: number;
178
+ };
179
+ /** Everything the tests can be filtered and grouped by, with counts over every test. */
180
+ export type CatalogFacets = {
181
+ readonly statuses: readonly FacetValue[];
182
+ readonly environments: readonly FacetValue[];
183
+ readonly producers: readonly FacetValue[];
184
+ readonly domains: readonly FacetValue[];
185
+ readonly specs: readonly (CatalogSpec & {
186
+ readonly tests: number;
187
+ readonly counts: StatusCounts;
188
+ })[];
189
+ readonly tags: readonly FacetValue[];
190
+ readonly branches: readonly FacetValue[];
191
+ readonly labels: Readonly<Record<string, readonly FacetValue[]>>;
192
+ };
193
+ /** How tests are ordered. */
194
+ export declare const TEST_SORTS: readonly ["spec", "title", "recent", "slowest", "failures", "flaky", "runs"];
195
+ /** One of `TEST_SORTS`. `spec` (the default) orders by domain, spec and title. */
196
+ export type TestSort = (typeof TEST_SORTS)[number];
197
+ /** Which tests to list. Every field is optional; several values of one field match any of them, and fields combine. */
198
+ export type TestQuery = {
199
+ /** Words that must all appear in the title, id, spec, domain, tags, labels, environments or producers. Case does not matter. */
200
+ readonly q?: string;
201
+ /** Current status, or `flaky`. */
202
+ readonly status?: readonly string[];
203
+ readonly environment?: readonly string[];
204
+ readonly producer?: readonly string[];
205
+ readonly domain?: readonly string[];
206
+ /** Spec paths (`specs/...`). */
207
+ readonly spec?: readonly string[];
208
+ readonly tag?: readonly string[];
209
+ /** Only tests recorded in this run (a key from `runList`). */
210
+ readonly run?: string;
211
+ readonly sort?: TestSort;
212
+ readonly offset?: number;
213
+ /** At most this many tests (default 500, at most 20000). */
214
+ readonly limit?: number;
215
+ };
216
+ /** `GET /api/reports/tests`: the summary, the facets and the tests that match a query. */
217
+ export type TestCatalog = {
218
+ readonly schemaVersion: typeof TEST_CATALOG_VERSION;
219
+ /** Changes whenever the report or its history does. */
220
+ readonly version: string;
221
+ readonly generatedAtMs: number;
222
+ readonly summary: CatalogSummary;
223
+ readonly facets: CatalogFacets;
224
+ /** Tests that matched, before `offset` and `limit`. */
225
+ readonly total: number;
226
+ readonly offset: number;
227
+ readonly limit: number;
228
+ readonly tests: readonly TestSummary[];
229
+ };
230
+ /** `GET /api/reports/tests/history`: one test and every result recorded for it, newest first. */
231
+ export type TestHistoryDocument = {
232
+ readonly schemaVersion: typeof TEST_CATALOG_VERSION;
233
+ readonly version: string;
234
+ readonly test: TestSummary;
235
+ readonly history: readonly TestResultEntry[];
236
+ };
237
+ /** A CI run (or any group of layers sharing a run URL, else a branch and commit): when, where, and how its tests went. */
238
+ export type RunSummary = {
239
+ /** Stable key: the run URL, or `branch@sha`. */
240
+ readonly key: string;
241
+ readonly runUrl: string | null;
242
+ readonly branch: string | null;
243
+ readonly sha: string | null;
244
+ readonly startedAtMs: number;
245
+ readonly endedAtMs: number;
246
+ /** From its first to its last recorded result, ms. */
247
+ readonly spanMs: number;
248
+ readonly producers: readonly string[];
249
+ readonly environments: readonly string[];
250
+ readonly layers: number;
251
+ /** Distinct tests it recorded. */
252
+ readonly tests: number;
253
+ /** Its results by status (its last word on each test, environment and producer). */
254
+ readonly counts: StatusCounts;
255
+ /** Sum of its results' durations, ms, when any was measured. */
256
+ readonly totalDurationMs: number | null;
257
+ /** Up to twenty tests that failed in it. */
258
+ readonly failing: readonly {
259
+ readonly key: string;
260
+ readonly title: string;
261
+ readonly environment: string | null;
262
+ readonly detail: string;
263
+ }[];
264
+ };
265
+ /** `GET /api/reports/runs`: runs, newest first. */
266
+ export type RunList = {
267
+ readonly schemaVersion: typeof TEST_CATALOG_VERSION;
268
+ readonly version: string;
269
+ readonly total: number;
270
+ readonly runs: readonly RunSummary[];
271
+ };
272
+ /** One test's last result within one run. */
273
+ export type RunResult = {
274
+ readonly key: string;
275
+ readonly id: string;
276
+ readonly title: string;
277
+ readonly specPath: string | null;
278
+ readonly environment: string | null;
279
+ readonly producer: string;
280
+ readonly status: EvidenceStatus;
281
+ readonly durationMs: number | null;
282
+ readonly at: number;
283
+ readonly layer: string;
284
+ readonly detail: string;
285
+ };
286
+ /** `GET /api/reports/runs/detail`: one run and every result it recorded. */
287
+ export type RunDetail = {
288
+ readonly schemaVersion: typeof TEST_CATALOG_VERSION;
289
+ readonly version: string;
290
+ readonly run: RunSummary;
291
+ readonly results: readonly RunResult[];
292
+ };
293
+ /** The run a layer belongs to: its run URL, else `branch@sha`, else `local`. Layers with the same key are one run. */
294
+ export declare function runKey(layer: Pick<LayerHeader, 'runUrl' | 'branch' | 'sha'>): string;
295
+ /**
296
+ * Every recorded result of every test (the newest `HISTORY_PER_TEST` per test, leaving out `running` progress) and the
297
+ * runs they came from. A run's results are read from that history, so memory follows the number of tests and not the age
298
+ * of the project: a run older than a test's last hundred results no longer lists that test. Add each layer once with
299
+ * `add`; the order layers arrive in does not change what it holds. It never decides a status.
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * const history = new TestHistory();
304
+ * for (const { layer, rows } of layers) history.add(layer, rows, specs, layer.backend);
305
+ * const catalog = testCatalog(catalogModel(layers, { history }));
306
+ * ```
307
+ */
308
+ export declare class TestHistory {
309
+ #private;
310
+ /** Changes each time a layer is added. */
311
+ get rev(): number;
312
+ /** The specs the added layers recorded. */
313
+ get specs(): RecordedSpecs;
314
+ /** How many layers were added. */
315
+ get layers(): number;
316
+ /** Takes in one layer: its header, its rows, the specs it recorded and the backend it was read from. A layer id already added is ignored. */
317
+ add(layer: LayerHeader, rows: readonly LayerRow[], specs?: readonly SpecEntry[], backend?: string): void;
318
+ /** The recorded results of one test (by `TestSummary.key`), newest first. Empty when none. */
319
+ of(key: string): readonly TestResultEntry[];
320
+ /** How many runs it knows of. */
321
+ get runCount(): number;
322
+ /** Every run, newest first. */
323
+ runs(): RunSummary[];
324
+ /** One run and its results (failed first, then by spec and title), or null. */
325
+ run(key: string): {
326
+ readonly run: RunSummary;
327
+ readonly results: RunResult[];
328
+ } | null;
329
+ /** The keys of the tests one run recorded. */
330
+ testsIn(key: string): ReadonlySet<string>;
331
+ }
332
+ /** Durations of a list of numbers (ms), or null when empty. `lastMs` is the first value, so give them newest first. */
333
+ export declare function durationStats(values: readonly number[]): DurationStats | null;
334
+ /** The spec a path names: the recorded one, or one derived from the path (`specs/<domain>/<name>.<ext>`). */
335
+ export declare function catalogSpec(path: string | null, known: (path: string) => SpecEntry | undefined): CatalogSpec | null;
336
+ /** Where the catalog's statistics and specs come from. */
337
+ export type CatalogSources = {
338
+ /** Recorded results; without it, statistics describe only the current results. */
339
+ readonly history?: TestHistory | null;
340
+ /** Specs by path, published or recorded. Defaults to those the history recorded. */
341
+ readonly specs?: readonly SpecEntry[];
342
+ /** The report's version, echoed in the documents. */
343
+ readonly version?: string;
344
+ readonly now?: number;
345
+ };
346
+ /** The tests of a report, built once and queried many times: what `testCatalog` and `testHistory` read from. */
347
+ export type CatalogModel = {
348
+ readonly version: string;
349
+ readonly generatedAtMs: number;
350
+ readonly tests: readonly TestSummary[];
351
+ readonly byKey: ReadonlyMap<string, TestSummary>;
352
+ readonly summary: CatalogSummary;
353
+ readonly facets: CatalogFacets;
354
+ readonly haystacks: ReadonlyMap<string, string>;
355
+ readonly history: TestHistory | null;
356
+ };
357
+ /** The lowercase text a search matches a test against: title, id, spec, domain, tags, labels, environments and producers. */
358
+ export declare function haystackOf(test: TestSummary): string;
359
+ /**
360
+ * Builds the tests of a report from its winners (`layers`, as the effective report holds them: each layer with the rows
361
+ * that win) and, when given, a history. The model is what `testCatalog` and `testHistory` query; build it once per
362
+ * version and reuse it.
363
+ */
364
+ export declare function catalogModel(layers: readonly LayerEvidence[], sources?: CatalogSources): CatalogModel;
365
+ /** Whether one test matches a query (everything but sorting and paging). */
366
+ export declare function matchesQuery(model: CatalogModel, test: TestSummary, query: TestQuery, inRun?: ReadonlySet<string> | null): boolean;
367
+ /** The catalog document for a query: the model's summary and facets, and the matching tests, sorted and paged. */
368
+ export declare function testCatalog(model: CatalogModel, query?: TestQuery): TestCatalog;
369
+ /** One test and its recorded results, or null when the report has no test with that key. */
370
+ export declare function testHistory(model: CatalogModel, key: string): TestHistoryDocument | null;
371
+ /** Every run the history knows of, newest first, `limit` at most (default 100). */
372
+ export declare function runList(history: TestHistory, version: string, limit?: number): RunList;
373
+ /** One run with every result it recorded, or null when no run has that key. */
374
+ export declare function runDetail(history: TestHistory, version: string, key: string): RunDetail | null;
375
+ /** Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`. */
376
+ export declare function parseTestQuery(params: URLSearchParams): TestQuery;
377
+ /** A duration for a person: `850 ms`, `12.3 s`, `4m 05s`. */
378
+ export declare function formatMs(ms: number | null | undefined): string;
379
+ /** The catalog as Markdown: the summary, insights, groups and the listed tests as a table. */
380
+ export declare function catalogMarkdown(catalog: TestCatalog): string;
381
+ /** One test's history as Markdown. */
382
+ export declare function historyMarkdown(document: TestHistoryDocument): string;
383
+ /** The run list as Markdown. */
384
+ export declare function runsMarkdown(list: RunList): string;
385
+ /** One run as Markdown. */
386
+ export declare function runMarkdown(detail: RunDetail): string;