@crvouga/atlas 1.7.0 → 1.9.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 (46) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/reports/src/evidence-detail.js +6 -1
  3. package/dist/reports/src/evidence-types.d.ts +2 -0
  4. package/dist/reports/src/explorer-types.d.ts +19 -0
  5. package/dist/reports/src/latency.d.ts +204 -0
  6. package/dist/reports/src/latency.js +219 -0
  7. package/dist/reports/src/reliability.d.ts +181 -0
  8. package/dist/reports/src/reliability.js +294 -0
  9. package/dist/reports/src/report-index.js +1 -1
  10. package/dist/reports/src/reports-explorer-route.d.ts +19 -6
  11. package/dist/reports/src/reports-route.d.ts +19 -6
  12. package/dist/reports/src/test-catalog.d.ts +11 -3
  13. package/dist/reports/src/test-catalog.js +12 -5
  14. package/dist/schema/src/api.d.ts +377 -0
  15. package/dist/schema/src/api.js +55 -3
  16. package/dist/schema/src/layer.d.ts +90 -0
  17. package/dist/schema/src/layer.js +20 -1
  18. package/dist/server/src/app.js +15 -0
  19. package/dist/server/src/llms.js +1 -0
  20. package/dist/server/src/openapi.js +25 -2
  21. package/docs/extending.md +1 -1
  22. package/docs/reference/http-api.md +48 -0
  23. package/docs/reference/reports.md +619 -1
  24. package/docs/reference/schema.md +39 -0
  25. package/docs/rich-evidence.md +43 -0
  26. package/docs/server.md +18 -0
  27. package/docs/ui.md +20 -1
  28. package/llms-full.txt +130 -2
  29. package/package.json +1 -1
  30. package/packages/schema/schemas/check-detail.schema.json +56 -1
  31. package/packages/schema/schemas/effective-delta.schema.json +86 -2
  32. package/packages/schema/schemas/effective-evidence.schema.json +86 -2
  33. package/packages/schema/schemas/evidence-page.schema.json +13 -0
  34. package/packages/schema/schemas/layer-rows.schema.json +73 -2
  35. package/packages/schema/schemas/layer.schema.json +112 -2
  36. package/packages/schema/schemas/publish-checks.schema.json +56 -1
  37. package/packages/schema/schemas/test-catalog.schema.json +102 -0
  38. package/packages/schema/schemas/test-history.schema.json +204 -0
  39. package/packages/schema/schemas/test-reliability.schema.json +629 -0
  40. package/packages/server/openapi.json +2354 -661
  41. package/packages/visualizer/src/evidence/check-detail.tsx +9 -0
  42. package/packages/visualizer/src/explorer/ProductLatency.tsx +214 -0
  43. package/packages/visualizer/src/explorer/Reliability.tsx +337 -0
  44. package/packages/visualizer/src/explorer/TestDetail.tsx +12 -2
  45. package/packages/visualizer/src/explorer/catalog.tsx +1 -1
  46. package/packages/visualizer/src/explorer/search.ts +36 -0
@@ -14,7 +14,7 @@
14
14
  * @see docs/server.md
15
15
  */
16
16
  import { z } from 'zod/v4';
17
- import { CheckSchema, EvidenceScopeSchema, LayerRunSchema } from './layer.js';
17
+ import { CheckSchema, EvidenceScopeSchema, LayerRunSchema, MeasurementSchema } from './layer.js';
18
18
  /** One of the statuses a check has in a report: `passed`, `failed`, `running` (not finished) or `not-run`. Closed. */
19
19
  export const EvidenceStatusSchema = z.enum(['passed', 'failed', 'running', 'not-run']);
