@crvouga/atlas 1.14.0 → 1.16.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.
- package/CHANGELOG.md +14 -1
- package/dist/reports/src/catalog-health.d.ts +15 -2
- package/dist/reports/src/catalog-health.js +23 -2
- package/dist/reports/src/evidence-detail.d.ts +19 -0
- package/dist/reports/src/evidence-detail.js +31 -4
- package/dist/reports/src/evidence-types.d.ts +2 -2
- package/dist/reports/src/layer-merge.js +4 -3
- package/dist/reports/src/layer-store.d.ts +1 -1
- package/dist/reports/src/layer-store.js +4 -2
- package/dist/reports/src/layer-types.d.ts +2 -0
- package/dist/reports/src/run-compare.d.ts +182 -0
- package/dist/reports/src/run-compare.js +329 -0
- package/dist/reports/src/test-catalog.d.ts +8 -0
- package/dist/reports/src/test-catalog.js +14 -1
- package/dist/schema/src/api.d.ts +10 -0
- package/dist/schema/src/api.js +15 -5
- package/dist/schema/src/server-config.d.ts +3 -0
- package/dist/schema/src/server-config.js +5 -0
- package/dist/server/src/app.d.ts +2 -0
- package/dist/server/src/app.js +6 -2
- package/dist/server/src/config.js +4 -0
- package/dist/server/src/main.js +1 -0
- package/dist/server/src/openapi.js +2 -2
- package/docs/extending.md +1 -1
- package/docs/reference/http-api.md +4 -4
- package/docs/reference/reports.md +378 -5
- package/docs/reference/server.md +2 -0
- package/docs/server.md +11 -2
- package/docs/ui.md +49 -1
- package/llms-full.txt +65 -8
- package/package.json +1 -1
- package/packages/schema/schemas/catalog-health.schema.json +20 -0
- package/packages/schema/schemas/effective-delta.schema.json +2 -2
- package/packages/schema/schemas/effective-evidence.schema.json +2 -2
- package/packages/schema/schemas/evidence-page.schema.json +1 -1
- package/packages/schema/schemas/layer-rows.schema.json +1 -1
- package/packages/schema/schemas/report-run.schema.json +25 -3
- package/packages/schema/schemas/report-runs.schema.json +1 -1
- package/packages/schema/schemas/review-collection.schema.json +1 -1
- package/packages/schema/schemas/review-manifest.schema.json +1 -1
- package/packages/schema/schemas/server-config.schema.json +17 -0
- package/packages/schema/schemas/test-catalog.schema.json +1 -1
- package/packages/schema/schemas/test-history.schema.json +2 -2
- package/packages/server/openapi.json +63 -21
- package/packages/visualizer/src/explorer/ComparePair.tsx +597 -0
- package/packages/visualizer/src/explorer/CompareView.tsx +340 -0
- package/packages/visualizer/src/explorer/Explorer.tsx +13 -2
- package/packages/visualizer/src/explorer/HealthView.tsx +402 -0
- package/packages/visualizer/src/explorer/RunsView.tsx +10 -0
- package/packages/visualizer/src/explorer/catalog.tsx +33 -0
- package/packages/visualizer/src/explorer/playlist.tsx +1 -1
- package/packages/visualizer/src/explorer/search.ts +18 -3
|
@@ -806,6 +806,10 @@ Explains why the Specs and the Tests of a report may disagree, and what a produc
|
|
|
806
806
|
- **Fallback identity.** A check whose id is its title has no stable id; renaming it starts a new check. Counted, not
|
|
807
807
|
guessed at.
|
|
808
808
|
|
|
809
|
+
- **Declared ids.** A producer may say what a check is with the labels `behavior-id` (the promise) and `example-id`
|
|
810
|
+
(one example of an outline). Atlas counts them and names ids that sit on more than one check; it does not join the
|
|
811
|
+
checks, because a check is still its spec path and id.
|
|
812
|
+
|
|
809
813
|
It only reads: it never changes a status, a slot or a winner, so it cannot disagree with the report it describes.
|
|
810
814
|
Optional label `outline` on a check names the scenario outline its example belongs to.
|
|
811
815
|
|
|
@@ -827,13 +831,30 @@ export const HEALTH_ITEM_LIMIT = …
|
|
|
827
831
|
|
|
828
832
|
The most paths listed in one finding; its `count` is always the whole.
|
|
829
833
|
|
|
834
|
+
#### `BEHAVIOR_ID_LABEL`
|
|
835
|
+
|
|
836
|
+
```ts
|
|
837
|
+
export const BEHAVIOR_ID_LABEL = …
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
The label a producer puts on a check to give its behavior (the promise) an id that outlives renames and moves.
|
|
841
|
+
|
|
842
|
+
#### `EXAMPLE_ID_LABEL`
|
|
843
|
+
|
|
844
|
+
```ts
|
|
845
|
+
export const EXAMPLE_ID_LABEL = …
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
The label a producer puts on a check to give one example of a scenario outline an id of its own.
|
|
849
|
+
|
|
830
850
|
#### `RootMapping`
|
|
831
851
|
|
|
832
852
|
```ts
|
|
833
|
-
export type RootMapping = { readonly from: string; readonly to: string };
|
|
853
|
+
export type RootMapping = { readonly from: string; readonly to: string; readonly source?: 'configuration' | 'request' };
|
|
834
854
|
```
|
|
835
855
|
|
|
836
|
-
Declares that paths under `from` are the same specs as paths under `to`, e.g. `{ from: '', to: 'specs/' }`. Empty `from`
|
|
856
|
+
Declares that paths under `from` are the same specs as paths under `to`, e.g. `{ from: '', to: 'specs/' }`. Empty `from`
|
|
857
|
+
matches every path. `source` says where it was declared (the server's configuration or the request); it changes nothing.
|
|
837
858
|
|
|
838
859
|
#### `HealthItem`
|
|
839
860
|
|
|
@@ -860,6 +881,7 @@ export type HealthCode =
|
|
|
860
881
|
| 'no-spec-definitions'
|
|
861
882
|
| 'fallback-identity'
|
|
862
883
|
| 'unused-root-mapping'
|
|
884
|
+
| 'declared-id-split'
|
|
863
885
|
| 'spec-without-execution'
|
|
864
886
|
| 'historical-only';
|
|
865
887
|
```
|
|
@@ -1694,6 +1716,16 @@ Like {@link summarizeEvidence} for the checks of a part: a video that declares n
|
|
|
1694
1716
|
declares none, takes the scope `inherited` from its part or layer (the nearest declaration of the two, see
|
|
1695
1717
|
`effectiveScope`). Without `inherited` it is the same as `summarizeEvidence`.
|
|
1696
1718
|
|
|
1719
|
+
#### `summarizeStoredEvidence`
|
|
1720
|
+
|
|
1721
|
+
```ts
|
|
1722
|
+
export function summarizeStoredEvidence(inherited: EvidenceScope | undefined, unstored: ReadonlySet<string> | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined
|
|
1723
|
+
```
|
|
1724
|
+
|
|
1725
|
+
Like {@link summarizeEvidenceIn}, knowing which bundle keys the layer names but does not hold (`unstored`, see
|
|
1726
|
+
`unstoredKeys`): a video whose only bytes are such a key is `available: false`, never a recording. Without
|
|
1727
|
+
`unstored` it is the same as `summarizeEvidenceIn`.
|
|
1728
|
+
|
|
1697
1729
|
#### `summarizeLayerEvidence`
|
|
1698
1730
|
|
|
1699
1731
|
```ts
|
|
@@ -1703,6 +1735,14 @@ export function summarizeLayerEvidence(...holders: readonly EvidenceHolder[]): E
|
|
|
1703
1735
|
Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
|
|
1704
1736
|
part is owned by the `layer`, not by a check.
|
|
1705
1737
|
|
|
1738
|
+
#### `summarizeStoredLayerEvidence`
|
|
1739
|
+
|
|
1740
|
+
```ts
|
|
1741
|
+
export function summarizeStoredLayerEvidence(unstored: ReadonlySet<string> | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined
|
|
1742
|
+
```
|
|
1743
|
+
|
|
1744
|
+
Like {@link summarizeLayerEvidence}, knowing which bundle keys the layer names but does not hold (see {@link summarizeStoredEvidence}).
|
|
1745
|
+
|
|
1706
1746
|
#### `isVideoAttachment`
|
|
1707
1747
|
|
|
1708
1748
|
```ts
|
|
@@ -1804,6 +1844,16 @@ export function bundleKeysOf(holder: EvidenceHolder): string[]
|
|
|
1804
1844
|
|
|
1805
1845
|
Every distinct bundle key (`bundleKey`) that the attachments of a holder, its steps and its attempts point to, so a layer's files can be copied. Attachments held by `uri` or inline `text` have none.
|
|
1806
1846
|
|
|
1847
|
+
#### `unstoredKeys`
|
|
1848
|
+
|
|
1849
|
+
```ts
|
|
1850
|
+
export function unstoredKeys(layer: EvidenceHolder & { readonly parts: readonly (EvidenceHolder & { readonly checks: readonly EvidenceHolder[] })[]; readonly bundle: Readonly<Record<string, unknown>> }): string[]
|
|
1851
|
+
```
|
|
1852
|
+
|
|
1853
|
+
The bundle keys that attachments of a layer, its parts and their checks name, but that the layer's `bundle` index does
|
|
1854
|
+
not hold: files the producer referenced and never stored. Empty for a layer whose every file is there. This reads only
|
|
1855
|
+
the index, never a blob, so it is cheap enough to ask of every layer as it is read.
|
|
1856
|
+
|
|
1807
1857
|
### Live evidence from running suites
|
|
1808
1858
|
|
|
1809
1859
|
Source: `packages/reports/src/evidence-live.ts`. Import: `@crvouga/atlas/reports/evidence-live`
|
|
@@ -2109,7 +2159,7 @@ export type VideoRef = {
|
|
|
2109
2159
|
readonly tags?: readonly string[];
|
|
2110
2160
|
/** What identifies the bytes (digest, bundle key or address); references with the same one are the same recording. Absent when `available` is false. */
|
|
2111
2161
|
readonly recording?: string;
|
|
2112
|
-
/** False when the reference names no bytes at all (no bundle key, address or inline text), so it can never be played. */
|
|
2162
|
+
/** False when the reference names no bytes at all (no bundle key, address or inline text), or a bundle key that the layer's own bundle does not hold, so it can never be played. */
|
|
2113
2163
|
readonly available: boolean;
|
|
2114
2164
|
/** What this recording shows, as declared for it: its own `scope`, else its attempt's, check's, part's or layer's. Absent when none declared one: its scope is unknown, not live, physical or audible. */
|
|
2115
2165
|
readonly scope?: EvidenceScope;
|
|
@@ -2128,7 +2178,7 @@ export type VideoCounts = {
|
|
|
2128
2178
|
readonly references: number;
|
|
2129
2179
|
/** Distinct playable recordings: identical bytes referenced twice are one. */
|
|
2130
2180
|
readonly recordings: number;
|
|
2131
|
-
/** References that name no bytes. A report that predates availability metadata has none of these and none of the others. */
|
|
2181
|
+
/** References that name no bytes, or bytes the layer does not hold. A report that predates availability metadata has none of these and none of the others. */
|
|
2132
2182
|
readonly unavailable: number;
|
|
2133
2183
|
};
|
|
2134
2184
|
```
|
|
@@ -4870,7 +4920,7 @@ Reads one captured file (a screenshot, a frame, a recorded report) of a layer, o
|
|
|
4870
4920
|
export function layerSummary(layer: ReportLayer): LayerSummary
|
|
4871
4921
|
```
|
|
4872
4922
|
|
|
4873
|
-
Drops a layer's bundle index, leaving the summary composition reads.
|
|
4923
|
+
Drops a layer's bundle index, leaving the summary composition reads, plus the bundle keys its attachments name that the bundle does not hold (`unstored`), so a video that was never stored is not counted as a recording.
|
|
4874
4924
|
|
|
4875
4925
|
### Layer types
|
|
4876
4926
|
|
|
@@ -4931,6 +4981,8 @@ export type LayerSummary = LayerExtras & {
|
|
|
4931
4981
|
readonly source: SourceRecord | null;
|
|
4932
4982
|
/** Where the layer's timelines and captured files are read from: its own bundle (`layer`), an older commit-keyed recording (`commit`), or nowhere (`null`). */
|
|
4933
4983
|
readonly recording: { readonly type: 'layer' } | { readonly type: 'commit'; readonly sha: string; readonly source: string } | null;
|
|
4984
|
+
/** Bundle keys that attachments name but the layer's bundle does not hold, read from the layer's index when its summary was made (see `unstoredKeys`). Absent when there are none. In memory only: never part of the layer document. */
|
|
4985
|
+
readonly unstored?: readonly string[];
|
|
4934
4986
|
};
|
|
4935
4987
|
```
|
|
4936
4988
|
|
|
@@ -8162,6 +8214,319 @@ export async function readRunFile(store: ObjectStore, id: string, relative: stri
|
|
|
8162
8214
|
|
|
8163
8215
|
Reads one file of a run, or `null` when it is missing. Also `null`, without reading, for an `id` that is not a plain run name or a `relative` path with an empty, `.`, `..` or backslash segment, so a path can never leave the run.
|
|
8164
8216
|
|
|
8217
|
+
### Run comparison
|
|
8218
|
+
|
|
8219
|
+
Source: `packages/reports/src/run-compare.ts`. Import: `@crvouga/atlas/reports/run-compare`
|
|
8220
|
+
|
|
8221
|
+
Answers "what changed between these two runs?" without claiming more than the evidence supports. A run is a pinned
|
|
8222
|
+
source: the comparison reads two {@link RunDetail}s as they are and never reaches for a newer run. Pure functions,
|
|
8223
|
+
safe in a browser; nothing here changes a status, a slot or a winner.
|
|
8224
|
+
|
|
8225
|
+
The rules, each of which keeps a comparison from looking more certain than it is:
|
|
8226
|
+
|
|
8227
|
+
- **Identity is explicit.** Results are paired by the check they record (`TestSummary.key`: spec path and check id),
|
|
8228
|
+
then by the condition they were recorded under (kind, producer, environment, parameters). A title is never what
|
|
8229
|
+
pairs two results. A result with the same check but another condition is paired as well, marked as recorded under
|
|
8230
|
+
different conditions, and is not framed as the same experiment. Results of one side with no partner are `added`
|
|
8231
|
+
(only the candidate has them) or `removed` (only the baseline has them); a `removed` result is not a regression,
|
|
8232
|
+
because the candidate may not have run, renamed or dropped it, and the comparison cannot tell which.
|
|
8233
|
+
- **Not run is not regressed.** A result that did not run, or is still running, on either side is `unknown`; it is
|
|
8234
|
+
counted as neither a pass nor a failure. Only a completed result against a completed result is a movement.
|
|
8235
|
+
- **A changed promise is not the same experiment.** When the wording of a behavior differs between the two sides, the
|
|
8236
|
+
pair is `not-comparable` and both outcomes are shown beside the changed promise instead of as a regression or a
|
|
8237
|
+
recovery. A different spec-file revision with the same wording is disclosed beside the movement: a fingerprint is
|
|
8238
|
+
of the whole file, so it cannot say that this behavior's own promise changed. A revision that was not recorded is
|
|
8239
|
+
unknown, never the same.
|
|
8240
|
+
- **Figures are compared like for like.** Product latency and reliability are compared only when both sides were
|
|
8241
|
+
recorded under the same conditions and support the figure; otherwise the reason is given and no improvement is
|
|
8242
|
+
computed. A metric only one side has is named, not compared.
|
|
8243
|
+
|
|
8244
|
+
See docs/ui.md
|
|
8245
|
+
|
|
8246
|
+
#### `COMPARE_GROUPS`
|
|
8247
|
+
|
|
8248
|
+
```ts
|
|
8249
|
+
export const COMPARE_GROUPS = …
|
|
8250
|
+
```
|
|
8251
|
+
|
|
8252
|
+
Where a pair of results (or one result with no partner) stands, in the order a review reads them.
|
|
8253
|
+
|
|
8254
|
+
#### `CompareGroup`
|
|
8255
|
+
|
|
8256
|
+
```ts
|
|
8257
|
+
export type CompareGroup = (typeof COMPARE_GROUPS)[number];
|
|
8258
|
+
```
|
|
8259
|
+
|
|
8260
|
+
One of {@link COMPARE_GROUPS}.
|
|
8261
|
+
|
|
8262
|
+
#### `COMPARE_GROUP_TEXT`
|
|
8263
|
+
|
|
8264
|
+
```ts
|
|
8265
|
+
export const COMPARE_GROUP_TEXT: Readonly<Record<CompareGroup, { readonly label: string; readonly meaning: string }>>
|
|
8266
|
+
```
|
|
8267
|
+
|
|
8268
|
+
What each group is called and what it does and does not say.
|
|
8269
|
+
|
|
8270
|
+
#### `PromiseState`
|
|
8271
|
+
|
|
8272
|
+
```ts
|
|
8273
|
+
export type PromiseState = 'same' | 'changed' | 'revision-differs' | 'unknown';
|
|
8274
|
+
```
|
|
8275
|
+
|
|
8276
|
+
Whether the promise (what the behavior says should happen) is the same on both sides: `same` (same wording and the
|
|
8277
|
+
same recorded spec revision), `changed` (the wording differs), `revision-differs` (same wording, another spec-file
|
|
8278
|
+
revision), or `unknown` (a revision was not recorded on a side, so nothing can be said).
|
|
8279
|
+
|
|
8280
|
+
#### `PromiseReading`
|
|
8281
|
+
|
|
8282
|
+
```ts
|
|
8283
|
+
export type PromiseReading = {
|
|
8284
|
+
readonly state: PromiseState;
|
|
8285
|
+
readonly baselineTitle: string | null;
|
|
8286
|
+
readonly candidateTitle: string | null;
|
|
8287
|
+
readonly baselineRevision: string | null;
|
|
8288
|
+
readonly candidateRevision: string | null;
|
|
8289
|
+
/** In words, for a person. */
|
|
8290
|
+
readonly reasons: readonly string[];
|
|
8291
|
+
};
|
|
8292
|
+
```
|
|
8293
|
+
|
|
8294
|
+
What is known about whether the promise changed, and why.
|
|
8295
|
+
|
|
8296
|
+
#### `Condition`
|
|
8297
|
+
|
|
8298
|
+
```ts
|
|
8299
|
+
export type Condition = {
|
|
8300
|
+
readonly kind: string | null;
|
|
8301
|
+
readonly producer: string;
|
|
8302
|
+
readonly environment: string | null;
|
|
8303
|
+
readonly parameters: Readonly<Record<string, string>>;
|
|
8304
|
+
};
|
|
8305
|
+
```
|
|
8306
|
+
|
|
8307
|
+
The condition a result was recorded under: with the check, what makes it its own result.
|
|
8308
|
+
|
|
8309
|
+
#### `conditionOf`
|
|
8310
|
+
|
|
8311
|
+
```ts
|
|
8312
|
+
export function conditionOf(result: Pick<RunResult, 'kind' | 'producer' | 'environment' | 'parameters'>): Condition
|
|
8313
|
+
```
|
|
8314
|
+
|
|
8315
|
+
The condition of a result. A server that does not send `kind` leaves it null: unknown, equal only to another unknown.
|
|
8316
|
+
|
|
8317
|
+
#### `conditionId`
|
|
8318
|
+
|
|
8319
|
+
```ts
|
|
8320
|
+
export function conditionId(condition: Condition): string
|
|
8321
|
+
```
|
|
8322
|
+
|
|
8323
|
+
A stable text for a condition, equal exactly when two conditions are.
|
|
8324
|
+
|
|
8325
|
+
#### `conditionDifferences`
|
|
8326
|
+
|
|
8327
|
+
```ts
|
|
8328
|
+
export function conditionDifferences(baseline: Condition, candidate: Condition): string[]
|
|
8329
|
+
```
|
|
8330
|
+
|
|
8331
|
+
How two conditions differ, in words; empty when they are the same.
|
|
8332
|
+
|
|
8333
|
+
#### `ComparedResult`
|
|
8334
|
+
|
|
8335
|
+
```ts
|
|
8336
|
+
export type ComparedResult = {
|
|
8337
|
+
/** Identifies the pair within one comparison: the check, and the condition of each side. Stable for the same two runs. */
|
|
8338
|
+
readonly id: string;
|
|
8339
|
+
/** The check's identity (`TestSummary.key`). */
|
|
8340
|
+
readonly key: string;
|
|
8341
|
+
/** The check's title: the candidate's when it has one, else the baseline's. */
|
|
8342
|
+
readonly title: string;
|
|
8343
|
+
readonly specPath: string | null;
|
|
8344
|
+
readonly baseline: RunResult | null;
|
|
8345
|
+
readonly candidate: RunResult | null;
|
|
8346
|
+
readonly group: CompareGroup;
|
|
8347
|
+
/** One sentence saying what happened, and what is not known. */
|
|
8348
|
+
readonly summary: string;
|
|
8349
|
+
readonly promise: PromiseReading;
|
|
8350
|
+
/** How the two conditions differ, in words, when both sides have a result and they were recorded under different conditions. */
|
|
8351
|
+
readonly conditions: readonly string[];
|
|
8352
|
+
/** What a reader must not assume about this pair, in words. */
|
|
8353
|
+
readonly caveats: readonly string[];
|
|
8354
|
+
};
|
|
8355
|
+
```
|
|
8356
|
+
|
|
8357
|
+
One behavior's results on the two sides.
|
|
8358
|
+
|
|
8359
|
+
#### `RunComparison`
|
|
8360
|
+
|
|
8361
|
+
```ts
|
|
8362
|
+
export type RunComparison = {
|
|
8363
|
+
readonly baseline: RunSummary;
|
|
8364
|
+
readonly candidate: RunSummary;
|
|
8365
|
+
/** Every pair and every unpaired result, in the order of {@link COMPARE_GROUPS}, then spec and title. */
|
|
8366
|
+
readonly results: readonly ComparedResult[];
|
|
8367
|
+
readonly counts: Readonly<Record<CompareGroup, number>>;
|
|
8368
|
+
/** What a reader must not assume about the two runs as a whole: the same run on both sides, different branches, a candidate older than the baseline. */
|
|
8369
|
+
readonly notes: readonly string[];
|
|
8370
|
+
};
|
|
8371
|
+
```
|
|
8372
|
+
|
|
8373
|
+
The whole comparison of two pinned runs.
|
|
8374
|
+
|
|
8375
|
+
#### `shortRevision`
|
|
8376
|
+
|
|
8377
|
+
```ts
|
|
8378
|
+
export function shortRevision(revision: string | null | undefined): string
|
|
8379
|
+
```
|
|
8380
|
+
|
|
8381
|
+
Short form of a spec fingerprint, to show.
|
|
8382
|
+
|
|
8383
|
+
#### `recordedTitle`
|
|
8384
|
+
|
|
8385
|
+
```ts
|
|
8386
|
+
export function recordedTitle(result: Pick<RunResult, 'title' | 'recordedTitle'>): string
|
|
8387
|
+
```
|
|
8388
|
+
|
|
8389
|
+
The wording a run recorded for a check: what that run's layer called it, else the check's newest title.
|
|
8390
|
+
|
|
8391
|
+
#### `compareRuns`
|
|
8392
|
+
|
|
8393
|
+
```ts
|
|
8394
|
+
export function compareRuns(baseline: RunDetail, candidate: RunDetail): RunComparison
|
|
8395
|
+
```
|
|
8396
|
+
|
|
8397
|
+
Pairs the results of two runs and reads what changed. Results are paired by check identity and then by condition (see
|
|
8398
|
+
the module comment); titles never pair two results. Each side's results are those the run recorded last for each
|
|
8399
|
+
check and condition (`RunDetail.results`).
|
|
8400
|
+
|
|
8401
|
+
- `baseline`: The run to compare against.
|
|
8402
|
+
|
|
8403
|
+
- `candidate`: The run under review. Both are used as given: a newer run never replaces either.
|
|
8404
|
+
|
|
8405
|
+
**Returns** Every result of either run, grouped by what changed, with the reasons.
|
|
8406
|
+
|
|
8407
|
+
**Example**
|
|
8408
|
+
|
|
8409
|
+
```ts
|
|
8410
|
+
const comparison = compareRuns(await runDetail('run:ci-100'), await runDetail('run:ci-101'));
|
|
8411
|
+
comparison.results.filter(item => item.group === 'regressed');
|
|
8412
|
+
```
|
|
8413
|
+
|
|
8414
|
+
#### `MetricComparison`
|
|
8415
|
+
|
|
8416
|
+
```ts
|
|
8417
|
+
export type MetricComparison =
|
|
8418
|
+
| { readonly state: 'compared'; readonly name: string; readonly baseline: LatencySummary; readonly candidate: LatencySummary; readonly change: LatencyChange; readonly reasons: readonly string[] }
|
|
8419
|
+
| { readonly state: 'incompatible'; readonly name: string; readonly baseline: LatencySummary; readonly candidate: LatencySummary; readonly change: LatencyChange | null; readonly reasons: readonly string[] }
|
|
8420
|
+
| { readonly state: 'baseline-only' | 'candidate-only'; readonly name: string; readonly summary: LatencySummary; readonly reasons: readonly string[] };
|
|
8421
|
+
```
|
|
8422
|
+
|
|
8423
|
+
What one product-latency metric shows on the two sides.
|
|
8424
|
+
|
|
8425
|
+
#### `compareProductLatency`
|
|
8426
|
+
|
|
8427
|
+
```ts
|
|
8428
|
+
export function compareProductLatency(baseline: readonly LatencyEntry[], candidate: readonly LatencyEntry[], conditionsDiffer: readonly string[] = []): MetricComparison[]
|
|
8429
|
+
```
|
|
8430
|
+
|
|
8431
|
+
Compares the product latency recorded in two sets of results of one check, one per side. A metric is compared only
|
|
8432
|
+
when it is the same measurement (name, unit, boundaries) and both sides were measured on the same devices, in the
|
|
8433
|
+
same environments, with the same instrumentation and other conditions; otherwise it is `incompatible` and no change
|
|
8434
|
+
is computed, with the reasons. A metric only one side recorded is named as such. A side without any latency is unknown,
|
|
8435
|
+
not fast: it contributes no metric. `conditionsDiffer` names differences of the results' own conditions, which make
|
|
8436
|
+
every metric incompatible.
|
|
8437
|
+
|
|
8438
|
+
#### `ReliabilityComparison`
|
|
8439
|
+
|
|
8440
|
+
```ts
|
|
8441
|
+
export type ReliabilityComparison = {
|
|
8442
|
+
/** True only when both figures are established, were counted in the same scope and against the same recorded spec revision. */
|
|
8443
|
+
readonly comparable: boolean;
|
|
8444
|
+
readonly baseline: ReliabilityRate;
|
|
8445
|
+
readonly candidate: ReliabilityRate;
|
|
8446
|
+
/** Candidate minus baseline, in percentage points, only when comparable. */
|
|
8447
|
+
readonly points?: number;
|
|
8448
|
+
/** `higher` or `lower` only when the two intervals do not overlap; `indistinguishable` when they do. Absent when not comparable. */
|
|
8449
|
+
readonly direction?: 'higher' | 'lower' | 'indistinguishable';
|
|
8450
|
+
/** Why no change is stated, or what qualifies one. */
|
|
8451
|
+
readonly reasons: readonly string[];
|
|
8452
|
+
};
|
|
8453
|
+
```
|
|
8454
|
+
|
|
8455
|
+
How reliable a check was on each side, and whether the two figures can be set against each other.
|
|
8456
|
+
|
|
8457
|
+
#### `reliabilityScopeAt`
|
|
8458
|
+
|
|
8459
|
+
```ts
|
|
8460
|
+
export function reliabilityScopeAt(result: Pick<RunResult, 'environment' | 'producer' | 'parameters' | 'specRevision'>, run: Pick<RunSummary, 'endedAtMs'>): ReliabilityScope
|
|
8461
|
+
```
|
|
8462
|
+
|
|
8463
|
+
The scope that counts the independent executions of one result's check up to the end of its run: the same
|
|
8464
|
+
environment, producer and parameters, and the spec revision that result was recorded against (executions against
|
|
8465
|
+
another revision are other promises). A result that recorded no revision is counted across revisions, which
|
|
8466
|
+
{@link compareReliability} will not compare. Use it for each side, with that side's result and run.
|
|
8467
|
+
|
|
8468
|
+
#### `compareReliability`
|
|
8469
|
+
|
|
8470
|
+
```ts
|
|
8471
|
+
export function compareReliability(baseline: ReliabilityDocument, candidate: ReliabilityDocument, conditionsDiffer: readonly string[] = []): ReliabilityComparison
|
|
8472
|
+
```
|
|
8473
|
+
|
|
8474
|
+
Sets the reliability of a check up to the baseline run against its reliability up to the candidate run. A change is
|
|
8475
|
+
stated only when both samples are established (at least `minimumExecutions` independent executions), both counted the
|
|
8476
|
+
same recorded spec revision (see {@link reliabilityScopeAt}) and the conditions match; whether the difference exceeds the uncertainty is
|
|
8477
|
+
said by `direction`. Otherwise each figure stands alone and the reasons say why.
|
|
8478
|
+
|
|
8479
|
+
#### `StepLike`
|
|
8480
|
+
|
|
8481
|
+
```ts
|
|
8482
|
+
export type StepLike = { readonly name: string; readonly offsetMs?: number | undefined };
|
|
8483
|
+
```
|
|
8484
|
+
|
|
8485
|
+
The part of a recorded step this module reads.
|
|
8486
|
+
|
|
8487
|
+
#### `AlignedStep`
|
|
8488
|
+
|
|
8489
|
+
```ts
|
|
8490
|
+
export type AlignedStep = {
|
|
8491
|
+
readonly name: string;
|
|
8492
|
+
/** Milliseconds after the check began, on each side; absent when that side did not record it or its time. */
|
|
8493
|
+
readonly baselineOffsetMs?: number;
|
|
8494
|
+
readonly candidateOffsetMs?: number;
|
|
8495
|
+
readonly onBaseline: boolean;
|
|
8496
|
+
readonly onCandidate: boolean;
|
|
8497
|
+
};
|
|
8498
|
+
```
|
|
8499
|
+
|
|
8500
|
+
One step, found on one or both sides.
|
|
8501
|
+
|
|
8502
|
+
#### `StepAlignment`
|
|
8503
|
+
|
|
8504
|
+
```ts
|
|
8505
|
+
export type StepAlignment = {
|
|
8506
|
+
/** `aligned`: some step is on both sides with a time on both (a recording can be sought to it); `unaligned`: otherwise. */
|
|
8507
|
+
readonly state: 'aligned' | 'unaligned';
|
|
8508
|
+
readonly steps: readonly AlignedStep[];
|
|
8509
|
+
/** The steps with a time on both sides: where both recordings can be sought together. */
|
|
8510
|
+
readonly seekable: readonly (AlignedStep & { readonly baselineOffsetMs: number; readonly candidateOffsetMs: number })[];
|
|
8511
|
+
/** Whether the two lists of step names differ (the steps themselves are part of the promise). */
|
|
8512
|
+
readonly differs: boolean;
|
|
8513
|
+
/** In words, why the recordings are or are not aligned. */
|
|
8514
|
+
readonly explanation: string;
|
|
8515
|
+
};
|
|
8516
|
+
```
|
|
8517
|
+
|
|
8518
|
+
How the recorded steps of two results line up.
|
|
8519
|
+
|
|
8520
|
+
#### `alignSteps`
|
|
8521
|
+
|
|
8522
|
+
```ts
|
|
8523
|
+
export function alignSteps(baseline: readonly StepLike[], candidate: readonly StepLike[]): StepAlignment
|
|
8524
|
+
```
|
|
8525
|
+
|
|
8526
|
+
Lines up the recorded steps of two results by name (the n-th step of a name on one side with the n-th of that name on
|
|
8527
|
+
the other), keeping each side's own time origin: an offset is milliseconds after that side's check began. Nothing is
|
|
8528
|
+
aligned by position or by fuzzy wording. When nothing lines up, `explanation` says why.
|
|
8529
|
+
|
|
8165
8530
|
### S3 object store
|
|
8166
8531
|
|
|
8167
8532
|
Source: `packages/reports/src/s3-store.ts`. Import: `@crvouga/atlas/reports/s3-store`
|
|
@@ -9620,6 +9985,14 @@ export type RunResult = {
|
|
|
9620
9985
|
readonly detail: string;
|
|
9621
9986
|
/** Every video its evidence references, with who holds it (check, retry attempt or step). Absent when there is none. */
|
|
9622
9987
|
readonly videos?: readonly VideoRef[];
|
|
9988
|
+
/** The kind of the part that held it (`behavior`, `checks`…). With the producer, environment and parameters it is the condition the result was recorded under. Absent from an older server. */
|
|
9989
|
+
readonly kind?: string;
|
|
9990
|
+
/** The other choices that made it its own result (a device size, a locale…). Absent when there are none, and from an older server. */
|
|
9991
|
+
readonly parameters?: Readonly<Record<string, string>>;
|
|
9992
|
+
/** The fingerprint of its spec file the layer recorded: results with different ones were recorded against different revisions of the spec. Absent means not recorded, never "the same". */
|
|
9993
|
+
readonly specRevision?: string;
|
|
9994
|
+
/** The title this run's layer gave the check. `title` is the newest title the check has in the whole history, so a check that was reworded shows its newest wording in both runs; this is the wording as recorded. Absent from an older server. */
|
|
9995
|
+
readonly recordedTitle?: string;
|
|
9623
9996
|
};
|
|
9624
9997
|
```
|
|
9625
9998
|
|
package/docs/reference/server.md
CHANGED
|
@@ -31,6 +31,8 @@ export type AtlasServerOptions = {
|
|
|
31
31
|
/** The built viewer. Without it, only the API is served. */
|
|
32
32
|
readonly site?: string;
|
|
33
33
|
readonly outlineBranch?: string;
|
|
34
|
+
/** Root mappings `from>to` the catalog health report always applies (the `catalog.roots` setting), beside any `root` a request adds. */
|
|
35
|
+
readonly catalogRoots?: readonly string[];
|
|
34
36
|
readonly include?: (layer: LayerHeader) => boolean;
|
|
35
37
|
readonly refreshMs?: number;
|
|
36
38
|
readonly limits?: { readonly maxObjectMiB?: number; readonly maxJsonMiB?: number; readonly maxStreams?: number };
|
package/docs/server.md
CHANGED
|
@@ -202,6 +202,7 @@ is said in words and the server does not start. The file has a published
|
|
|
202
202
|
| Whose specs the report is laid over | `outlineBranch` | `--outline-branch <branch>` | `ATLAS_OUTLINE_BRANCH` | `main`, then `master`, then the newest |
|
|
203
203
|
| Only layers recorded on these branches take part | `include.branches` | `--branch <name>` (repeatable) | `ATLAS_ONLY_BRANCH` | all |
|
|
204
204
|
| Leave out layers from modified checkouts and unknown origins | `include.ignoreModified` | `--ignore-modified` | `ATLAS_IGNORE_MODIFIED` | no |
|
|
205
|
+
| Root mappings `from>to` that join spec paths a producer publishes to the paths the specs are recorded under, for the catalog health report (comma-separated; an empty `from` matches every path) | `catalog.roots` | | `ATLAS_CATALOG_ROOTS` | none |
|
|
205
206
|
| How often (seconds) the backends are looked at for layers written by something else | `refreshSeconds` | `--refresh <seconds>` | `ATLAS_REFRESH_SECONDS` | `10` |
|
|
206
207
|
| Let anyone look, not upload | `auth.anonymousRead` | `--anonymous-read` | `ATLAS_ANONYMOUS_READ` | no |
|
|
207
208
|
| Listen publicly with no tokens (something in front signs people in) | `auth.insecure` | `--insecure` | `ATLAS_INSECURE` | no |
|
|
@@ -442,8 +443,16 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
|
|
|
442
443
|
the Tests total. Two spec paths are one logical spec only when a mapping says so
|
|
443
444
|
(`?root=<from>><to>`, for example `?root=%3Especs/` to put `checkout/cart.feature` under `specs/`); similar paths
|
|
444
445
|
are reported as ambiguous and never merged, and a similar title is never an identity. Give a check the label
|
|
445
|
-
`outline` to say which scenario outline an example belongs to
|
|
446
|
-
|
|
446
|
+
`outline` to say which scenario outline an example belongs to, and `behavior-id` / `example-id` to give a behavior
|
|
447
|
+
or one example a stable id of your own: Atlas counts them and names an id that sits on more than one check (a
|
|
448
|
+
rename or a move), but a check is still its spec path and id, so nothing is joined by them. A domain shared by nearly
|
|
449
|
+
every spec is flagged, but explicit domain metadata always stays authoritative. The document only reads: it changes
|
|
450
|
+
no status.
|
|
451
|
+
- Mappings that should always apply are a setting, not a query parameter: `catalog.roots` in `atlas.server.json` (a list
|
|
452
|
+
of `from>to`) or `ATLAS_CATALOG_ROOTS` (comma-separated; it replaces the file's list). The health document applies
|
|
453
|
+
them, then adds the request's `?root=`, and says in `roots[].source` which is which. A mapping with no `>` stops the
|
|
454
|
+
server starting. The viewer's **Catalog health** tab shows the same document, and its mapping form (the `root`
|
|
455
|
+
address parameter) tries further mappings without changing the server.
|
|
447
456
|
- 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
|
|
448
457
|
arrive; it is rebuilt from the layers on restart. History never changes a result.
|
|
449
458
|
- Without a published spec set, tests are grouped under the specs their layers recorded (`specs[]`: path, domain,
|
package/docs/ui.md
CHANGED
|
@@ -96,6 +96,25 @@ when Watch is on screen.
|
|
|
96
96
|
- The spec index searches titles, paths and declared domains, and can group by the domain from spec metadata (specs with
|
|
97
97
|
none are listed under "No domain declared"; the Tests view reports domain problems).
|
|
98
98
|
|
|
99
|
+
## Catalog health
|
|
100
|
+
|
|
101
|
+
The **Catalog health** tab (`explorer/HealthView.tsx`, `?tab=health`; `@crvouga/atlas/reports/catalog-health`) explains why the Specs
|
|
102
|
+
and the Tests disagree. The Specs list links to it ("Why do these counts differ from the Tests?"). On a server it reads
|
|
103
|
+
`GET /api/reports/tests/health`; elsewhere the same document is worked out in the page from the specs and results it holds, and an
|
|
104
|
+
older server without the route is told so in words.
|
|
105
|
+
|
|
106
|
+
- Denominators are never one number: spec files, defined specs, specs with tests, specs executed and defined but not executed
|
|
107
|
+
(the coverage gaps, which add to no passed count), tests, outlines, examples and observations (an outline, its examples and
|
|
108
|
+
their per-environment results are told apart). Each figure has its meaning in its `title`.
|
|
109
|
+
- A table walks from the Specs total to the Tests total, step by step, with the sentences below it. Findings are an `Accordion`
|
|
110
|
+
(warnings open), each with its severity as text, its code, how many it affects and the paths. Paths that might be one promise
|
|
111
|
+
are shown as "may be the same promise as ...; not merged": a similar title never joins two specs.
|
|
112
|
+
- Root mappings (`from>to`) join spec paths. Those the server is configured with (`catalog.roots`, `ATLAS_CATALOG_ROOTS`) are
|
|
113
|
+
labelled "Configured on the server"; others are tried in the form and kept in the address (`root`), labelled "Tried here", and
|
|
114
|
+
change nothing on the server. A mapping that matched no path is a warning. Joined specs list every original path.
|
|
115
|
+
- Domains are listed as declared; a domain shared by nearly every spec is a finding, never rewritten. Identity states how many
|
|
116
|
+
checks are known only by title, how many declare `behavior-id` / `example-id`, and the limits of both.
|
|
117
|
+
|
|
99
118
|
## UI catalog
|
|
100
119
|
|
|
101
120
|
`/catalog` (`catalog/`) is the viewer's inventory of captured UI states ([the guide](ui-catalog.md)). It is computed in the page from
|
|
@@ -143,6 +162,35 @@ environment) is unknown unless the producer declared it under the `atlas.failure
|
|
|
143
162
|
wording. Records of one source whose outcome changed are listed oldest first, and a later pass is never called a fix.
|
|
144
163
|
In the explorer the chosen source is the `source` search parameter (a layer id; unlike `layer` it does not pin a run).
|
|
145
164
|
|
|
165
|
+
## Compare two runs
|
|
166
|
+
|
|
167
|
+
The **Compare** tab (`explorer/CompareView.tsx`, `explorer/ComparePair.tsx`, `@crvouga/atlas/reports/run-compare`) sets a
|
|
168
|
+
baseline run against a candidate run. Both are pinned in the address (`base` and `cand`, run keys; `change` is the group
|
|
169
|
+
shown and `pair` the behavior opened), so a link reproduces the comparison and a newer run never replaces either. A run
|
|
170
|
+
that is no longer in the report is said to be missing and is never swapped for another. A run's page offers "Compare with
|
|
171
|
+
this run as the baseline / candidate".
|
|
172
|
+
|
|
173
|
+
- Results are paired by the check they record (spec path and check id), then by the condition they were recorded under
|
|
174
|
+
(kind, producer, environment, parameters). A title never pairs two results. The same check under another condition is
|
|
175
|
+
paired and marked "Conditions differ".
|
|
176
|
+
- Every behavior is in one group: newly failing, recovered, still failing, promise or conditions differ, not run or
|
|
177
|
+
unfinished, not in the candidate run, not in the baseline run, same outcome. A result the candidate did not run is
|
|
178
|
+
unknown, never regressed or passed; one only the baseline has is "not in the candidate run" (not run, renamed or
|
|
179
|
+
dropped: the comparison cannot tell). A reworded behavior, or one recorded under different conditions, is not framed as
|
|
180
|
+
a regression or recovery: both outcomes are shown beside the changed promise. A different spec-file revision with the
|
|
181
|
+
same wording is disclosed beside the movement (a fingerprint is of the whole file); a revision that was not recorded is
|
|
182
|
+
unknown, never the same.
|
|
183
|
+
- Opening a behavior shows Mantine `Tabs`: **Promise** (wording, spec revision, recorded steps with the ones only one side
|
|
184
|
+
has), **Outcomes** (each side's own provenance, execution time and failure), **Recordings** (one `<video>` per side with
|
|
185
|
+
`preload="metadata"`, read only while that tab is on screen; each keeps its own attempt, spec revision and time
|
|
186
|
+
origin, they are not synchronized, and "Go to a step in both recordings" seeks each to its own recorded step time when
|
|
187
|
+
the steps are shared by name and timed; otherwise the page says why they are not aligned) and **Figures**.
|
|
188
|
+
- Figures are compared like for like. Product latency is compared only for the same measurement on the same devices,
|
|
189
|
+
environments and instrumentation, and reliability only when both sides count at least the minimum of independent
|
|
190
|
+
executions against the same recorded spec revision (and a difference inside the uncertainty is "cannot be told apart").
|
|
191
|
+
Otherwise the page says "Not compared" with the reasons and computes no change. Test execution time is labelled as such
|
|
192
|
+
and is never product latency.
|
|
193
|
+
|
|
146
194
|
## Watch a filtered set as a playlist
|
|
147
195
|
|
|
148
196
|
The Artifacts tab and an opened run have a **Watch these N results** button. It opens a Mantine `Modal` (`explorer/playlist.tsx`)
|
|
@@ -291,7 +339,7 @@ The narrowly scoped exceptions are:
|
|
|
291
339
|
| --- | --- | --- |
|
|
292
340
|
| Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
|
|
293
341
|
| UI catalog | Captured UI states per profile as cards with coverage badges, filters in the index column, surface coverage, an open state with its profiles side by side and where each picture came from, manifest download | AppShell navbar, SimpleGrid, Card, Image, MultiSelect, TextInput, Drawer, Accordion, Table, Badge, Alert |
|
|
294
|
-
| Project home | Tests/runs/artifacts tabs, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
|
|
342
|
+
| Project home | Tests/runs/artifacts/compare/catalog health tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
|
|
295
343
|
| Test detail | Current outcomes, statistics, duration graph, product latency with budgets and baselines, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
|
|
296
344
|
| Specs | All six existing kinds, overview/index with title search and folder or declared-domain grouping, Gherkin rules/scenarios/examples with a coverage summary and Read / Watch / Inspect per behavior, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Tabs, Table, CodeHighlightTabs, ResultRow |
|
|
297
345
|
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|