@crvouga/atlas 1.14.0 → 1.15.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 +8 -1
- 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 +4 -0
- package/dist/schema/src/api.js +7 -3
- package/docs/reference/reports.md +354 -3
- package/docs/ui.md +30 -1
- package/llms-full.txt +30 -1
- package/package.json +1 -1
- 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/test-catalog.schema.json +1 -1
- package/packages/schema/schemas/test-history.schema.json +2 -2
- package/packages/server/openapi.json +39 -17
- 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 +6 -2
- package/packages/visualizer/src/explorer/RunsView.tsx +10 -0
- package/packages/visualizer/src/explorer/playlist.tsx +1 -1
- package/packages/visualizer/src/explorer/search.ts +15 -3
|
@@ -1694,6 +1694,16 @@ Like {@link summarizeEvidence} for the checks of a part: a video that declares n
|
|
|
1694
1694
|
declares none, takes the scope `inherited` from its part or layer (the nearest declaration of the two, see
|
|
1695
1695
|
`effectiveScope`). Without `inherited` it is the same as `summarizeEvidence`.
|
|
1696
1696
|
|
|
1697
|
+
#### `summarizeStoredEvidence`
|
|
1698
|
+
|
|
1699
|
+
```ts
|
|
1700
|
+
export function summarizeStoredEvidence(inherited: EvidenceScope | undefined, unstored: ReadonlySet<string> | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
Like {@link summarizeEvidenceIn}, knowing which bundle keys the layer names but does not hold (`unstored`, see
|
|
1704
|
+
`unstoredKeys`): a video whose only bytes are such a key is `available: false`, never a recording. Without
|
|
1705
|
+
`unstored` it is the same as `summarizeEvidenceIn`.
|
|
1706
|
+
|
|
1697
1707
|
#### `summarizeLayerEvidence`
|
|
1698
1708
|
|
|
1699
1709
|
```ts
|
|
@@ -1703,6 +1713,14 @@ export function summarizeLayerEvidence(...holders: readonly EvidenceHolder[]): E
|
|
|
1703
1713
|
Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
|
|
1704
1714
|
part is owned by the `layer`, not by a check.
|
|
1705
1715
|
|
|
1716
|
+
#### `summarizeStoredLayerEvidence`
|
|
1717
|
+
|
|
1718
|
+
```ts
|
|
1719
|
+
export function summarizeStoredLayerEvidence(unstored: ReadonlySet<string> | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
Like {@link summarizeLayerEvidence}, knowing which bundle keys the layer names but does not hold (see {@link summarizeStoredEvidence}).
|
|
1723
|
+
|
|
1706
1724
|
#### `isVideoAttachment`
|
|
1707
1725
|
|
|
1708
1726
|
```ts
|
|
@@ -1804,6 +1822,16 @@ export function bundleKeysOf(holder: EvidenceHolder): string[]
|
|
|
1804
1822
|
|
|
1805
1823
|
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
1824
|
|
|
1825
|
+
#### `unstoredKeys`
|
|
1826
|
+
|
|
1827
|
+
```ts
|
|
1828
|
+
export function unstoredKeys(layer: EvidenceHolder & { readonly parts: readonly (EvidenceHolder & { readonly checks: readonly EvidenceHolder[] })[]; readonly bundle: Readonly<Record<string, unknown>> }): string[]
|
|
1829
|
+
```
|
|
1830
|
+
|
|
1831
|
+
The bundle keys that attachments of a layer, its parts and their checks name, but that the layer's `bundle` index does
|
|
1832
|
+
not hold: files the producer referenced and never stored. Empty for a layer whose every file is there. This reads only
|
|
1833
|
+
the index, never a blob, so it is cheap enough to ask of every layer as it is read.
|
|
1834
|
+
|
|
1807
1835
|
### Live evidence from running suites
|
|
1808
1836
|
|
|
1809
1837
|
Source: `packages/reports/src/evidence-live.ts`. Import: `@crvouga/atlas/reports/evidence-live`
|
|
@@ -2109,7 +2137,7 @@ export type VideoRef = {
|
|
|
2109
2137
|
readonly tags?: readonly string[];
|
|
2110
2138
|
/** What identifies the bytes (digest, bundle key or address); references with the same one are the same recording. Absent when `available` is false. */
|
|
2111
2139
|
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. */
|
|
2140
|
+
/** 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
2141
|
readonly available: boolean;
|
|
2114
2142
|
/** 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
2143
|
readonly scope?: EvidenceScope;
|
|
@@ -2128,7 +2156,7 @@ export type VideoCounts = {
|
|
|
2128
2156
|
readonly references: number;
|
|
2129
2157
|
/** Distinct playable recordings: identical bytes referenced twice are one. */
|
|
2130
2158
|
readonly recordings: number;
|
|
2131
|
-
/** References that name no bytes. A report that predates availability metadata has none of these and none of the others. */
|
|
2159
|
+
/** 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
2160
|
readonly unavailable: number;
|
|
2133
2161
|
};
|
|
2134
2162
|
```
|
|
@@ -4870,7 +4898,7 @@ Reads one captured file (a screenshot, a frame, a recorded report) of a layer, o
|
|
|
4870
4898
|
export function layerSummary(layer: ReportLayer): LayerSummary
|
|
4871
4899
|
```
|
|
4872
4900
|
|
|
4873
|
-
Drops a layer's bundle index, leaving the summary composition reads.
|
|
4901
|
+
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
4902
|
|
|
4875
4903
|
### Layer types
|
|
4876
4904
|
|
|
@@ -4931,6 +4959,8 @@ export type LayerSummary = LayerExtras & {
|
|
|
4931
4959
|
readonly source: SourceRecord | null;
|
|
4932
4960
|
/** 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
4961
|
readonly recording: { readonly type: 'layer' } | { readonly type: 'commit'; readonly sha: string; readonly source: string } | null;
|
|
4962
|
+
/** 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. */
|
|
4963
|
+
readonly unstored?: readonly string[];
|
|
4934
4964
|
};
|
|
4935
4965
|
```
|
|
4936
4966
|
|
|
@@ -8162,6 +8192,319 @@ export async function readRunFile(store: ObjectStore, id: string, relative: stri
|
|
|
8162
8192
|
|
|
8163
8193
|
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
8194
|
|
|
8195
|
+
### Run comparison
|
|
8196
|
+
|
|
8197
|
+
Source: `packages/reports/src/run-compare.ts`. Import: `@crvouga/atlas/reports/run-compare`
|
|
8198
|
+
|
|
8199
|
+
Answers "what changed between these two runs?" without claiming more than the evidence supports. A run is a pinned
|
|
8200
|
+
source: the comparison reads two {@link RunDetail}s as they are and never reaches for a newer run. Pure functions,
|
|
8201
|
+
safe in a browser; nothing here changes a status, a slot or a winner.
|
|
8202
|
+
|
|
8203
|
+
The rules, each of which keeps a comparison from looking more certain than it is:
|
|
8204
|
+
|
|
8205
|
+
- **Identity is explicit.** Results are paired by the check they record (`TestSummary.key`: spec path and check id),
|
|
8206
|
+
then by the condition they were recorded under (kind, producer, environment, parameters). A title is never what
|
|
8207
|
+
pairs two results. A result with the same check but another condition is paired as well, marked as recorded under
|
|
8208
|
+
different conditions, and is not framed as the same experiment. Results of one side with no partner are `added`
|
|
8209
|
+
(only the candidate has them) or `removed` (only the baseline has them); a `removed` result is not a regression,
|
|
8210
|
+
because the candidate may not have run, renamed or dropped it, and the comparison cannot tell which.
|
|
8211
|
+
- **Not run is not regressed.** A result that did not run, or is still running, on either side is `unknown`; it is
|
|
8212
|
+
counted as neither a pass nor a failure. Only a completed result against a completed result is a movement.
|
|
8213
|
+
- **A changed promise is not the same experiment.** When the wording of a behavior differs between the two sides, the
|
|
8214
|
+
pair is `not-comparable` and both outcomes are shown beside the changed promise instead of as a regression or a
|
|
8215
|
+
recovery. A different spec-file revision with the same wording is disclosed beside the movement: a fingerprint is
|
|
8216
|
+
of the whole file, so it cannot say that this behavior's own promise changed. A revision that was not recorded is
|
|
8217
|
+
unknown, never the same.
|
|
8218
|
+
- **Figures are compared like for like.** Product latency and reliability are compared only when both sides were
|
|
8219
|
+
recorded under the same conditions and support the figure; otherwise the reason is given and no improvement is
|
|
8220
|
+
computed. A metric only one side has is named, not compared.
|
|
8221
|
+
|
|
8222
|
+
See docs/ui.md
|
|
8223
|
+
|
|
8224
|
+
#### `COMPARE_GROUPS`
|
|
8225
|
+
|
|
8226
|
+
```ts
|
|
8227
|
+
export const COMPARE_GROUPS = …
|
|
8228
|
+
```
|
|
8229
|
+
|
|
8230
|
+
Where a pair of results (or one result with no partner) stands, in the order a review reads them.
|
|
8231
|
+
|
|
8232
|
+
#### `CompareGroup`
|
|
8233
|
+
|
|
8234
|
+
```ts
|
|
8235
|
+
export type CompareGroup = (typeof COMPARE_GROUPS)[number];
|
|
8236
|
+
```
|
|
8237
|
+
|
|
8238
|
+
One of {@link COMPARE_GROUPS}.
|
|
8239
|
+
|
|
8240
|
+
#### `COMPARE_GROUP_TEXT`
|
|
8241
|
+
|
|
8242
|
+
```ts
|
|
8243
|
+
export const COMPARE_GROUP_TEXT: Readonly<Record<CompareGroup, { readonly label: string; readonly meaning: string }>>
|
|
8244
|
+
```
|
|
8245
|
+
|
|
8246
|
+
What each group is called and what it does and does not say.
|
|
8247
|
+
|
|
8248
|
+
#### `PromiseState`
|
|
8249
|
+
|
|
8250
|
+
```ts
|
|
8251
|
+
export type PromiseState = 'same' | 'changed' | 'revision-differs' | 'unknown';
|
|
8252
|
+
```
|
|
8253
|
+
|
|
8254
|
+
Whether the promise (what the behavior says should happen) is the same on both sides: `same` (same wording and the
|
|
8255
|
+
same recorded spec revision), `changed` (the wording differs), `revision-differs` (same wording, another spec-file
|
|
8256
|
+
revision), or `unknown` (a revision was not recorded on a side, so nothing can be said).
|
|
8257
|
+
|
|
8258
|
+
#### `PromiseReading`
|
|
8259
|
+
|
|
8260
|
+
```ts
|
|
8261
|
+
export type PromiseReading = {
|
|
8262
|
+
readonly state: PromiseState;
|
|
8263
|
+
readonly baselineTitle: string | null;
|
|
8264
|
+
readonly candidateTitle: string | null;
|
|
8265
|
+
readonly baselineRevision: string | null;
|
|
8266
|
+
readonly candidateRevision: string | null;
|
|
8267
|
+
/** In words, for a person. */
|
|
8268
|
+
readonly reasons: readonly string[];
|
|
8269
|
+
};
|
|
8270
|
+
```
|
|
8271
|
+
|
|
8272
|
+
What is known about whether the promise changed, and why.
|
|
8273
|
+
|
|
8274
|
+
#### `Condition`
|
|
8275
|
+
|
|
8276
|
+
```ts
|
|
8277
|
+
export type Condition = {
|
|
8278
|
+
readonly kind: string | null;
|
|
8279
|
+
readonly producer: string;
|
|
8280
|
+
readonly environment: string | null;
|
|
8281
|
+
readonly parameters: Readonly<Record<string, string>>;
|
|
8282
|
+
};
|
|
8283
|
+
```
|
|
8284
|
+
|
|
8285
|
+
The condition a result was recorded under: with the check, what makes it its own result.
|
|
8286
|
+
|
|
8287
|
+
#### `conditionOf`
|
|
8288
|
+
|
|
8289
|
+
```ts
|
|
8290
|
+
export function conditionOf(result: Pick<RunResult, 'kind' | 'producer' | 'environment' | 'parameters'>): Condition
|
|
8291
|
+
```
|
|
8292
|
+
|
|
8293
|
+
The condition of a result. A server that does not send `kind` leaves it null: unknown, equal only to another unknown.
|
|
8294
|
+
|
|
8295
|
+
#### `conditionId`
|
|
8296
|
+
|
|
8297
|
+
```ts
|
|
8298
|
+
export function conditionId(condition: Condition): string
|
|
8299
|
+
```
|
|
8300
|
+
|
|
8301
|
+
A stable text for a condition, equal exactly when two conditions are.
|
|
8302
|
+
|
|
8303
|
+
#### `conditionDifferences`
|
|
8304
|
+
|
|
8305
|
+
```ts
|
|
8306
|
+
export function conditionDifferences(baseline: Condition, candidate: Condition): string[]
|
|
8307
|
+
```
|
|
8308
|
+
|
|
8309
|
+
How two conditions differ, in words; empty when they are the same.
|
|
8310
|
+
|
|
8311
|
+
#### `ComparedResult`
|
|
8312
|
+
|
|
8313
|
+
```ts
|
|
8314
|
+
export type ComparedResult = {
|
|
8315
|
+
/** Identifies the pair within one comparison: the check, and the condition of each side. Stable for the same two runs. */
|
|
8316
|
+
readonly id: string;
|
|
8317
|
+
/** The check's identity (`TestSummary.key`). */
|
|
8318
|
+
readonly key: string;
|
|
8319
|
+
/** The check's title: the candidate's when it has one, else the baseline's. */
|
|
8320
|
+
readonly title: string;
|
|
8321
|
+
readonly specPath: string | null;
|
|
8322
|
+
readonly baseline: RunResult | null;
|
|
8323
|
+
readonly candidate: RunResult | null;
|
|
8324
|
+
readonly group: CompareGroup;
|
|
8325
|
+
/** One sentence saying what happened, and what is not known. */
|
|
8326
|
+
readonly summary: string;
|
|
8327
|
+
readonly promise: PromiseReading;
|
|
8328
|
+
/** How the two conditions differ, in words, when both sides have a result and they were recorded under different conditions. */
|
|
8329
|
+
readonly conditions: readonly string[];
|
|
8330
|
+
/** What a reader must not assume about this pair, in words. */
|
|
8331
|
+
readonly caveats: readonly string[];
|
|
8332
|
+
};
|
|
8333
|
+
```
|
|
8334
|
+
|
|
8335
|
+
One behavior's results on the two sides.
|
|
8336
|
+
|
|
8337
|
+
#### `RunComparison`
|
|
8338
|
+
|
|
8339
|
+
```ts
|
|
8340
|
+
export type RunComparison = {
|
|
8341
|
+
readonly baseline: RunSummary;
|
|
8342
|
+
readonly candidate: RunSummary;
|
|
8343
|
+
/** Every pair and every unpaired result, in the order of {@link COMPARE_GROUPS}, then spec and title. */
|
|
8344
|
+
readonly results: readonly ComparedResult[];
|
|
8345
|
+
readonly counts: Readonly<Record<CompareGroup, number>>;
|
|
8346
|
+
/** 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. */
|
|
8347
|
+
readonly notes: readonly string[];
|
|
8348
|
+
};
|
|
8349
|
+
```
|
|
8350
|
+
|
|
8351
|
+
The whole comparison of two pinned runs.
|
|
8352
|
+
|
|
8353
|
+
#### `shortRevision`
|
|
8354
|
+
|
|
8355
|
+
```ts
|
|
8356
|
+
export function shortRevision(revision: string | null | undefined): string
|
|
8357
|
+
```
|
|
8358
|
+
|
|
8359
|
+
Short form of a spec fingerprint, to show.
|
|
8360
|
+
|
|
8361
|
+
#### `recordedTitle`
|
|
8362
|
+
|
|
8363
|
+
```ts
|
|
8364
|
+
export function recordedTitle(result: Pick<RunResult, 'title' | 'recordedTitle'>): string
|
|
8365
|
+
```
|
|
8366
|
+
|
|
8367
|
+
The wording a run recorded for a check: what that run's layer called it, else the check's newest title.
|
|
8368
|
+
|
|
8369
|
+
#### `compareRuns`
|
|
8370
|
+
|
|
8371
|
+
```ts
|
|
8372
|
+
export function compareRuns(baseline: RunDetail, candidate: RunDetail): RunComparison
|
|
8373
|
+
```
|
|
8374
|
+
|
|
8375
|
+
Pairs the results of two runs and reads what changed. Results are paired by check identity and then by condition (see
|
|
8376
|
+
the module comment); titles never pair two results. Each side's results are those the run recorded last for each
|
|
8377
|
+
check and condition (`RunDetail.results`).
|
|
8378
|
+
|
|
8379
|
+
- `baseline`: The run to compare against.
|
|
8380
|
+
|
|
8381
|
+
- `candidate`: The run under review. Both are used as given: a newer run never replaces either.
|
|
8382
|
+
|
|
8383
|
+
**Returns** Every result of either run, grouped by what changed, with the reasons.
|
|
8384
|
+
|
|
8385
|
+
**Example**
|
|
8386
|
+
|
|
8387
|
+
```ts
|
|
8388
|
+
const comparison = compareRuns(await runDetail('run:ci-100'), await runDetail('run:ci-101'));
|
|
8389
|
+
comparison.results.filter(item => item.group === 'regressed');
|
|
8390
|
+
```
|
|
8391
|
+
|
|
8392
|
+
#### `MetricComparison`
|
|
8393
|
+
|
|
8394
|
+
```ts
|
|
8395
|
+
export type MetricComparison =
|
|
8396
|
+
| { readonly state: 'compared'; readonly name: string; readonly baseline: LatencySummary; readonly candidate: LatencySummary; readonly change: LatencyChange; readonly reasons: readonly string[] }
|
|
8397
|
+
| { readonly state: 'incompatible'; readonly name: string; readonly baseline: LatencySummary; readonly candidate: LatencySummary; readonly change: LatencyChange | null; readonly reasons: readonly string[] }
|
|
8398
|
+
| { readonly state: 'baseline-only' | 'candidate-only'; readonly name: string; readonly summary: LatencySummary; readonly reasons: readonly string[] };
|
|
8399
|
+
```
|
|
8400
|
+
|
|
8401
|
+
What one product-latency metric shows on the two sides.
|
|
8402
|
+
|
|
8403
|
+
#### `compareProductLatency`
|
|
8404
|
+
|
|
8405
|
+
```ts
|
|
8406
|
+
export function compareProductLatency(baseline: readonly LatencyEntry[], candidate: readonly LatencyEntry[], conditionsDiffer: readonly string[] = []): MetricComparison[]
|
|
8407
|
+
```
|
|
8408
|
+
|
|
8409
|
+
Compares the product latency recorded in two sets of results of one check, one per side. A metric is compared only
|
|
8410
|
+
when it is the same measurement (name, unit, boundaries) and both sides were measured on the same devices, in the
|
|
8411
|
+
same environments, with the same instrumentation and other conditions; otherwise it is `incompatible` and no change
|
|
8412
|
+
is computed, with the reasons. A metric only one side recorded is named as such. A side without any latency is unknown,
|
|
8413
|
+
not fast: it contributes no metric. `conditionsDiffer` names differences of the results' own conditions, which make
|
|
8414
|
+
every metric incompatible.
|
|
8415
|
+
|
|
8416
|
+
#### `ReliabilityComparison`
|
|
8417
|
+
|
|
8418
|
+
```ts
|
|
8419
|
+
export type ReliabilityComparison = {
|
|
8420
|
+
/** True only when both figures are established, were counted in the same scope and against the same recorded spec revision. */
|
|
8421
|
+
readonly comparable: boolean;
|
|
8422
|
+
readonly baseline: ReliabilityRate;
|
|
8423
|
+
readonly candidate: ReliabilityRate;
|
|
8424
|
+
/** Candidate minus baseline, in percentage points, only when comparable. */
|
|
8425
|
+
readonly points?: number;
|
|
8426
|
+
/** `higher` or `lower` only when the two intervals do not overlap; `indistinguishable` when they do. Absent when not comparable. */
|
|
8427
|
+
readonly direction?: 'higher' | 'lower' | 'indistinguishable';
|
|
8428
|
+
/** Why no change is stated, or what qualifies one. */
|
|
8429
|
+
readonly reasons: readonly string[];
|
|
8430
|
+
};
|
|
8431
|
+
```
|
|
8432
|
+
|
|
8433
|
+
How reliable a check was on each side, and whether the two figures can be set against each other.
|
|
8434
|
+
|
|
8435
|
+
#### `reliabilityScopeAt`
|
|
8436
|
+
|
|
8437
|
+
```ts
|
|
8438
|
+
export function reliabilityScopeAt(result: Pick<RunResult, 'environment' | 'producer' | 'parameters' | 'specRevision'>, run: Pick<RunSummary, 'endedAtMs'>): ReliabilityScope
|
|
8439
|
+
```
|
|
8440
|
+
|
|
8441
|
+
The scope that counts the independent executions of one result's check up to the end of its run: the same
|
|
8442
|
+
environment, producer and parameters, and the spec revision that result was recorded against (executions against
|
|
8443
|
+
another revision are other promises). A result that recorded no revision is counted across revisions, which
|
|
8444
|
+
{@link compareReliability} will not compare. Use it for each side, with that side's result and run.
|
|
8445
|
+
|
|
8446
|
+
#### `compareReliability`
|
|
8447
|
+
|
|
8448
|
+
```ts
|
|
8449
|
+
export function compareReliability(baseline: ReliabilityDocument, candidate: ReliabilityDocument, conditionsDiffer: readonly string[] = []): ReliabilityComparison
|
|
8450
|
+
```
|
|
8451
|
+
|
|
8452
|
+
Sets the reliability of a check up to the baseline run against its reliability up to the candidate run. A change is
|
|
8453
|
+
stated only when both samples are established (at least `minimumExecutions` independent executions), both counted the
|
|
8454
|
+
same recorded spec revision (see {@link reliabilityScopeAt}) and the conditions match; whether the difference exceeds the uncertainty is
|
|
8455
|
+
said by `direction`. Otherwise each figure stands alone and the reasons say why.
|
|
8456
|
+
|
|
8457
|
+
#### `StepLike`
|
|
8458
|
+
|
|
8459
|
+
```ts
|
|
8460
|
+
export type StepLike = { readonly name: string; readonly offsetMs?: number | undefined };
|
|
8461
|
+
```
|
|
8462
|
+
|
|
8463
|
+
The part of a recorded step this module reads.
|
|
8464
|
+
|
|
8465
|
+
#### `AlignedStep`
|
|
8466
|
+
|
|
8467
|
+
```ts
|
|
8468
|
+
export type AlignedStep = {
|
|
8469
|
+
readonly name: string;
|
|
8470
|
+
/** Milliseconds after the check began, on each side; absent when that side did not record it or its time. */
|
|
8471
|
+
readonly baselineOffsetMs?: number;
|
|
8472
|
+
readonly candidateOffsetMs?: number;
|
|
8473
|
+
readonly onBaseline: boolean;
|
|
8474
|
+
readonly onCandidate: boolean;
|
|
8475
|
+
};
|
|
8476
|
+
```
|
|
8477
|
+
|
|
8478
|
+
One step, found on one or both sides.
|
|
8479
|
+
|
|
8480
|
+
#### `StepAlignment`
|
|
8481
|
+
|
|
8482
|
+
```ts
|
|
8483
|
+
export type StepAlignment = {
|
|
8484
|
+
/** `aligned`: some step is on both sides with a time on both (a recording can be sought to it); `unaligned`: otherwise. */
|
|
8485
|
+
readonly state: 'aligned' | 'unaligned';
|
|
8486
|
+
readonly steps: readonly AlignedStep[];
|
|
8487
|
+
/** The steps with a time on both sides: where both recordings can be sought together. */
|
|
8488
|
+
readonly seekable: readonly (AlignedStep & { readonly baselineOffsetMs: number; readonly candidateOffsetMs: number })[];
|
|
8489
|
+
/** Whether the two lists of step names differ (the steps themselves are part of the promise). */
|
|
8490
|
+
readonly differs: boolean;
|
|
8491
|
+
/** In words, why the recordings are or are not aligned. */
|
|
8492
|
+
readonly explanation: string;
|
|
8493
|
+
};
|
|
8494
|
+
```
|
|
8495
|
+
|
|
8496
|
+
How the recorded steps of two results line up.
|
|
8497
|
+
|
|
8498
|
+
#### `alignSteps`
|
|
8499
|
+
|
|
8500
|
+
```ts
|
|
8501
|
+
export function alignSteps(baseline: readonly StepLike[], candidate: readonly StepLike[]): StepAlignment
|
|
8502
|
+
```
|
|
8503
|
+
|
|
8504
|
+
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
|
|
8505
|
+
the other), keeping each side's own time origin: an offset is milliseconds after that side's check began. Nothing is
|
|
8506
|
+
aligned by position or by fuzzy wording. When nothing lines up, `explanation` says why.
|
|
8507
|
+
|
|
8165
8508
|
### S3 object store
|
|
8166
8509
|
|
|
8167
8510
|
Source: `packages/reports/src/s3-store.ts`. Import: `@crvouga/atlas/reports/s3-store`
|
|
@@ -9620,6 +9963,14 @@ export type RunResult = {
|
|
|
9620
9963
|
readonly detail: string;
|
|
9621
9964
|
/** Every video its evidence references, with who holds it (check, retry attempt or step). Absent when there is none. */
|
|
9622
9965
|
readonly videos?: readonly VideoRef[];
|
|
9966
|
+
/** 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. */
|
|
9967
|
+
readonly kind?: string;
|
|
9968
|
+
/** The other choices that made it its own result (a device size, a locale…). Absent when there are none, and from an older server. */
|
|
9969
|
+
readonly parameters?: Readonly<Record<string, string>>;
|
|
9970
|
+
/** 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". */
|
|
9971
|
+
readonly specRevision?: string;
|
|
9972
|
+
/** 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. */
|
|
9973
|
+
readonly recordedTitle?: string;
|
|
9623
9974
|
};
|
|
9624
9975
|
```
|
|
9625
9976
|
|
package/docs/ui.md
CHANGED
|
@@ -143,6 +143,35 @@ environment) is unknown unless the producer declared it under the `atlas.failure
|
|
|
143
143
|
wording. Records of one source whose outcome changed are listed oldest first, and a later pass is never called a fix.
|
|
144
144
|
In the explorer the chosen source is the `source` search parameter (a layer id; unlike `layer` it does not pin a run).
|
|
145
145
|
|
|
146
|
+
## Compare two runs
|
|
147
|
+
|
|
148
|
+
The **Compare** tab (`explorer/CompareView.tsx`, `explorer/ComparePair.tsx`, `@crvouga/atlas/reports/run-compare`) sets a
|
|
149
|
+
baseline run against a candidate run. Both are pinned in the address (`base` and `cand`, run keys; `change` is the group
|
|
150
|
+
shown and `pair` the behavior opened), so a link reproduces the comparison and a newer run never replaces either. A run
|
|
151
|
+
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
|
|
152
|
+
this run as the baseline / candidate".
|
|
153
|
+
|
|
154
|
+
- Results are paired by the check they record (spec path and check id), then by the condition they were recorded under
|
|
155
|
+
(kind, producer, environment, parameters). A title never pairs two results. The same check under another condition is
|
|
156
|
+
paired and marked "Conditions differ".
|
|
157
|
+
- Every behavior is in one group: newly failing, recovered, still failing, promise or conditions differ, not run or
|
|
158
|
+
unfinished, not in the candidate run, not in the baseline run, same outcome. A result the candidate did not run is
|
|
159
|
+
unknown, never regressed or passed; one only the baseline has is "not in the candidate run" (not run, renamed or
|
|
160
|
+
dropped: the comparison cannot tell). A reworded behavior, or one recorded under different conditions, is not framed as
|
|
161
|
+
a regression or recovery: both outcomes are shown beside the changed promise. A different spec-file revision with the
|
|
162
|
+
same wording is disclosed beside the movement (a fingerprint is of the whole file); a revision that was not recorded is
|
|
163
|
+
unknown, never the same.
|
|
164
|
+
- Opening a behavior shows Mantine `Tabs`: **Promise** (wording, spec revision, recorded steps with the ones only one side
|
|
165
|
+
has), **Outcomes** (each side's own provenance, execution time and failure), **Recordings** (one `<video>` per side with
|
|
166
|
+
`preload="metadata"`, read only while that tab is on screen; each keeps its own attempt, spec revision and time
|
|
167
|
+
origin, they are not synchronized, and "Go to a step in both recordings" seeks each to its own recorded step time when
|
|
168
|
+
the steps are shared by name and timed; otherwise the page says why they are not aligned) and **Figures**.
|
|
169
|
+
- Figures are compared like for like. Product latency is compared only for the same measurement on the same devices,
|
|
170
|
+
environments and instrumentation, and reliability only when both sides count at least the minimum of independent
|
|
171
|
+
executions against the same recorded spec revision (and a difference inside the uncertainty is "cannot be told apart").
|
|
172
|
+
Otherwise the page says "Not compared" with the reasons and computes no change. Test execution time is labelled as such
|
|
173
|
+
and is never product latency.
|
|
174
|
+
|
|
146
175
|
## Watch a filtered set as a playlist
|
|
147
176
|
|
|
148
177
|
The Artifacts tab and an opened run have a **Watch these N results** button. It opens a Mantine `Modal` (`explorer/playlist.tsx`)
|
|
@@ -291,7 +320,7 @@ The narrowly scoped exceptions are:
|
|
|
291
320
|
| --- | --- | --- |
|
|
292
321
|
| Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
|
|
293
322
|
| 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 |
|
|
323
|
+
| Project home | Tests/runs/artifacts/compare 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
324
|
| 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
325
|
| 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
326
|
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|
package/llms-full.txt
CHANGED
|
@@ -4562,6 +4562,35 @@ environment) is unknown unless the producer declared it under the `atlas.failure
|
|
|
4562
4562
|
wording. Records of one source whose outcome changed are listed oldest first, and a later pass is never called a fix.
|
|
4563
4563
|
In the explorer the chosen source is the `source` search parameter (a layer id; unlike `layer` it does not pin a run).
|
|
4564
4564
|
|
|
4565
|
+
## Compare two runs
|
|
4566
|
+
|
|
4567
|
+
The **Compare** tab (`explorer/CompareView.tsx`, `explorer/ComparePair.tsx`, `@crvouga/atlas/reports/run-compare`) sets a
|
|
4568
|
+
baseline run against a candidate run. Both are pinned in the address (`base` and `cand`, run keys; `change` is the group
|
|
4569
|
+
shown and `pair` the behavior opened), so a link reproduces the comparison and a newer run never replaces either. A run
|
|
4570
|
+
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
|
|
4571
|
+
this run as the baseline / candidate".
|
|
4572
|
+
|
|
4573
|
+
- Results are paired by the check they record (spec path and check id), then by the condition they were recorded under
|
|
4574
|
+
(kind, producer, environment, parameters). A title never pairs two results. The same check under another condition is
|
|
4575
|
+
paired and marked "Conditions differ".
|
|
4576
|
+
- Every behavior is in one group: newly failing, recovered, still failing, promise or conditions differ, not run or
|
|
4577
|
+
unfinished, not in the candidate run, not in the baseline run, same outcome. A result the candidate did not run is
|
|
4578
|
+
unknown, never regressed or passed; one only the baseline has is "not in the candidate run" (not run, renamed or
|
|
4579
|
+
dropped: the comparison cannot tell). A reworded behavior, or one recorded under different conditions, is not framed as
|
|
4580
|
+
a regression or recovery: both outcomes are shown beside the changed promise. A different spec-file revision with the
|
|
4581
|
+
same wording is disclosed beside the movement (a fingerprint is of the whole file); a revision that was not recorded is
|
|
4582
|
+
unknown, never the same.
|
|
4583
|
+
- Opening a behavior shows Mantine `Tabs`: **Promise** (wording, spec revision, recorded steps with the ones only one side
|
|
4584
|
+
has), **Outcomes** (each side's own provenance, execution time and failure), **Recordings** (one `<video>` per side with
|
|
4585
|
+
`preload="metadata"`, read only while that tab is on screen; each keeps its own attempt, spec revision and time
|
|
4586
|
+
origin, they are not synchronized, and "Go to a step in both recordings" seeks each to its own recorded step time when
|
|
4587
|
+
the steps are shared by name and timed; otherwise the page says why they are not aligned) and **Figures**.
|
|
4588
|
+
- Figures are compared like for like. Product latency is compared only for the same measurement on the same devices,
|
|
4589
|
+
environments and instrumentation, and reliability only when both sides count at least the minimum of independent
|
|
4590
|
+
executions against the same recorded spec revision (and a difference inside the uncertainty is "cannot be told apart").
|
|
4591
|
+
Otherwise the page says "Not compared" with the reasons and computes no change. Test execution time is labelled as such
|
|
4592
|
+
and is never product latency.
|
|
4593
|
+
|
|
4565
4594
|
## Watch a filtered set as a playlist
|
|
4566
4595
|
|
|
4567
4596
|
The Artifacts tab and an opened run have a **Watch these N results** button. It opens a Mantine `Modal` (`explorer/playlist.tsx`)
|
|
@@ -4710,7 +4739,7 @@ The narrowly scoped exceptions are:
|
|
|
4710
4739
|
| --- | --- | --- |
|
|
4711
4740
|
| Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
|
|
4712
4741
|
| 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 |
|
|
4713
|
-
| 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 |
|
|
4742
|
+
| Project home | Tests/runs/artifacts/compare 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 |
|
|
4714
4743
|
| 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 |
|
|
4715
4744
|
| 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 |
|
|
4716
4745
|
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crvouga/atlas",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Atlas: specs of any kind, reported as layered evidence. Publish checks from any runner (JUnit, CTRF, a script), combine them across nodes, branches, environments and backends, gate CI on the result, and run statecharts against the real app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -262,7 +262,7 @@
|
|
|
262
262
|
},
|
|
263
263
|
"available": {
|
|
264
264
|
"type": "boolean",
|
|
265
|
-
"description": "False when the reference names no bytes
|
|
265
|
+
"description": "False when the reference names no bytes: no bundle key, address or inline text, or a bundle key that the layer's own bundle does not hold"
|
|
266
266
|
},
|
|
267
267
|
"scope": {
|
|
268
268
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
@@ -741,7 +741,7 @@
|
|
|
741
741
|
},
|
|
742
742
|
"available": {
|
|
743
743
|
"type": "boolean",
|
|
744
|
-
"description": "False when the reference names no bytes
|
|
744
|
+
"description": "False when the reference names no bytes: no bundle key, address or inline text, or a bundle key that the layer's own bundle does not hold"
|
|
745
745
|
},
|
|
746
746
|
"scope": {
|
|
747
747
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
@@ -262,7 +262,7 @@
|
|
|
262
262
|
},
|
|
263
263
|
"available": {
|
|
264
264
|
"type": "boolean",
|
|
265
|
-
"description": "False when the reference names no bytes
|
|
265
|
+
"description": "False when the reference names no bytes: no bundle key, address or inline text, or a bundle key that the layer's own bundle does not hold"
|
|
266
266
|
},
|
|
267
267
|
"scope": {
|
|
268
268
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
@@ -741,7 +741,7 @@
|
|
|
741
741
|
},
|
|
742
742
|
"available": {
|
|
743
743
|
"type": "boolean",
|
|
744
|
-
"description": "False when the reference names no bytes
|
|
744
|
+
"description": "False when the reference names no bytes: no bundle key, address or inline text, or a bundle key that the layer's own bundle does not hold"
|
|
745
745
|
},
|
|
746
746
|
"scope": {
|
|
747
747
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
@@ -249,7 +249,7 @@
|
|
|
249
249
|
},
|
|
250
250
|
"available": {
|
|
251
251
|
"type": "boolean",
|
|
252
|
-
"description": "False when the reference names no bytes
|
|
252
|
+
"description": "False when the reference names no bytes: no bundle key, address or inline text, or a bundle key that the layer's own bundle does not hold"
|
|
253
253
|
},
|
|
254
254
|
"scope": {
|
|
255
255
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
@@ -253,7 +253,7 @@
|
|
|
253
253
|
},
|
|
254
254
|
"available": {
|
|
255
255
|
"type": "boolean",
|
|
256
|
-
"description": "False when the reference names no bytes
|
|
256
|
+
"description": "False when the reference names no bytes: no bundle key, address or inline text, or a bundle key that the layer's own bundle does not hold"
|
|
257
257
|
},
|
|
258
258
|
"scope": {
|
|
259
259
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"type": "integer",
|
|
162
162
|
"minimum": 0,
|
|
163
163
|
"maximum": 9007199254740991,
|
|
164
|
-
"description": "References that name no bytes"
|
|
164
|
+
"description": "References that name no bytes, or bytes the layer does not hold"
|
|
165
165
|
}
|
|
166
166
|
},
|
|
167
167
|
"required": [
|
|
@@ -314,7 +314,7 @@
|
|
|
314
314
|
},
|
|
315
315
|
"available": {
|
|
316
316
|
"type": "boolean",
|
|
317
|
-
"description": "False when the reference names no bytes
|
|
317
|
+
"description": "False when the reference names no bytes: no bundle key, address or inline text, or a bundle key that the layer's own bundle does not hold"
|
|
318
318
|
},
|
|
319
319
|
"scope": {
|
|
320
320
|
"description": "What this recording shows as its producer declared it: its own `scope`, else its attempt's, check's, part's or layer's. Absent: the scope is unknown, never live, physical or audible",
|
|
@@ -420,6 +420,28 @@
|
|
|
420
420
|
],
|
|
421
421
|
"additionalProperties": {}
|
|
422
422
|
}
|
|
423
|
+
},
|
|
424
|
+
"kind": {
|
|
425
|
+
"description": "The kind of the part that held it. With the producer, environment and parameters it is the condition the result was recorded under. Absent from an older server",
|
|
426
|
+
"type": "string"
|
|
427
|
+
},
|
|
428
|
+
"parameters": {
|
|
429
|
+
"description": "The other choices that made it its own result (a device size, a locale…). Absent when there are none, and from an older server",
|
|
430
|
+
"type": "object",
|
|
431
|
+
"propertyNames": {
|
|
432
|
+
"type": "string"
|
|
433
|
+
},
|
|
434
|
+
"additionalProperties": {
|
|
435
|
+
"type": "string"
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
"specRevision": {
|
|
439
|
+
"description": "The fingerprint of its spec file the layer recorded. Absent means not recorded, never \"the same\"",
|
|
440
|
+
"type": "string"
|
|
441
|
+
},
|
|
442
|
+
"recordedTitle": {
|
|
443
|
+
"description": "The title this run's layer gave the check. `title` is the check's newest title in the whole history; this is the wording as recorded. Absent from an older server",
|
|
444
|
+
"type": "string"
|
|
423
445
|
}
|
|
424
446
|
},
|
|
425
447
|
"required": [
|
|
@@ -494,7 +516,7 @@
|
|
|
494
516
|
"type": "integer",
|
|
495
517
|
"minimum": 0,
|
|
496
518
|
"maximum": 9007199254740991,
|
|
497
|
-
"description": "References that name no bytes"
|
|
519
|
+
"description": "References that name no bytes, or bytes the layer does not hold"
|
|
498
520
|
}
|
|
499
521
|
},
|
|
500
522
|
"required": [
|