20
20
  /**
@@ -85,6 +85,7 @@ export const EvidenceSummarySchema = z.looseObject({
85
85
  steps: z.number().int().nonnegative(),
86
86
  annotations: z.number().int().nonnegative(),
87
87
  attempts: z.number().int().nonnegative(),
88
+ attemptStatuses: z.array(EvidenceStatusSchema).optional().describe('The status of each recorded try, in the order they were made. Absent when no try was recorded, and in reports written before this was recorded: a first try is then unknown, never assumed to have passed'),
88
89
  links: z.number().int().nonnegative(),
89
90
  tags: z.array(z.string()),
90
91
  artifactTags: z.array(z.string()).optional().describe('Normalized tags of the attachments themselves, sorted and distinct. Absent when no attachment is tagged; never inherited from a check or run'),
@@ -124,7 +125,7 @@ export const LayerRowSchema = z.looseObject({
124
125
  status: EvidenceStatusSchema,
125
126
  specPath: z.string().nullable().describe('The spec the check belongs to, `specs/<path>`, or null'),
126
127
  detail: z.string().describe('Why it has this status; empty when there is nothing to add'),
127
- measurements: z.array(z.looseObject({ name: z.string(), value: z.number(), unit: z.string(), limit: z.number().optional() })),
128
+ measurements: z.array(MeasurementSchema),
128
129
  testId: z.string().nullable(),
129
130
  variant: z.record(z.string(), z.string()),
130
131
  durationMs: z.number().nullable(),
@@ -270,7 +271,9 @@ export const TestResultEntrySchema = z.looseObject({
270
271
  layer: z.string().describe('The id of the layer that recorded it'),
271
272
  backend: z.string(),
272
273
  detail: z.string().describe('Why it has its status, at most 500 characters'),
273
- evidence: EvidenceSummarySchema.optional()
274
+ specRevision: z.string().optional().describe('The fingerprint of its spec text that the layer recorded, when it recorded one. Two results with different revisions were recorded against different promises. Absent: not recorded, never assumed to match'),
275
+ evidence: EvidenceSummarySchema.optional(),
276
+ latency: z.array(MeasurementSchema).optional().describe('Its product-latency measurements (`kind: product-latency`), when it recorded any. Separate from `durationMs`, which is how long the test took')
274
277
  });
275
278
  const DurationStatsSchema = z.looseObject({
276
279
  count: z.number().int().nonnegative(), totalMs: z.number(), avgMs: z.number(), p50Ms: z.number(), p95Ms: z.number(), minMs: z.number(), maxMs: z.number(), lastMs: z.number()
@@ -389,6 +392,55 @@ export const TestHistorySchema = z.looseObject({
389
392
  test: TestSummarySchema,
390
393
  history: z.array(TestResultEntrySchema)
391
394
  });
395
+ const ReliabilityRateSchema = z.looseObject({
396
+ executions: z.number().int().nonnegative().describe('Independent executions that passed or failed'),
397
+ passed: z.number().int().nonnegative(),
398
+ failed: z.number().int().nonnegative(),
399
+ rate: z.number().nullable().describe('passed / executions, or null with none')
400
+ });
401
+ /**
402
+ * `GET /api/reports/tests/reliability`: how reliable one test is inside a scope, and exactly how that figure was counted.
403
+ * The snapshots one run recorded for one producer, environment and parameter set are one independent execution; only
404
+ * `passed` and `failed` executions are trials; `notes` say when the sample is too small, pooled, selectively rerun or
405
+ * bounded. It reads results and never changes a status. It is not a production failure probability.
406
+ */
407
+ export const TestReliabilitySchema = z.looseObject({
408
+ schemaVersion: z.literal(1),
409
+ key: z.string().describe('The test\'s key'),
410
+ scope: z.looseObject({
411
+ environment: z.array(z.string()).optional(), producer: z.array(z.string()).optional(), parameter: z.array(z.string()).optional(),
412
+ branch: z.array(z.string()).optional(), sha: z.array(z.string()).optional(), specRevision: z.array(z.string()).optional(),
413
+ revision: z.enum(['current', 'any']).optional(), since: z.number().optional(), until: z.number().optional()
414
+ }).describe('What was asked: the selection the figures are about'),
415
+ minimumExecutions: z.number().int().positive().describe('Completed executions below which the sample is `insufficient`'),
416
+ sample: z.enum(['none', 'insufficient', 'sufficient']).describe('A count only: it says nothing about whether the executions are representative; read `notes`'),
417
+ eventual: ReliabilityRateSchema.describe('Executions that passed in the end, over those that passed or failed'),
418
+ interval: z.looseObject({ method: z.literal('wilson'), level: z.number(), lower: z.number(), upper: z.number(), assumptions: z.string() }).nullable().describe('An interval estimate with the assumptions it needs; null without an execution'),
419
+ firstAttempt: ReliabilityRateSchema.describe('Over the executions that recorded their tries: those whose first try passed'),
420
+ unrecordedAttempts: z.number().int().nonnegative().describe('Counted executions that recorded no tries: their first try is unknown, never assumed to have passed'),
421
+ attempts: z.looseObject({ total: z.number().int().nonnegative(), retried: z.number().int().nonnegative(), recovered: z.number().int().nonnegative() }),
422
+ currentRevision: z.string().nullable().describe('The spec revision `revision=current` counted, or null'),
423
+ historical: z.looseObject({
424
+ executions: z.number().int().nonnegative(), passed: z.number().int().nonnegative(), failed: z.number().int().nonnegative(),
425
+ revisions: z.array(z.looseObject({ specRevision: z.string(), executions: z.number().int().nonnegative() }))
426
+ }).describe('Earlier spec revisions: context only, counted nowhere above'),
427
+ excluded: z.record(z.string(), z.number().int().nonnegative()).describe('Executions in the scope that are not trials, by reason: `not-run`, `unfinished`, `unknown-status`, `historical-revision`, `revision-not-recorded`'),
428
+ outsideScope: z.number().int().nonnegative().describe('Executions the scope did not select'),
429
+ snapshots: z.number().int().nonnegative().describe('Raw recorded snapshots behind the counted executions'),
430
+ cohorts: z.array(ReliabilityRateSchema.extend({ environment: z.string().nullable(), producer: z.string(), parameters: z.record(z.string(), z.string()) })),
431
+ available: z.looseObject({
432
+ environments: z.array(FacetValueSchema), producers: z.array(FacetValueSchema), branches: z.array(FacetValueSchema), specRevisions: z.array(FacetValueSchema)
433
+ }).describe('What a scope could choose among, with independent executions'),
434
+ notes: z.array(z.looseObject({ code: z.string().describe('An open set: treat a code you do not know as informational'), severity: z.enum(['warning', 'info']), message: z.string() })),
435
+ executions: z.array(z.looseObject({
436
+ run: z.string(), at: z.number(), status: z.string(), counted: z.boolean(), excluded: z.string().optional(),
437
+ environment: z.string().nullable(), producer: z.string(), kind: z.string(), parameters: z.record(z.string(), z.string()),
438
+ branch: z.string().nullable(), sha: z.string().nullable(), runUrl: z.string().nullable(), layer: z.string(), backend: z.string(),
439
+ durationMs: z.number().nullable(), specRevision: z.string().optional(), snapshots: z.number().int().positive(),
440
+ attempts: z.array(z.string()).optional().describe('The status of each recorded try, in order'), firstAttempt: z.string().optional(),
441
+ republished: z.boolean().describe('A failed snapshot of this execution was followed by a later one')
442
+ })).describe('Every independent execution of the selection, newest first: the denominator, open to inspection')
443
+ });
392
444
  /** A run: the layers that name the same execution (else share a run URL, else a branch and commit), with what they recorded. */
