@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
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.15.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- count a video whose bundle file was never stored as unavailable ([6e342e1](https://github.com/crvouga/atlas/commit/6e342e1c0994b78499e373496afa5035a387a43c))
|
|
8
|
+
- compare behavior promises, recordings and outcomes between two pinned runs ([9bc0056](https://github.com/crvouga/atlas/commit/9bc0056de84899801377c0cc1c3bb70c80e3820a))
|
|
9
|
+
|
|
10
|
+
## 1.14.0 (2026-10-28)
|
|
4
11
|
|
|
5
12
|
### Features
|
|
6
13
|
|
|
@@ -46,11 +46,19 @@ export declare function summarizeEvidence(...holders: readonly EvidenceHolder[])
|
|
|
46
46
|
* `effectiveScope`). Without `inherited` it is the same as `summarizeEvidence`.
|
|
47
47
|
*/
|
|
48
48
|
export declare function summarizeEvidenceIn(inherited: EvidenceScope | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Like {@link summarizeEvidenceIn}, knowing which bundle keys the layer names but does not hold (`unstored`, see
|
|
51
|
+
* `unstoredKeys`): a video whose only bytes are such a key is `available: false`, never a recording. Without
|
|
52
|
+
* `unstored` it is the same as `summarizeEvidenceIn`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function summarizeStoredEvidence(inherited: EvidenceScope | undefined, unstored: ReadonlySet<string> | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined;
|
|
49
55
|
/**
|
|
50
56
|
* Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
|
|
51
57
|
* part is owned by the `layer`, not by a check.
|
|
52
58
|
*/
|
|
53
59
|
export declare function summarizeLayerEvidence(...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined;
|
|
60
|
+
/** Like {@link summarizeLayerEvidence}, knowing which bundle keys the layer names but does not hold (see {@link summarizeStoredEvidence}). */
|
|
61
|
+
export declare function summarizeStoredLayerEvidence(unstored: ReadonlySet<string> | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined;
|
|
54
62
|
/**
|
|
55
63
|
* Whether an attachment is a video, by its effective media type: the producer's `contentType`, else the one its file
|
|
56
64
|
* name implies. A role never decides this on its own (`demo` on a PNG is not a video), except that a `video` role
|
|
@@ -113,3 +121,14 @@ export declare function partsEvidence(layer: {
|
|
|
113
121
|
}): PartEvidence[];
|
|
114
122
|
/** 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. */
|
|
115
123
|
export declare function bundleKeysOf(holder: EvidenceHolder): string[];
|
|
124
|
+
/**
|
|
125
|
+
* The bundle keys that attachments of a layer, its parts and their checks name, but that the layer's `bundle` index does
|
|
126
|
+
* not hold: files the producer referenced and never stored. Empty for a layer whose every file is there. This reads only
|
|
127
|
+
* the index, never a blob, so it is cheap enough to ask of every layer as it is read.
|
|
128
|
+
*/
|
|
129
|
+
export declare function unstoredKeys(layer: EvidenceHolder & {
|
|
130
|
+
readonly parts: readonly (EvidenceHolder & {
|
|
131
|
+
readonly checks: readonly EvidenceHolder[];
|
|
132
|
+
})[];
|
|
133
|
+
readonly bundle: Readonly<Record<string, unknown>>;
|
|
134
|
+
}): string[];
|
|
@@ -26,6 +26,14 @@ export function summarizeEvidence(...holders) {
|
|
|
26
26
|
export function summarizeEvidenceIn(inherited, ...holders) {
|
|
27
27
|
return summarize(holders, 'check', inherited);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Like {@link summarizeEvidenceIn}, knowing which bundle keys the layer names but does not hold (`unstored`, see
|
|
31
|
+
* `unstoredKeys`): a video whose only bytes are such a key is `available: false`, never a recording. Without
|
|
32
|
+
* `unstored` it is the same as `summarizeEvidenceIn`.
|
|
33
|
+
*/
|
|
34
|
+
export function summarizeStoredEvidence(inherited, unstored, ...holders) {
|
|
35
|
+
return summarize(holders, 'check', inherited, false, unstored);
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
|
|
31
39
|
* part is owned by the `layer`, not by a check.
|
|
@@ -34,7 +42,11 @@ export function summarizeLayerEvidence(...holders) {
|
|
|
34
42
|
// The first holder is the layer: its scope reaches the parts after it, which may declare their own.
|
|
35
43
|
return summarize(holders, 'layer', undefined, true);
|
|
36
44
|
}
|
|
37
|
-
|
|
45
|
+
/** Like {@link summarizeLayerEvidence}, knowing which bundle keys the layer names but does not hold (see {@link summarizeStoredEvidence}). */
|
|
46
|
+
export function summarizeStoredLayerEvidence(unstored, ...holders) {
|
|
47
|
+
return summarize(holders, 'layer', undefined, true, unstored);
|
|
48
|
+
}
|
|
49
|
+
function summarize(holders, owner, inherited, firstIsParent = false, unstored) {
|
|
38
50
|
if (!holders.some(holder => count(holder) > 0))
|
|
39
51
|
return undefined;
|
|
40
52
|
const roles = new Set();
|
|
@@ -50,7 +62,7 @@ function summarize(holders, owner, inherited, firstIsParent = false) {
|
|
|
50
62
|
for (const tag of normalizeTags(item.tags))
|
|
51
63
|
artifactTags.add(tag);
|
|
52
64
|
roles.add(item.role ?? roleOfType(item.contentType ?? contentTypeOf(item.name)));
|
|
53
|
-
const video = videoOf(item, by, where, effectiveScope(item.scope, ...scopes));
|
|
65
|
+
const video = videoOf(item, by, where, effectiveScope(item.scope, ...scopes), unstored);
|
|
54
66
|
if (video)
|
|
55
67
|
videos.push(video);
|
|
56
68
|
}
|
|
@@ -98,10 +110,12 @@ export function isVideoAttachment(attachment) {
|
|
|
98
110
|
export function attachmentIdentity(item) {
|
|
99
111
|
return item.sha256 ? `sha256:${item.sha256}` : item.bundleKey ?? item.uri ?? (item.text === undefined ? undefined : `inline:${item.name}`);
|
|
100
112
|
}
|
|
101
|
-
function videoOf(item, owner, where, scope) {
|
|
113
|
+
function videoOf(item, owner, where, scope, unstored) {
|
|
102
114
|
if (!isVideoAttachment(item))
|
|
103
115
|
return undefined;
|
|
104
|
-
|
|
116
|
+
// A bundle key the layer's bundle does not hold names bytes that are not there: only an address or inline text can still carry them, and the digest of lost bytes identifies nothing playable.
|
|
117
|
+
const lost = item.bundleKey !== undefined && unstored?.has(item.bundleKey) === true;
|
|
118
|
+
const recording = lost ? attachmentIdentity({ name: item.name, uri: item.uri, text: item.text }) : attachmentIdentity(item);
|
|
105
119
|
const tags = normalizeTags(item.tags);
|
|
106
120
|
return { name: item.name, owner, ...where, ...(tags.length ? { tags } : {}), ...(recording === undefined ? {} : { recording }), available: recording !== undefined, ...(scope ? { scope } : {}) };
|
|
107
121
|
}
|
|
@@ -181,3 +195,16 @@ export function bundleKeysOf(holder) {
|
|
|
181
195
|
}
|
|
182
196
|
return [...keys];
|
|
183
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* The bundle keys that attachments of a layer, its parts and their checks name, but that the layer's `bundle` index does
|
|
200
|
+
* not hold: files the producer referenced and never stored. Empty for a layer whose every file is there. This reads only
|
|
201
|
+
* the index, never a blob, so it is cheap enough to ask of every layer as it is read.
|
|
202
|
+
*/
|
|
203
|
+
export function unstoredKeys(layer) {
|
|
204
|
+
const missing = new Set();
|
|
205
|
+
for (const holder of [layer, ...layer.parts, ...layer.parts.flatMap(part => part.checks)])
|
|
206
|
+
for (const key of bundleKeysOf(holder))
|
|
207
|
+
if (!Object.hasOwn(layer.bundle, key))
|
|
208
|
+
missing.add(key);
|
|
209
|
+
return [...missing];
|
|
210
|
+
}
|
|
@@ -56,7 +56,7 @@ export type VideoRef = {
|
|
|
56
56
|
readonly tags?: readonly string[];
|
|
57
57
|
/** What identifies the bytes (digest, bundle key or address); references with the same one are the same recording. Absent when `available` is false. */
|
|
58
58
|
readonly recording?: string;
|
|
59
|
-
/** False when the reference names no bytes at all (no bundle key, address or inline text), so it can never be played. */
|
|
59
|
+
/** 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. */
|
|
60
60
|
readonly available: boolean;
|
|
61
61
|
/** 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. */
|
|
62
62
|
readonly scope?: EvidenceScope;
|
|
@@ -67,7 +67,7 @@ export type VideoCounts = {
|
|
|
67
67
|
readonly references: number;
|
|
68
68
|
/** Distinct playable recordings: identical bytes referenced twice are one. */
|
|
69
69
|
readonly recordings: number;
|
|
70
|
-
/** References that name no bytes. A report that predates availability metadata has none of these and none of the others. */
|
|
70
|
+
/** 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. */
|
|
71
71
|
readonly unavailable: number;
|
|
72
72
|
};
|
|
73
73
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { summarizeStoredEvidence, summarizeStoredLayerEvidence } from './evidence-detail.js';
|
|
2
2
|
import { effectiveScope } from './evidence-scope.js';
|
|
3
3
|
import { layerPlace } from './layer-axes.js';
|
|
4
4
|
import { UI_PLAN_KIND, uiStateOf } from './ui-state.js';
|
|
@@ -90,13 +90,14 @@ export function layerEvidence(summary, backend) {
|
|
|
90
90
|
normalized.set(path, found = specPath(path));
|
|
91
91
|
return found;
|
|
92
92
|
};
|
|
93
|
+
const unstored = summary.unstored?.length ? new Set(summary.unstored) : undefined;
|
|
93
94
|
const rows = [];
|
|
94
95
|
const receipt = summary.recordings;
|
|
95
96
|
const expectations = receipt ? new Map(receipt.items.map(item => [item.check, item])) : null;
|
|
96
97
|
for (const part of summary.parts) {
|
|
97
98
|
for (const check of part.checks) {
|
|
98
99
|
const test = tests.get(check.id);
|
|
99
|
-
const evidence =
|
|
100
|
+
const evidence = summarizeStoredEvidence(effectiveScope(part.scope, summary.scope), unstored, check);
|
|
100
101
|
const item = expectations?.get(check.id);
|
|
101
102
|
const state = uiStateOf(part.kind, check);
|
|
102
103
|
rows.push({
|
|
@@ -117,7 +118,7 @@ export function layerEvidence(summary, backend) {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
const recording = summary.recording;
|
|
120
|
-
const layerSummaryEvidence =
|
|
121
|
+
const layerSummaryEvidence = summarizeStoredLayerEvidence(unstored, summary, ...summary.parts);
|
|
121
122
|
const layer = {
|
|
122
123
|
...(summary.freshness ? { freshness: summary.freshness } : {}),
|
|
123
124
|
...(summary.run ? { run: summary.run } : {}),
|
|
@@ -48,5 +48,5 @@ export declare function publishLayerBundle(store: ObjectStore, summary: Omit<Lay
|
|
|
48
48
|
export declare function readLayer(store: ObjectStore, id: string): Promise<ReportLayer | null>;
|
|
49
49
|
/** Reads one captured file (a screenshot, a frame, a recorded report) of a layer, or `null` when the layer or the file is missing. Throws like `readLayer`. */
|
|
50
50
|
export declare function readLayerFile(store: ObjectStore, id: string, file: string): Promise<BundleEntry | null>;
|
|
51
|
-
/** Drops a layer's bundle index, leaving the summary composition reads. */
|
|
51
|
+
/** 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. */
|
|
52
52
|
export declare function layerSummary(layer: ReportLayer): LayerSummary;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createBundleReader, writeBundle } from './bundle.js';
|
|
12
12
|
import { readUtf8, writeUtf8 } from './object-io.js';
|
|
13
|
+
import { unstoredKeys } from './evidence-detail.js';
|
|
13
14
|
import { LAYER_SCHEMA_VERSION, parseLayerDocument } from '../../schema/src/layer.js';
|
|
14
15
|
/** The key prefix under which every layer's catalog entry is written. */
|
|
15
16
|
export const LAYER_INDEX = 'layers/index/';
|
|
@@ -87,8 +88,9 @@ export async function readLayerFile(store, id, file) {
|
|
|
87
88
|
const layer = await readLayer(store, id);
|
|
88
89
|
return layer ? createBundleReader(store, layer.bundle)(file) : null;
|
|
89
90
|
}
|
|
90
|
-
/** Drops a layer's bundle index, leaving the summary composition reads. */
|
|
91
|
+
/** 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. */
|
|
91
92
|
export function layerSummary(layer) {
|
|
92
93
|
const { bundle: _, ...summary } = layer;
|
|
93
|
-
|
|
94
|
+
const unstored = unstoredKeys(layer);
|
|
95
|
+
return unstored.length ? { ...summary, unstored } : summary;
|
|
94
96
|
}
|
|
@@ -52,6 +52,8 @@ export type LayerSummary = LayerExtras & {
|
|
|
52
52
|
readonly sha: string;
|
|
53
53
|
readonly source: string;
|
|
54
54
|
} | null;
|
|
55
|
+
/** 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. */
|
|
56
|
+
readonly unstored?: readonly string[];
|
|
55
57
|
};
|
|
56
58
|
/** A layer with the index that locates its captured files (screenshots, frames) in the store. */
|
|
57
59
|
export type ReportLayer = LayerSummary & {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { type LatencyChange, type LatencyEntry, type LatencySummary } from './latency.js';
|
|
2
|
+
import type { ReliabilityDocument, ReliabilityRate, ReliabilityScope } from './reliability.js';
|
|
3
|
+
import type { RunDetail, RunResult, RunSummary } from './test-catalog.js';
|
|
4
|
+
/** Where a pair of results (or one result with no partner) stands, in the order a review reads them. */
|
|
5
|
+
export declare const COMPARE_GROUPS: readonly ["regressed", "recovered", "still-failing", "not-comparable", "unknown", "removed", "added", "unchanged"];
|
|
6
|
+
/** One of {@link COMPARE_GROUPS}. */
|
|
7
|
+
export type CompareGroup = (typeof COMPARE_GROUPS)[number];
|
|
8
|
+
/** What each group is called and what it does and does not say. */
|
|
9
|
+
export declare const COMPARE_GROUP_TEXT: Readonly<Record<CompareGroup, {
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly meaning: string;
|
|
12
|
+
}>>;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the promise (what the behavior says should happen) is the same on both sides: `same` (same wording and the
|
|
15
|
+
* same recorded spec revision), `changed` (the wording differs), `revision-differs` (same wording, another spec-file
|
|
16
|
+
* revision), or `unknown` (a revision was not recorded on a side, so nothing can be said).
|
|
17
|
+
*/
|
|
18
|
+
export type PromiseState = 'same' | 'changed' | 'revision-differs' | 'unknown';
|
|
19
|
+
/** What is known about whether the promise changed, and why. */
|
|
20
|
+
export type PromiseReading = {
|
|
21
|
+
readonly state: PromiseState;
|
|
22
|
+
readonly baselineTitle: string | null;
|
|
23
|
+
readonly candidateTitle: string | null;
|
|
24
|
+
readonly baselineRevision: string | null;
|
|
25
|
+
readonly candidateRevision: string | null;
|
|
26
|
+
/** In words, for a person. */
|
|
27
|
+
readonly reasons: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
/** The condition a result was recorded under: with the check, what makes it its own result. */
|
|
30
|
+
export type Condition = {
|
|
31
|
+
readonly kind: string | null;
|
|
32
|
+
readonly producer: string;
|
|
33
|
+
readonly environment: string | null;
|
|
34
|
+
readonly parameters: Readonly<Record<string, string>>;
|
|
35
|
+
};
|
|
36
|
+
/** The condition of a result. A server that does not send `kind` leaves it null: unknown, equal only to another unknown. */
|
|
37
|
+
export declare function conditionOf(result: Pick<RunResult, 'kind' | 'producer' | 'environment' | 'parameters'>): Condition;
|
|
38
|
+
/** A stable text for a condition, equal exactly when two conditions are. */
|
|
39
|
+
export declare function conditionId(condition: Condition): string;
|
|
40
|
+
/** How two conditions differ, in words; empty when they are the same. */
|
|
41
|
+
export declare function conditionDifferences(baseline: Condition, candidate: Condition): string[];
|
|
42
|
+
/** One behavior's results on the two sides. */
|
|
43
|
+
export type ComparedResult = {
|
|
44
|
+
/** Identifies the pair within one comparison: the check, and the condition of each side. Stable for the same two runs. */
|
|
45
|
+
readonly id: string;
|
|
46
|
+
/** The check's identity (`TestSummary.key`). */
|
|
47
|
+
readonly key: string;
|
|
48
|
+
/** The check's title: the candidate's when it has one, else the baseline's. */
|
|
49
|
+
readonly title: string;
|
|
50
|
+
readonly specPath: string | null;
|
|
51
|
+
readonly baseline: RunResult | null;
|
|
52
|
+
readonly candidate: RunResult | null;
|
|
53
|
+
readonly group: CompareGroup;
|
|
54
|
+
/** One sentence saying what happened, and what is not known. */
|
|
55
|
+
readonly summary: string;
|
|
56
|
+
readonly promise: PromiseReading;
|
|
57
|
+
/** How the two conditions differ, in words, when both sides have a result and they were recorded under different conditions. */
|
|
58
|
+
readonly conditions: readonly string[];
|
|
59
|
+
/** What a reader must not assume about this pair, in words. */
|
|
60
|
+
readonly caveats: readonly string[];
|
|
61
|
+
};
|
|
62
|
+
/** The whole comparison of two pinned runs. */
|
|
63
|
+
export type RunComparison = {
|
|
64
|
+
readonly baseline: RunSummary;
|
|
65
|
+
readonly candidate: RunSummary;
|
|
66
|
+
/** Every pair and every unpaired result, in the order of {@link COMPARE_GROUPS}, then spec and title. */
|
|
67
|
+
readonly results: readonly ComparedResult[];
|
|
68
|
+
readonly counts: Readonly<Record<CompareGroup, number>>;
|
|
69
|
+
/** 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. */
|
|
70
|
+
readonly notes: readonly string[];
|
|
71
|
+
};
|
|
72
|
+
/** Short form of a spec fingerprint, to show. */
|
|
73
|
+
export declare function shortRevision(revision: string | null | undefined): string;
|
|
74
|
+
/** The wording a run recorded for a check: what that run's layer called it, else the check's newest title. */
|
|
75
|
+
export declare function recordedTitle(result: Pick<RunResult, 'title' | 'recordedTitle'>): string;
|
|
76
|
+
/**
|
|
77
|
+
* Pairs the results of two runs and reads what changed. Results are paired by check identity and then by condition (see
|
|
78
|
+
* the module comment); titles never pair two results. Each side's results are those the run recorded last for each
|
|
79
|
+
* check and condition (`RunDetail.results`).
|
|
80
|
+
*
|
|
81
|
+
* @param baseline - The run to compare against.
|
|
82
|
+
* @param candidate - The run under review. Both are used as given: a newer run never replaces either.
|
|
83
|
+
* @returns Every result of either run, grouped by what changed, with the reasons.
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const comparison = compareRuns(await runDetail('run:ci-100'), await runDetail('run:ci-101'));
|
|
87
|
+
* comparison.results.filter(item => item.group === 'regressed');
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function compareRuns(baseline: RunDetail, candidate: RunDetail): RunComparison;
|
|
91
|
+
/** What one product-latency metric shows on the two sides. */
|
|
92
|
+
export type MetricComparison = {
|
|
93
|
+
readonly state: 'compared';
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly baseline: LatencySummary;
|
|
96
|
+
readonly candidate: LatencySummary;
|
|
97
|
+
readonly change: LatencyChange;
|
|
98
|
+
readonly reasons: readonly string[];
|
|
99
|
+
} | {
|
|
100
|
+
readonly state: 'incompatible';
|
|
101
|
+
readonly name: string;
|
|
102
|
+
readonly baseline: LatencySummary;
|
|
103
|
+
readonly candidate: LatencySummary;
|
|
104
|
+
readonly change: LatencyChange | null;
|
|
105
|
+
readonly reasons: readonly string[];
|
|
106
|
+
} | {
|
|
107
|
+
readonly state: 'baseline-only' | 'candidate-only';
|
|
108
|
+
readonly name: string;
|
|
109
|
+
readonly summary: LatencySummary;
|
|
110
|
+
readonly reasons: readonly string[];
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Compares the product latency recorded in two sets of results of one check, one per side. A metric is compared only
|
|
114
|
+
* when it is the same measurement (name, unit, boundaries) and both sides were measured on the same devices, in the
|
|
115
|
+
* same environments, with the same instrumentation and other conditions; otherwise it is `incompatible` and no change
|
|
116
|
+
* is computed, with the reasons. A metric only one side recorded is named as such. A side without any latency is unknown,
|
|
117
|
+
* not fast: it contributes no metric. `conditionsDiffer` names differences of the results' own conditions, which make
|
|
118
|
+
* every metric incompatible.
|
|
119
|
+
*/
|
|
120
|
+
export declare function compareProductLatency(baseline: readonly LatencyEntry[], candidate: readonly LatencyEntry[], conditionsDiffer?: readonly string[]): MetricComparison[];
|
|
121
|
+
/** How reliable a check was on each side, and whether the two figures can be set against each other. */
|
|
122
|
+
export type ReliabilityComparison = {
|
|
123
|
+
/** True only when both figures are established, were counted in the same scope and against the same recorded spec revision. */
|
|
124
|
+
readonly comparable: boolean;
|
|
125
|
+
readonly baseline: ReliabilityRate;
|
|
126
|
+
readonly candidate: ReliabilityRate;
|
|
127
|
+
/** Candidate minus baseline, in percentage points, only when comparable. */
|
|
128
|
+
readonly points?: number;
|
|
129
|
+
/** `higher` or `lower` only when the two intervals do not overlap; `indistinguishable` when they do. Absent when not comparable. */
|
|
130
|
+
readonly direction?: 'higher' | 'lower' | 'indistinguishable';
|
|
131
|
+
/** Why no change is stated, or what qualifies one. */
|
|
132
|
+
readonly reasons: readonly string[];
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* The scope that counts the independent executions of one result's check up to the end of its run: the same
|
|
136
|
+
* environment, producer and parameters, and the spec revision that result was recorded against (executions against
|
|
137
|
+
* another revision are other promises). A result that recorded no revision is counted across revisions, which
|
|
138
|
+
* {@link compareReliability} will not compare. Use it for each side, with that side's result and run.
|
|
139
|
+
*/
|
|
140
|
+
export declare function reliabilityScopeAt(result: Pick<RunResult, 'environment' | 'producer' | 'parameters' | 'specRevision'>, run: Pick<RunSummary, 'endedAtMs'>): ReliabilityScope;
|
|
141
|
+
/**
|
|
142
|
+
* Sets the reliability of a check up to the baseline run against its reliability up to the candidate run. A change is
|
|
143
|
+
* stated only when both samples are established (at least `minimumExecutions` independent executions), both counted the
|
|
144
|
+
* same recorded spec revision (see {@link reliabilityScopeAt}) and the conditions match; whether the difference exceeds the uncertainty is
|
|
145
|
+
* said by `direction`. Otherwise each figure stands alone and the reasons say why.
|
|
146
|
+
*/
|
|
147
|
+
export declare function compareReliability(baseline: ReliabilityDocument, candidate: ReliabilityDocument, conditionsDiffer?: readonly string[]): ReliabilityComparison;
|
|
148
|
+
/** The part of a recorded step this module reads. */
|
|
149
|
+
export type StepLike = {
|
|
150
|
+
readonly name: string;
|
|
151
|
+
readonly offsetMs?: number | undefined;
|
|
152
|
+
};
|
|
153
|
+
/** One step, found on one or both sides. */
|
|
154
|
+
export type AlignedStep = {
|
|
155
|
+
readonly name: string;
|
|
156
|
+
/** Milliseconds after the check began, on each side; absent when that side did not record it or its time. */
|
|
157
|
+
readonly baselineOffsetMs?: number;
|
|
158
|
+
readonly candidateOffsetMs?: number;
|
|
159
|
+
readonly onBaseline: boolean;
|
|
160
|
+
readonly onCandidate: boolean;
|
|
161
|
+
};
|
|
162
|
+
/** How the recorded steps of two results line up. */
|
|
163
|
+
export type StepAlignment = {
|
|
164
|
+
/** `aligned`: some step is on both sides with a time on both (a recording can be sought to it); `unaligned`: otherwise. */
|
|
165
|
+
readonly state: 'aligned' | 'unaligned';
|
|
166
|
+
readonly steps: readonly AlignedStep[];
|
|
167
|
+
/** The steps with a time on both sides: where both recordings can be sought together. */
|
|
168
|
+
readonly seekable: readonly (AlignedStep & {
|
|
169
|
+
readonly baselineOffsetMs: number;
|
|
170
|
+
readonly candidateOffsetMs: number;
|
|
171
|
+
})[];
|
|
172
|
+
/** Whether the two lists of step names differ (the steps themselves are part of the promise). */
|
|
173
|
+
readonly differs: boolean;
|
|
174
|
+
/** In words, why the recordings are or are not aligned. */
|
|
175
|
+
readonly explanation: string;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* 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
|
|
179
|
+
* the other), keeping each side's own time origin: an offset is milliseconds after that side's check began. Nothing is
|
|
180
|
+
* aligned by position or by fuzzy wording. When nothing lines up, `explanation` says why.
|
|
181
|
+
*/
|
|
182
|
+
export declare function alignSteps(baseline: readonly StepLike[], candidate: readonly StepLike[]): StepAlignment;
|