393
445
  export const ReportRunSchema = z.looseObject({
394
446
  key: z.string().describe('`run:<id>` for an execution a producer named, else the run URL, else `branch@sha`. Pass it to `/api/reports/runs/detail?key=` or `/api/reports/tests?run=`'),
@@ -74,16 +74,40 @@ export declare const LayerRunSchema: z.ZodObject<{
74
74
  }, z.core.$loose>;
75
75
  /** The execution of a layer, parsed from `LayerRunSchema`. */
76
76
  export type LayerRun = z.output<typeof LayerRunSchema>;
77
+ /** The `kind` of a measurement that is a named, user-visible product latency (see docs/rich-evidence.md#product-latency). */
78
+ export declare const PRODUCT_LATENCY_KIND = "product-latency";
79
+ /** The two ends of a measured interval, named in the producer's words: `{ from: 'submit pressed', to: 'confirmation visible' }`. */
80
+ export declare const MeasurementBoundarySchema: z.ZodObject<{
81
+ from: z.ZodOptional<z.ZodString>;
82
+ to: z.ZodOptional<z.ZodString>;
83
+ }, z.core.$loose>;
77
84
  /**
78
85
  * A number a check measured, such as a latency or a size, with its unit and an optional allowance. A measurement over
79
86
  * its `limit` is reported and never changes the check's status.
87
+ *
88
+ * A measurement of `kind` `product-latency` is how quickly the product responded to a user, between two named events.
89
+ * It is not the check's duration (which includes setup, waits and pacing) and a number with no `kind` is never read as
90
+ * product latency. All the fields after `limit` are optional and describe the measurement; none decides a status.
80
91
  */
81
92
  export declare const MeasurementSchema: z.ZodObject<{
82
93
  name: z.ZodString;
83
94
  value: z.ZodNumber;
84
95
  unit: z.ZodString;
85
96
  limit: z.ZodOptional<z.ZodNumber>;
97
+ kind: z.ZodOptional<z.ZodString>;
98
+ from: z.ZodOptional<z.ZodString>;
99
+ to: z.ZodOptional<z.ZodString>;
100
+ intended: z.ZodOptional<z.ZodObject<{
101
+ from: z.ZodOptional<z.ZodString>;
102
+ to: z.ZodOptional<z.ZodString>;
103
+ }, z.core.$loose>>;
104
+ source: z.ZodOptional<z.ZodString>;
105
+ outcome: z.ZodOptional<z.ZodString>;
106
+ instrumentation: z.ZodOptional<z.ZodString>;
107
+ labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
86
108
  }, z.core.$loose>;
109
+ /** A measurement as stored in a layer, parsed from `MeasurementSchema`. */
110
+ export type Measurement = z.output<typeof MeasurementSchema>;
87
111
  /**
88
112
  * Structured data of a producer's or tool's own, by namespace: `{ "com.example.coverage": { … } }`.
89
113
  * Use a name you own (a reverse domain, or your tool's name). Readers preserve it and never fail on it.
@@ -867,6 +891,17 @@ export declare const CheckSchema: z.ZodObject<{
867
891
  value: z.ZodNumber;
868
892
  unit: z.ZodString;
869
893
  limit: z.ZodOptional<z.ZodNumber>;
894
+ kind: z.ZodOptional<z.ZodString>;
895
+ from: z.ZodOptional<z.ZodString>;
896
+ to: z.ZodOptional<z.ZodString>;
897
+ intended: z.ZodOptional<z.ZodObject<{
898
+ from: z.ZodOptional<z.ZodString>;
899
+ to: z.ZodOptional<z.ZodString>;
900
+ }, z.core.$loose>>;
901
+ source: z.ZodOptional<z.ZodString>;
902
+ outcome: z.ZodOptional<z.ZodString>;
903
+ instrumentation: z.ZodOptional<z.ZodString>;
904
+ labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
870
905
  }, z.core.$loose>>>;
871
906
  durationMs: z.ZodOptional<z.ZodNumber>;
872
907
  recordingExpectation: z.ZodOptional<z.ZodString>;
@@ -1338,6 +1373,17 @@ export declare const ReportPartSchema: z.ZodObject<{
1338
1373
  value: z.ZodNumber;
1339
1374
  unit: z.ZodString;
1340
1375
  limit: z.ZodOptional<z.ZodNumber>;
1376
+ kind: z.ZodOptional<z.ZodString>;
1377
+ from: z.ZodOptional<z.ZodString>;
1378
+ to: z.ZodOptional<z.ZodString>;
1379
+ intended: z.ZodOptional<z.ZodObject<{
1380
+ from: z.ZodOptional<z.ZodString>;
1381
+ to: z.ZodOptional<z.ZodString>;
1382
+ }, z.core.$loose>>;
1383
+ source: z.ZodOptional<z.ZodString>;
1384
+ outcome: z.ZodOptional<z.ZodString>;
1385
+ instrumentation: z.ZodOptional<z.ZodString>;
1386
+ labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1341
1387
  }, z.core.$loose>>>;
1342
1388
  durationMs: z.ZodOptional<z.ZodNumber>;
1343
1389
  recordingExpectation: z.ZodOptional<z.ZodString>;
@@ -1708,6 +1754,17 @@ export declare const LayerSourceSchema: z.ZodObject<{
1708
1754
  value: z.ZodNumber;
1709
1755
  unit: z.ZodString;
1710
1756
  limit: z.ZodOptional<z.ZodNumber>;
1757
+ kind: z.ZodOptional<z.ZodString>;
1758
+ from: z.ZodOptional<z.ZodString>;
1759
+ to: z.ZodOptional<z.ZodString>;
1760
+ intended: z.ZodOptional<z.ZodObject<{
1761
+ from: z.ZodOptional<z.ZodString>;
1762
+ to: z.ZodOptional<z.ZodString>;
1763
+ }, z.core.$loose>>;
1764
+ source: z.ZodOptional<z.ZodString>;
1765
+ outcome: z.ZodOptional<z.ZodString>;
1766
+ instrumentation: z.ZodOptional<z.ZodString>;
1767
+ labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1711
1768
  }, z.core.$loose>>>;
1712
1769
  durationMs: z.ZodOptional<z.ZodNumber>;
1713
1770
  recordingExpectation: z.ZodOptional<z.ZodString>;
@@ -2324,6 +2381,17 @@ export declare const LayerDocumentSchema: z.ZodObject<{
2324
2381
  value: z.ZodNumber;
2325
2382
  unit: z.ZodString;
2326
2383
  limit: z.ZodOptional<z.ZodNumber>;
2384
+ kind: z.ZodOptional<z.ZodString>;
2385
+ from: z.ZodOptional<z.ZodString>;
2386
+ to: z.ZodOptional<z.ZodString>;
2387
+ intended: z.ZodOptional<z.ZodObject<{
2388
+ from: z.ZodOptional<z.ZodString>;
2389
+ to: z.ZodOptional<z.ZodString>;
2390
+ }, z.core.$loose>>;
2391
+ source: z.ZodOptional<z.ZodString>;
2392
+ outcome: z.ZodOptional<z.ZodString>;
2393
+ instrumentation: z.ZodOptional<z.ZodString>;
2394
+ labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
2327
2395
  }, z.core.$loose>>>;
2328
2396
  durationMs: z.ZodOptional<z.ZodNumber>;
2329
2397
  recordingExpectation: z.ZodOptional<z.ZodString>;
@@ -2693,6 +2761,17 @@ export declare const LayerDocumentSchema: z.ZodObject<{
2693
2761
  value: z.ZodNumber;
2694
2762
  unit: z.ZodString;
2695
2763
  limit: z.ZodOptional<z.ZodNumber>;
2764
+ kind: z.ZodOptional<z.ZodString>;
2765
+ from: z.ZodOptional<z.ZodString>;
2766
+ to: z.ZodOptional<z.ZodString>;
2767
+ intended: z.ZodOptional<z.ZodObject<{
2768
+ from: z.ZodOptional<z.ZodString>;
2769
+ to: z.ZodOptional<z.ZodString>;
2770
+ }, z.core.$loose>>;
2771
+ source: z.ZodOptional<z.ZodString>;
2772
+ outcome: z.ZodOptional<z.ZodString>;
2773
+ instrumentation: z.ZodOptional<z.ZodString>;
2774
+ labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
2696
2775
  }, z.core.$loose>>>;
2697
2776
  durationMs: z.ZodOptional<z.ZodNumber>;
2698
2777
  recordingExpectation: z.ZodOptional<z.ZodString>;
@@ -2901,6 +2980,17 @@ export declare const PublishChecksSchema: z.ZodObject<{
2901
2980
  value: z.ZodNumber;
2902
2981
  unit: z.ZodString;
2903
2982
  limit: z.ZodOptional<z.ZodNumber>;
2983
+ kind: z.ZodOptional<z.ZodString>;
2984
+ from: z.ZodOptional<z.ZodString>;
2985
+ to: z.ZodOptional<z.ZodString>;
2986
+ intended: z.ZodOptional<z.ZodObject<{
2987
+ from: z.ZodOptional<z.ZodString>;
2988
+ to: z.ZodOptional<z.ZodString>;
2989
+ }, z.core.$loose>>;
2990
+ source: z.ZodOptional<z.ZodString>;
2991
+ outcome: z.ZodOptional<z.ZodString>;
2992
+ instrumentation: z.ZodOptional<z.ZodString>;
2993
+ labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
2904
2994
  }, z.core.$loose>>>;
2905
2995
  durationMs: z.ZodOptional<z.ZodNumber>;
2906
2996
  recordingExpectation: z.ZodOptional<z.ZodString>;
@@ -62,15 +62,34 @@ export const LayerRunSchema = z.looseObject({
62
62
  startedAtMs: z.number().nonnegative().optional().describe('When the execution started, epoch milliseconds'),
63
63
  endedAtMs: z.number().nonnegative().optional().describe('When the execution ended, epoch milliseconds')
64
64
  });
65
+ /** The `kind` of a measurement that is a named, user-visible product latency (see docs/rich-evidence.md#product-latency). */
66
+ export const PRODUCT_LATENCY_KIND = 'product-latency';
67
+ /** The two ends of a measured interval, named in the producer's words: `{ from: 'submit pressed', to: 'confirmation visible' }`. */
68
+ export const MeasurementBoundarySchema = z.looseObject({
69
+ from: z.string().min(1).optional().describe('The event the interval starts at: `end of speech`, `submit pressed`'),
70
+ to: z.string().min(1).optional().describe('The event the interval ends at: `audible response`, `confirmation visible`')
71
+ });
65
72
  /**
66
73
  * A number a check measured, such as a latency or a size, with its unit and an optional allowance. A measurement over
67
74
  * its `limit` is reported and never changes the check's status.
75
+ *
76
+ * A measurement of `kind` `product-latency` is how quickly the product responded to a user, between two named events.
77
+ * It is not the check's duration (which includes setup, waits and pacing) and a number with no `kind` is never read as
78
+ * product latency. All the fields after `limit` are optional and describe the measurement; none decides a status.
68
79
  */
69
80
  export const MeasurementSchema = z.looseObject({
70
81
  name: z.string().min(1),
71
82
  value: z.number().describe('A finite number'),
72
83
  unit: z.string(),
73
- limit: z.number().optional().describe('The allowance. A measurement over it is reported, and never fails the check by itself')
84
+ limit: z.number().optional().describe('The allowance, or budget. A measurement over it is reported, and never fails the check by itself'),
85
+ kind: z.string().min(1).optional().describe('What the number is, in an open vocabulary. `product-latency`: a user-visible response time between `from` and `to`. Absent: just a number, never read as product latency'),
86
+ from: z.string().min(1).optional().describe('The event the interval starts at. Without both `from` and `to` a latency is shown but no product claim is made from it'),
87
+ to: z.string().min(1).optional().describe('The event the interval ends at'),
88
+ intended: MeasurementBoundarySchema.optional().describe('The interval the name of the measurement claims, when what was measured is only part of it. If `intended.to` differs from `to` (or `intended.from` from `from`) the measurement is shown as partial, never as the complete interval'),
89
+ source: z.string().optional().describe('How it was measured: `app-instrumentation`, `trace`, `recording-analysis`'),
90
+ outcome: z.string().min(1).optional().describe('`observed` (the default), `timeout` or `failed`. For anything but `observed` the value is how long the observation waited: at least that much, so it counts against a budget and is never dropped'),
91
+ instrumentation: z.string().optional().describe('The version or method of the measurement. A change of it between two sets of observations is disclosed when they are compared'),
92
+ labels: z.record(z.string(), z.string()).optional().describe('What the observation is comparable with: `device`, `build`, `network`. `device` and `build` group the observations and, when two groups differ in the others, the comparison says so')
74
93
  });
75
94
  /**
76
95
  * Structured data of a producer's or tool's own, by namespace: `{ "com.example.coverage": { … } }`.
@@ -28,6 +28,7 @@ import { listRunSummaries, readRunFile } from '../../reports/src/run-bundle.js';
28
28
  import { latestSpecSet, readSpecBody } from '../../reports/src/spec-set.js';
29
29
  import { catalogHealth, healthMarkdown, parseRootMappings } from '../../reports/src/catalog-health.js';
30
30
  import { parseArtifactFilter } from '../../reports/src/artifact-query.js';
31
+ import { parseReliabilityScope, reliability, reliabilityMarkdown } from '../../reports/src/reliability.js';
31
32
  import { artifactList, artifactsMarkdown, catalogMarkdown, catalogModel, historyMarkdown, parseTestQuery, runDetail, runList, runMarkdown, runsMarkdown, testCatalog, testHistory } from '../../reports/src/test-catalog.js';
32
33
  import { llmsTxt } from './llms.js';
33
34
  import { openApiDocument } from './openapi.js';
@@ -467,6 +468,20 @@ function createProjectApplication(options) {
467
468
  return problemResponse(404, 'No test in the report has that key.');
468
469
  return answer(c, found.version, () => document, () => historyMarkdown(document), `history|${key}|${wantsMarkdown(c)}`);
469
470
  });
471
+ app.get('/api/reports/tests/reliability', async (c) => {
472
+ if (!index.ready)
473
+ return notReady(c);
474
+ const key = c.req.query('key');
475
+ if (!key)
476
+ return problemResponse(400, 'Name the test with ?key=, a key from GET /api/reports/tests.');
477
+ const found = await catalog();
478
+ const recorded = testHistory(found, key);
479
+ if (!recorded)
480
+ return problemResponse(404, 'No test in the report has that key.');
481
+ const search = new URL(c.req.url).searchParams;
482
+ const document = () => reliability(key, recorded.history, parseReliabilityScope(search));
483
+ return answer(c, found.version, document, () => reliabilityMarkdown(document()), `reliability|${search.toString()}|${wantsMarkdown(c)}`);
484
+ });
470
485
  app.get('/api/reports/runs', async (c) => {
471
486
  if (!index.ready)
472
487
  return notReady(c);
@@ -68,6 +68,7 @@ Every endpoint below answers JSON, or Markdown with \`format=markdown\`. Prefix
68
68
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests?format=markdown&limit=50" # summary, environments, domains, failing, flaky, slowest, then the tests
69
69
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests?status=failed,flaky&environment=desktop-web" # filter: q, status, environment, producer, domain, spec, tag, run; sort, offset, limit
70
70
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests/history?key=<url-encoded test key>" # one test: every recorded result, durations, branches, CI runs
71
+ curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests/reliability?key=<url-encoded test key>&environment=phone&format=markdown" # how reliable one test is in a scope: independent executions, first-try vs eventual success, what was excluded and why
71
72
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/tests/health?format=markdown" # why Specs, domains and counts disagree; add root=<from>><to> to declare a path-root mapping
72
73
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/runs?format=markdown" # CI runs, newest first, with their results by status and video counts
73
74
  curl "${url}/api/projects/$ATLAS_PROJECT/reports/runs?video=true&format=markdown" # only runs with a playable video recording
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { z } from 'zod/v4';
16
16
  import { ProjectsSchema } from '../../schema/src/project.js';
17
- import { EffectiveDeltaSchema, EffectiveEvidenceSchema, EvidencePageSchema, HealthSchema, LayerRowsSchema, CheckDetailSchema, OutlineSchema, ProblemSchema, ReadySchema, ServerInfoSchema, StoreListingSchema, StoreWriteResultSchema, ChecksResultSchema, HelloEventSchema, ReportEventSchema, PingEventSchema, TestCatalogSchema, CatalogHealthSchema, TestHistorySchema, RunListSchema, RunDetailSchema, ArtifactListSchema, ArtifactTagEditSchema, ArtifactTagEntrySchema, ArtifactTagsSchema } from '../../schema/src/api.js';
17
+ import { EffectiveDeltaSchema, EffectiveEvidenceSchema, EvidencePageSchema, HealthSchema, LayerRowsSchema, CheckDetailSchema, OutlineSchema, ProblemSchema, ReadySchema, ServerInfoSchema, StoreListingSchema, StoreWriteResultSchema, ChecksResultSchema, HelloEventSchema, ReportEventSchema, PingEventSchema, TestCatalogSchema, CatalogHealthSchema, TestHistorySchema, TestReliabilitySchema, RunListSchema, RunDetailSchema, ArtifactListSchema, ArtifactTagEditSchema, ArtifactTagEntrySchema, ArtifactTagsSchema } from '../../schema/src/api.js';
18
18
  import { LiveRunSchema, LiveRunStartSchema, LiveRunUpdateSchema, ServerEditableSettingsSchema, ServerSettingsSnapshotSchema, GcPolicySchema, GcSnapshotSchema, GcRunResultSchema, AtlasChangeSchema, PublishChecksSchema, ReportSourcesSchema, ReportStatusSchema, RunsIndexSchema, SpecIndexSchema } from '../../schema/src/index.js';
19
19
  /** The version of the API described, `major.minor.patch`: the minor grows when an operation or field is added, the major never within `/api`. */
20
20
  export const API_VERSION = '1.5.0';
@@ -37,7 +37,7 @@ const COMPONENTS = {
37
37
  StoreListing: [StoreListingSchema, 'output'], StoreWriteResult: [StoreWriteResultSchema, 'output'],
38
38
  Outline: [OutlineSchema, 'output'], EvidencePage: [EvidencePageSchema, 'output'], LayerRows: [LayerRowsSchema, 'output'], CheckDetail: [CheckDetailSchema, 'output'],
39
39
  ArtifactTagEdit: [ArtifactTagEditSchema, 'input'], ArtifactTagEntry: [ArtifactTagEntrySchema, 'output'], ArtifactTags: [ArtifactTagsSchema, 'output'],
40
- TestCatalog: [TestCatalogSchema, 'output'], CatalogHealth: [CatalogHealthSchema, 'output'], TestHistory: [TestHistorySchema, 'output'], RunList: [RunListSchema, 'output'], ArtifactList: [ArtifactListSchema, 'output'], RunDetail: [RunDetailSchema, 'output'],
40
+ TestCatalog: [TestCatalogSchema, 'output'], CatalogHealth: [CatalogHealthSchema, 'output'], TestHistory: [TestHistorySchema, 'output'], TestReliability: [TestReliabilitySchema, 'output'], RunList: [RunListSchema, 'output'], ArtifactList: [ArtifactListSchema, 'output'], RunDetail: [RunDetailSchema, 'output'],
41
41
  SpecIndex: [SpecIndexSchema, 'output'], ReportSources: [ReportSourcesSchema, 'output'], RunsIndex: [RunsIndexSchema, 'output'],
42
42
  RunEvent: [LiveRunSchema, 'output'], HelloEvent: [HelloEventSchema, 'output'], ReportEvent: [ReportEventSchema, 'output'], AtlasChangeEvent: [AtlasChangeSchema, 'output'], PingEvent: [PingEventSchema, 'output']
43
43
  };
@@ -328,6 +328,29 @@ const BASE_OPERATIONS = [
328
328
  ],
329
329
  responses: { '200': json('The test and its history.', 'TestHistory'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '400': problem('No `key`.'), '401': UNAUTHORIZED, '404': problem('No test in the report has that key.'), '503': NOT_READY }
330
330
  },
331
+ {
332
+ route: 'GET /api/reports/tests/reliability', path: '/api/reports/tests/reliability', operationId: 'getTestReliability', tag: 'Report', role: 'read',
333
+ summary: 'How reliable one test is in a scope, and how that was counted',
334
+ description: [
335
+ 'A success rate for one test that says what it counted. The snapshots one run recorded for one producer, environment and parameter set (running, finished, enriched) are one independent execution; its last word counts once with the tries it recorded. `eventual` is the share of executions that passed in the end, `firstAttempt` the share whose first try passed (over executions that recorded their tries; the rest are `unrecordedAttempts`, never assumed to have passed first time).',
336
+ '',
337
+ 'Choose the scope with `environment`, `producer`, `parameter` (`key=value`), `branch`, `sha`, `specRevision`, `since` and `until`; executions elsewhere never count. By default only the newest recorded spec revision counts and earlier ones appear under `historical`; send `revision=any` to pool them. Only `passed` and `failed` executions are trials: `not-run` and unfinished ones are listed under `excluded` and never counted as a pass. `sample` is `none`, `insufficient` (fewer than `minimumExecutions`) or `sufficient`, a count only: `notes` say when environments, producers or branches are pooled, failed cases were published again, or history is bounded to the newest 100 results. `executions` is the denominator, open to inspection. The interval states its assumptions. This is not a production failure probability. Send `format=markdown` for Markdown.'
338
+ ].join('\n'),
339
+ parameters: [
340
+ { name: 'key', in: 'query', required: true, description: 'The test\'s `key` from `/api/reports/tests`.', schema: str, example: '["specs/checkout/pay.feature","Pay with a card"]' },
341
+ { name: 'environment', in: 'query', description: 'Environments or devices to count. Comma-separated.', schema: str, example: 'phone' },
342
+ { name: 'producer', in: 'query', description: 'Producers to count. Comma-separated.', schema: str },
343
+ { name: 'parameter', in: 'query', description: 'Parameter choices `key=value`; all must match. Comma-separated.', schema: str, example: 'device=small' },
344
+ { name: 'branch', in: 'query', description: 'Branches to count. Comma-separated.', schema: str },
345
+ { name: 'sha', in: 'query', description: 'Commits: a full id or a prefix of at least seven characters. Comma-separated.', schema: str },
346
+ { name: 'specRevision', in: 'query', description: 'Only executions recorded against these spec fingerprints (overrides `revision`). Comma-separated.', schema: str },
347
+ { name: 'revision', in: 'query', description: '`any` pools every spec revision; otherwise only the newest recorded one counts.', schema: { type: 'string', enum: ['current', 'any'] } },
348
+ { name: 'since', in: 'query', description: 'Only executions at or after this time: epoch milliseconds or a date.', schema: str, example: '2026-01-01' },
349
+ { name: 'until', in: 'query', description: 'Only executions at or before this time: epoch milliseconds or a date.', schema: str },
350
+ { name: 'format', in: 'query', description: '`markdown` for Markdown instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
351
+ ],
352
+ responses: { '200': json('The reliability of the test in the scope.', 'TestReliability'), '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '400': problem('No `key`.'), '401': UNAUTHORIZED, '404': problem('No test in the report has that key.'), '503': NOT_READY }
353
+ },
331
354
  {
332
355
  route: 'GET /api/reports/artifact-tags', path: '/api/reports/artifact-tags', operationId: 'getArtifactTags', tag: 'Report', role: 'read',
333
356
  summary: 'Reviewer tags of attachments: every recorded edit',
package/docs/extending.md CHANGED
@@ -34,7 +34,7 @@ carry any of these. Every one is optional.
34
34
  | `scope{}` | What the evidence does and does not show: surface, device kind, synthetic data, scripted dependencies, audio, pacing, what was asserted and its limits. Unknown when absent. See [Evidence scope](rich-evidence.md#evidence-scope). |
35
35
  | `links[]` | Where to read more: the CI run, the issue, the trace client. |
36
36
  | `extensions{}` | Structured data of your own or a tool's, under a name you own. |
37
- | `measurements[]` | Numbers, with units and an allowance (already part of the contract). |
37
+ | `measurements[]` | Numbers, with units and an allowance (already part of the contract). A measurement of `kind` `product-latency` names a user-visible response time between two events; see [Product latency](rich-evidence.md#product-latency). |
38
38
 
39
39
  An attachment may carry its own `tags[]` (`demo`): shown as `#demo` and searchable; see
40
40
  [Tagging artifacts and demos](rich-evidence.md#tagging-artifacts-and-demos).
@@ -598,6 +598,54 @@ Scoped to one project. The same project may be selected on the unprefixed API us
598
598
 
599
599
  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`.
600
600
 
601
+ ### How reliable one test is in a scope, and how that was counted
602
+
603
+ `GET /api/reports/tests/reliability` · role: read
604
+
605
+ A success rate for one test that says what it counted. The snapshots one run recorded for one producer, environment and parameter set (running, finished, enriched) are one independent execution; its last word counts once with the tries it recorded. `eventual` is the share of executions that passed in the end, `firstAttempt` the share whose first try passed (over executions that recorded their tries; the rest are `unrecordedAttempts`, never assumed to have passed first time).
606
+
607
+ Choose the scope with `environment`, `producer`, `parameter` (`key=value`), `branch`, `sha`, `specRevision`, `since` and `until`; executions elsewhere never count. By default only the newest recorded spec revision counts and earlier ones appear under `historical`; send `revision=any` to pool them. Only `passed` and `failed` executions are trials: `not-run` and unfinished ones are listed under `excluded` and never counted as a pass. `sample` is `none`, `insufficient` (fewer than `minimumExecutions`) or `sufficient`, a count only: `notes` say when environments, producers or branches are pooled, failed cases were published again, or history is bounded to the newest 100 results. `executions` is the denominator, open to inspection. The interval states its assumptions. This is not a production failure probability. Send `format=markdown` for Markdown.
608
+
609
+ - `key` (query, required): The test's `key` from `/api/reports/tests`.
610
+ - `environment` (query): Environments or devices to count. Comma-separated.
611
+ - `producer` (query): Producers to count. Comma-separated.
612
+ - `parameter` (query): Parameter choices `key=value`; all must match. Comma-separated.
613
+ - `branch` (query): Branches to count. Comma-separated.
614
+ - `sha` (query): Commits: a full id or a prefix of at least seven characters. Comma-separated.
615
+ - `specRevision` (query): Only executions recorded against these spec fingerprints (overrides `revision`). Comma-separated.
616
+ - `revision` (query): `any` pools every spec revision; otherwise only the newest recorded one counts.
617
+ - `since` (query): Only executions at or after this time: epoch milliseconds or a date.
618
+ - `until` (query): Only executions at or before this time: epoch milliseconds or a date.
619
+ - `format` (query): `markdown` for Markdown instead of JSON.
620
+ - `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
621
+
622
+ Responses: `200` The reliability of the test in the scope.; `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`.
623
+
624
+ ### How reliable one test is in a scope, and how that was counted
625
+
626
+ `GET /api/projects/{project}/reports/tests/reliability` · role: read
627
+
628
+ A success rate for one test that says what it counted. The snapshots one run recorded for one producer, environment and parameter set (running, finished, enriched) are one independent execution; its last word counts once with the tries it recorded. `eventual` is the share of executions that passed in the end, `firstAttempt` the share whose first try passed (over executions that recorded their tries; the rest are `unrecordedAttempts`, never assumed to have passed first time).
629
+
630
+ Choose the scope with `environment`, `producer`, `parameter` (`key=value`), `branch`, `sha`, `specRevision`, `since` and `until`; executions elsewhere never count. By default only the newest recorded spec revision counts and earlier ones appear under `historical`; send `revision=any` to pool them. Only `passed` and `failed` executions are trials: `not-run` and unfinished ones are listed under `excluded` and never counted as a pass. `sample` is `none`, `insufficient` (fewer than `minimumExecutions`) or `sufficient`, a count only: `notes` say when environments, producers or branches are pooled, failed cases were published again, or history is bounded to the newest 100 results. `executions` is the denominator, open to inspection. The interval states its assumptions. This is not a production failure probability. Send `format=markdown` for Markdown.
631
+
632
+ 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.
633
+
634
+ - `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.
635
+ - `key` (query, required): The test's `key` from `/api/reports/tests`.
636
+ - `environment` (query): Environments or devices to count. Comma-separated.
637
+ - `producer` (query): Producers to count. Comma-separated.
638
+ - `parameter` (query): Parameter choices `key=value`; all must match. Comma-separated.
639
+ - `branch` (query): Branches to count. Comma-separated.
640
+ - `sha` (query): Commits: a full id or a prefix of at least seven characters. Comma-separated.
641
+ - `specRevision` (query): Only executions recorded against these spec fingerprints (overrides `revision`). Comma-separated.
642
+ - `revision` (query): `any` pools every spec revision; otherwise only the newest recorded one counts.
643
+ - `since` (query): Only executions at or after this time: epoch milliseconds or a date.
644
+ - `until` (query): Only executions at or before this time: epoch milliseconds or a date.
645
+ - `format` (query): `markdown` for Markdown instead of JSON.
646
+
647
+ Responses: `200` The reliability of the test in the scope.; `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`.
648
+
601
649
  ### Reviewer tags of attachments: every recorded edit
602
650
 
603
651
  `GET /api/reports/artifact-tags` · role: read