@crvouga/atlas 1.15.0 → 1.17.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 +13 -1
- package/dist/reports/src/catalog-health.d.ts +15 -2
- package/dist/reports/src/catalog-health.js +23 -2
- package/dist/reports/src/test-catalog.d.ts +13 -0
- package/dist/reports/src/test-catalog.js +21 -5
- package/dist/schema/src/api.d.ts +35 -0
- package/dist/schema/src/api.js +14 -3
- 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 +41 -2
- package/docs/reference/server.md +2 -0
- package/docs/server.md +11 -2
- package/docs/ui.md +28 -1
- package/llms-full.txt +44 -8
- package/package.json +1 -1
- package/packages/schema/schemas/catalog-health.schema.json +20 -0
- package/packages/schema/schemas/server-config.schema.json +17 -0
- package/packages/schema/schemas/test-catalog.schema.json +26 -0
- package/packages/schema/schemas/test-history.schema.json +40 -0
- package/packages/server/openapi.json +90 -4
- package/packages/visualizer/src/evidence/applicability-view.tsx +46 -20
- package/packages/visualizer/src/evidence/applicability.test.ts +68 -1
- package/packages/visualizer/src/evidence/applicability.ts +121 -26
- package/packages/visualizer/src/explorer/Explorer.tsx +105 -10
- package/packages/visualizer/src/explorer/HealthView.tsx +402 -0
- package/packages/visualizer/src/explorer/TestDetail.tsx +14 -1
- package/packages/visualizer/src/explorer/applicability.ts +43 -0
- package/packages/visualizer/src/explorer/catalog.tsx +33 -0
- package/packages/visualizer/src/explorer/search.ts +20 -7
|
@@ -111,6 +111,26 @@ export function ApplicabilitySummary({ assessment, release }: { assessment: Asse
|
|
|
111
111
|
</time>
|
|
112
112
|
</Text>
|
|
113
113
|
</Text>
|
|
114
|
+
{facts.publishedAtMs !== null && (
|
|
115
|
+
<Text span inherit data-published>
|
|
116
|
+
Published:{' '}
|
|
117
|
+
<Text span inherit fw={600}>
|
|
118
|
+
<time dateTime={new Date(facts.publishedAtMs).toISOString()} title={exactTime(facts.publishedAtMs)}>
|
|
119
|
+
{exactTime(facts.publishedAtMs)} ({ago(facts.publishedAtMs, now)})
|
|
120
|
+
</time>
|
|
121
|
+
</Text>
|
|
122
|
+
</Text>
|
|
123
|
+
)}
|
|
124
|
+
{facts.enrichedAtMs !== null && (
|
|
125
|
+
<Text span inherit data-enriched>
|
|
126
|
+
Enriched:{' '}
|
|
127
|
+
<Text span inherit fw={600}>
|
|
128
|
+
<time dateTime={new Date(facts.enrichedAtMs).toISOString()} title={exactTime(facts.enrichedAtMs)}>
|
|
129
|
+
{exactTime(facts.enrichedAtMs)} ({ago(facts.enrichedAtMs, now)})
|
|
130
|
+
</time>
|
|
131
|
+
</Text>
|
|
132
|
+
</Text>
|
|
133
|
+
)}
|
|
114
134
|
<Text span inherit>
|
|
115
135
|
Build:{' '}
|
|
116
136
|
<Text span inherit fw={600}>
|
|
@@ -167,13 +187,17 @@ export function ApplicabilitySummary({ assessment, release }: { assessment: Asse
|
|
|
167
187
|
*/
|
|
168
188
|
export function ApplicabilityControls({
|
|
169
189
|
state,
|
|
170
|
-
stale,
|
|
171
|
-
total
|
|
190
|
+
stale = 0,
|
|
191
|
+
total = 0,
|
|
192
|
+
filter = true
|
|
172
193
|
}: {
|
|
173
194
|
state: ApplicabilityState;
|
|
174
|
-
/** How many behaviors have stale or unverified evidence. */
|
|
175
|
-
stale
|
|
176
|
-
|
|
195
|
+
/** How many behaviors (or tests) have stale or unverified evidence. Only shown with the filter. */
|
|
196
|
+
stale?: number;
|
|
197
|
+
/** How many behaviors (or tests) there are. Only shown with the filter. */
|
|
198
|
+
total?: number;
|
|
199
|
+
/** Whether to offer the stale-or-unverified filter here; a caller that offers it elsewhere sets this false. */
|
|
200
|
+
filter?: boolean;
|
|
177
201
|
}) {
|
|
178
202
|
const { release, setRelease, onlyStale, setOnlyStale } = state;
|
|
179
203
|
return (
|
|
@@ -208,21 +232,23 @@ export function ApplicabilityControls({
|
|
|
208
232
|
A platform alone does not name a release. Add its build or commit.
|
|
209
233
|
</Text>
|
|
210
234
|
)}
|
|
211
|
-
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
235
|
+
{filter && (
|
|
236
|
+
<Group role="group" aria-label="Evidence filter" gap="sm">
|
|
237
|
+
<Button type="button" aria-pressed={!onlyStale} onClick={() => setOnlyStale(false)} variant={onlyStale ? 'subtle' : 'light'}>
|
|
238
|
+
All evidence{' '}
|
|
239
|
+
<Text span inherit>
|
|
240
|
+
{total}
|
|
241
|
+
</Text>
|
|
242
|
+
</Button>
|
|
243
|
+
<Button type="button" aria-pressed={onlyStale} onClick={() => setOnlyStale(true)} variant={onlyStale ? 'light' : 'subtle'}>
|
|
244
|
+
Stale or unverified{' '}
|
|
245
|
+
<Text span inherit>
|
|
246
|
+
{stale}
|
|
247
|
+
</Text>
|
|
248
|
+
</Button>
|
|
249
|
+
</Group>
|
|
250
|
+
)}
|
|
251
|
+
{filter && onlyStale && stale === 0 && (
|
|
226
252
|
<Text role="status" component="div" c="dimmed">
|
|
227
253
|
Every behavior has evidence that applies.
|
|
228
254
|
</Text>
|
|
@@ -3,7 +3,9 @@ import type { CheckSlot } from '@crvouga/atlas/reports/evidence-view';
|
|
|
3
3
|
import type { EvidenceResult } from '@crvouga/atlas/reports/layer-merge';
|
|
4
4
|
import { expect, test } from 'bun:test';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import type { TestResultEntry, TestSummary } from '@crvouga/atlas/reports/test-catalog';
|
|
7
|
+
|
|
8
|
+
import { assessBehavior, assessResult, assessTest, attentionRank, hasRelease, needsAttention } from './applicability';
|
|
7
9
|
import { behaviorOf, type RevisionSource } from './behavior';
|
|
8
10
|
import { featureModel } from './gherkin';
|
|
9
11
|
|
|
@@ -129,3 +131,68 @@ test('a behavior is read by its best completed result and its status is untouche
|
|
|
129
131
|
const running = assessBehavior(behaviorOf(ADD, slot([result('l1', {}, 'running')]), null, same, 'k'), same, {});
|
|
130
132
|
expect(running.verdict).toBe('none');
|
|
131
133
|
});
|
|
134
|
+
|
|
135
|
+
type Outcome = Partial<TestResultEntry>;
|
|
136
|
+
function testOf(outcomes: Outcome[], fingerprint: string | null = 'f1'): TestSummary {
|
|
137
|
+
return {
|
|
138
|
+
key: 'k', id: 'k', title: 'Lists: Add', specPath: 'specs/a.feature',
|
|
139
|
+
spec: { path: 'specs/a.feature', title: 'Lists', domain: 'lists', kind: 'gherkin', ...(fingerprint ? { fingerprint } : {}) },
|
|
140
|
+
outcomes: outcomes.map((outcome, index) => ({
|
|
141
|
+
at: 1_000, status: 'passed', durationMs: null, environment: 'desktop-web', producer: 'p', kind: 'behavior', parameters: {}, branch: 'main', sha: null, runUrl: null,
|
|
142
|
+
run: 'r', layer: `l${index + 1}`, backend: 'b', detail: '', specRevision: 'f1', ...outcome
|
|
143
|
+
}))
|
|
144
|
+
} as unknown as TestSummary;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
test('a test whose promise changed after its only result is stale, and the same test read from the catalog agrees with the page', () => {
|
|
148
|
+
const stale = assessTest(testOf([{ specRevision: 'f0' }]), {});
|
|
149
|
+
expect(stale.verdict).toBe('stale');
|
|
150
|
+
expect(stale.reasons[0]).toContain('not proof of the current promise');
|
|
151
|
+
expect(assessTest(testOf([{ specRevision: 'f1' }]), {}).verdict).toBe('applies');
|
|
152
|
+
// The same facts, read from the report, give the same verdict.
|
|
153
|
+
expect(assessResult(result('l1'), revisions('f1', { l1: 'f0' }), {}).verdict).toBe(stale.verdict);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('a test with no recorded revision or no promise fingerprint is unknown, never applying', () => {
|
|
157
|
+
expect(assessTest(testOf([{ specRevision: undefined }]), {}).verdict).toBe('unknown');
|
|
158
|
+
expect(assessTest(testOf([{ specRevision: 'f1' }], null), {}).verdict).toBe('unknown');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('a test with no completed result has none, and its failure is not read at all as stale', () => {
|
|
162
|
+
expect(assessTest(testOf([{ status: 'running' }, { status: 'not-run' }]), {}).verdict).toBe('none');
|
|
163
|
+
expect(assessTest(testOf([]), { build: '9' }).release).toBe('unknown');
|
|
164
|
+
expect(assessTest(testOf([{ status: 'failed' }]), {}).verdict).toBe('applies');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('a test is read against the release from its provenance: build, platform, modified checkout and asserted compatibility', () => {
|
|
168
|
+
const release = { build: '91' };
|
|
169
|
+
expect(assessTest(testOf([{ provenance: { build: '90' } }]), release).release).toBe('other');
|
|
170
|
+
expect(assessTest(testOf([{ provenance: { build: '91' } }]), release).release).toBe('exact');
|
|
171
|
+
expect(assessTest(testOf([{ provenance: { build: '91' } }]), { ...release, platform: 'ios' }).release).toBe('unknown');
|
|
172
|
+
expect(assessTest(testOf([{ provenance: { build: '91' }, freshness: 'modified' }]), release).release).toBe('unknown');
|
|
173
|
+
const asserted = (by: Record<string, string>) => assessTest(testOf([{ provenance: { build: '90', 'applies-to': '91', ...by } }]), release);
|
|
174
|
+
expect(asserted({ 'applies-to-by': 'policy-7' }).release).toBe('asserted');
|
|
175
|
+
expect(asserted({}).release).toBe('unknown');
|
|
176
|
+
// A branch never supplies deployment state.
|
|
177
|
+
expect(assessTest(testOf([{ branch: 'release/91' }]), release).release).toBe('unknown');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('the best of a test\'s current results is kept, and stale evidence sorts before evidence that applies', () => {
|
|
181
|
+
const both = assessTest(testOf([{ specRevision: 'f0', at: 5_000 }, { specRevision: 'f1', at: 1_000 }]), {});
|
|
182
|
+
expect(both.verdict).toBe('applies');
|
|
183
|
+
expect(both.layer).toBe('l2');
|
|
184
|
+
expect(both.others).toBe(1);
|
|
185
|
+
const ranks = [assessTest(testOf([{}]), {}), assessTest(testOf([{ specRevision: 'f0' }]), {}), assessTest(testOf([{ specRevision: undefined }]), {}), assessTest(testOf([]), {})].map(attentionRank);
|
|
186
|
+
expect(ranks).toEqual([3, 0, 1, 2]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('when evidence was published or enriched is carried apart from when the checks ran, and a time that cannot be read is no time', () => {
|
|
190
|
+
const facts = assessTest(testOf([{ at: 1_000, provenance: { 'published-at': '1700000000000', 'enriched-at': '2026-10-01T00:00:00Z' } }]), {}).facts!;
|
|
191
|
+
expect(facts.executedAtMs).toBe(1_000);
|
|
192
|
+
expect(facts.publishedAtMs).toBe(1_700_000_000_000);
|
|
193
|
+
expect(facts.enrichedAtMs).toBe(Date.parse('2026-10-01T00:00:00Z'));
|
|
194
|
+
const unreadable = assessTest(testOf([{ provenance: { 'published-at': 'yesterday-ish', 'enriched-at': '0' } }]), {}).facts!;
|
|
195
|
+
expect([unreadable.publishedAtMs, unreadable.enrichedAtMs]).toEqual([null, null]);
|
|
196
|
+
// Read from the report, labels at the layer or the check say the same.
|
|
197
|
+
expect(assessResult(result('l1', { layerLabels: { 'published-at': '1700000000000' } }), same, {}).facts!.publishedAtMs).toBe(1_700_000_000_000);
|
|
198
|
+
});
|
|
@@ -13,9 +13,16 @@
|
|
|
13
13
|
* the app under test was), `platform` (the client family; the environment when absent), `applies-to` (a comma
|
|
14
14
|
* separated list of builds or commits the producer asserts the evidence is still good for) and `applies-to-by`
|
|
15
15
|
* (the policy, person or tool that makes the assertion). An assertion without `applies-to-by` is not accepted.
|
|
16
|
+
* `published-at` and `enriched-at` say when the evidence was published or enriched (epoch milliseconds or an ISO date).
|
|
17
|
+
* Layers carry no publication time of their own, because a layer's id is the digest of its content and a time of
|
|
18
|
+
* upload in it would make a retried publish a different layer; a producer that knows states it as a label. It is shown
|
|
19
|
+
* apart from when the checks ran and is never the age of the evidence.
|
|
20
|
+
*
|
|
21
|
+
* The same reading serves a feature page (from the report's results, {@link assessBehavior}) and the test explorer (from
|
|
22
|
+
* the catalog's current results, {@link assessTest}).
|
|
16
23
|
*/
|
|
17
|
-
import type { LayerHeader } from '@crvouga/atlas/reports/evidence-types';
|
|
18
24
|
import type { EvidenceResult } from '@crvouga/atlas/reports/layer-merge';
|
|
25
|
+
import type { TestResultEntry, TestSummary } from '@crvouga/atlas/reports/test-catalog';
|
|
19
26
|
|
|
20
27
|
import { compareRevisions, type Behavior, type Revision, type RevisionSource } from './behavior';
|
|
21
28
|
|
|
@@ -58,6 +65,10 @@ export const VERDICTS: Readonly<Record<Verdict, { readonly label: string; readon
|
|
|
58
65
|
export type EvidenceFacts = {
|
|
59
66
|
/** When the checks ran, epoch milliseconds. The age of evidence counts from here, not from publication or enrichment. */
|
|
60
67
|
readonly executedAtMs: number;
|
|
68
|
+
/** When the evidence was published, from the `published-at` label, or null when the producer did not say. Never the age of the evidence. */
|
|
69
|
+
readonly publishedAtMs: number | null;
|
|
70
|
+
/** When the evidence was enriched (annotated, captioned, re-indexed), from the `enriched-at` label, or null. Never the age of the evidence. */
|
|
71
|
+
readonly enrichedAtMs: number | null;
|
|
61
72
|
readonly build: string | null;
|
|
62
73
|
readonly sha: string | null;
|
|
63
74
|
/** The `platform` label, else the layer's environment. */
|
|
@@ -83,8 +94,10 @@ export type Assessment = {
|
|
|
83
94
|
readonly assertion: Assertion | null;
|
|
84
95
|
/** Sentences saying why, in the order a reader needs them. Empty when there is nothing to add. */
|
|
85
96
|
readonly reasons: readonly string[];
|
|
86
|
-
/** The result this reading is of, or null when there is none. */
|
|
97
|
+
/** The result this reading is of, or null when there is none (or it was read from a test catalog, which holds its layer id instead). */
|
|
87
98
|
readonly result: EvidenceResult | null;
|
|
99
|
+
/** The id of the layer that recorded the result this reading is of, or null when there is none. */
|
|
100
|
+
readonly layer: string | null;
|
|
88
101
|
/** How many other completed results were read and set aside for this one. */
|
|
89
102
|
readonly others: number;
|
|
90
103
|
};
|
|
@@ -107,23 +120,38 @@ function labelsOf(result: EvidenceResult): Readonly<Record<string, string>> {
|
|
|
107
120
|
return { ...(result.layer.evidence?.labels ?? {}), ...(result.row.evidence?.labels ?? {}) };
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
123
|
+
/** A time a label states: epoch milliseconds or an ISO date. Anything else is no time. */
|
|
124
|
+
function labelTime(value: string | undefined): number | null {
|
|
125
|
+
const text = value?.trim();
|
|
126
|
+
if (!text) return null;
|
|
127
|
+
const at = /^\d{9,}$/.test(text) ? Number(text) : /^\d{4}-\d{2}-\d{2}/.test(text) ? Date.parse(text) : Number.NaN;
|
|
128
|
+
return Number.isFinite(at) && at > 0 ? at : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** What one result recorded about itself, whether read from the report or from the test catalog. */
|
|
132
|
+
type Recorded = {
|
|
133
|
+
readonly labels: Readonly<Record<string, string>>;
|
|
134
|
+
readonly executedAtMs: number;
|
|
135
|
+
readonly sha: string | null;
|
|
136
|
+
readonly environment: string | null;
|
|
137
|
+
readonly freshness: 'modified' | 'unknown' | null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function factsFrom(recorded: Recorded): EvidenceFacts {
|
|
141
|
+
const { labels } = recorded;
|
|
114
142
|
return {
|
|
115
|
-
executedAtMs:
|
|
143
|
+
executedAtMs: recorded.executedAtMs,
|
|
144
|
+
publishedAtMs: labelTime(labels['published-at']),
|
|
145
|
+
enrichedAtMs: labelTime(labels['enriched-at']),
|
|
116
146
|
build: labels.build?.trim() || null,
|
|
117
|
-
sha:
|
|
118
|
-
platform: labels.platform?.trim() ||
|
|
119
|
-
environment:
|
|
120
|
-
provenance:
|
|
147
|
+
sha: recorded.sha,
|
|
148
|
+
platform: labels.platform?.trim() || recorded.environment,
|
|
149
|
+
environment: recorded.environment,
|
|
150
|
+
provenance: recorded.freshness
|
|
121
151
|
};
|
|
122
152
|
}
|
|
123
153
|
|
|
124
|
-
|
|
125
|
-
export function assertionOf(result: EvidenceResult): Assertion | null {
|
|
126
|
-
const labels = labelsOf(result);
|
|
154
|
+
function assertionFrom(labels: Readonly<Record<string, string>>): Assertion | null {
|
|
127
155
|
const builds = (labels['applies-to'] ?? '')
|
|
128
156
|
.split(',')
|
|
129
157
|
.map((each) => each.trim())
|
|
@@ -131,6 +159,32 @@ export function assertionOf(result: EvidenceResult): Assertion | null {
|
|
|
131
159
|
return builds.length ? { builds, by: labels['applies-to-by']?.trim() || null } : null;
|
|
132
160
|
}
|
|
133
161
|
|
|
162
|
+
const ofResult = (result: EvidenceResult): Recorded => ({
|
|
163
|
+
labels: labelsOf(result),
|
|
164
|
+
executedAtMs: result.layer.recordedAtMs,
|
|
165
|
+
sha: result.layer.sha,
|
|
166
|
+
environment: result.layer.environment,
|
|
167
|
+
freshness: result.layer.freshness ?? null
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const ofOutcome = (outcome: TestResultEntry): Recorded => ({
|
|
171
|
+
labels: outcome.provenance ?? {},
|
|
172
|
+
executedAtMs: outcome.at,
|
|
173
|
+
sha: outcome.sha,
|
|
174
|
+
environment: outcome.environment,
|
|
175
|
+
freshness: outcome.freshness ?? null
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/** What a result's layer and labels say it ran against. */
|
|
179
|
+
export function evidenceFacts(result: EvidenceResult): EvidenceFacts {
|
|
180
|
+
return factsFrom(ofResult(result));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The assertion a result's labels make, or null when it makes none. */
|
|
184
|
+
export function assertionOf(result: EvidenceResult): Assertion | null {
|
|
185
|
+
return assertionFrom(labelsOf(result));
|
|
186
|
+
}
|
|
187
|
+
|
|
134
188
|
const RANK: Readonly<Record<Verdict, number>> = { applies: 0, unknown: 1, stale: 2, none: 3 };
|
|
135
189
|
|
|
136
190
|
/** How one result's evidence stands against the release. Never reads a branch or a status. */
|
|
@@ -193,11 +247,10 @@ function matchRelease(facts: EvidenceFacts, release: ReleaseIdentity, promise: R
|
|
|
193
247
|
return { match: 'exact', reasons };
|
|
194
248
|
}
|
|
195
249
|
|
|
196
|
-
/** One result against the promise and the release. */
|
|
197
|
-
|
|
198
|
-
const facts =
|
|
199
|
-
const
|
|
200
|
-
const assertion = assertionOf(result);
|
|
250
|
+
/** One recorded result against the promise (current and recorded fingerprints) and the release. */
|
|
251
|
+
function read(recorded: Recorded, revision: Revision, release: ReleaseIdentity, source: { result: EvidenceResult | null; layer: string }): Assessment {
|
|
252
|
+
const facts = factsFrom(recorded);
|
|
253
|
+
const assertion = assertionFrom(recorded.labels);
|
|
201
254
|
const { match, reasons } = matchRelease(facts, release, revision.match, assertion);
|
|
202
255
|
const promiseReason =
|
|
203
256
|
revision.match === 'different'
|
|
@@ -214,11 +267,36 @@ export function assessResult(result: EvidenceResult, revisions: RevisionSource,
|
|
|
214
267
|
facts,
|
|
215
268
|
assertion,
|
|
216
269
|
reasons: [...(promiseReason ? [promiseReason] : []), ...reasons],
|
|
217
|
-
result,
|
|
270
|
+
result: source.result,
|
|
271
|
+
layer: source.layer,
|
|
218
272
|
others: 0
|
|
219
273
|
};
|
|
220
274
|
}
|
|
221
275
|
|
|
276
|
+
/** One result against the promise and the release. */
|
|
277
|
+
export function assessResult(result: EvidenceResult, revisions: RevisionSource, release: ReleaseIdentity): Assessment {
|
|
278
|
+
const revision = compareRevisions(revisions.current, revisions.recorded(result.layer.id));
|
|
279
|
+
return read(ofResult(result), revision, release, { result, layer: result.layer.id });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const NOTHING = (release: ReleaseIdentity): Assessment => ({
|
|
283
|
+
verdict: 'none',
|
|
284
|
+
promise: 'unknown',
|
|
285
|
+
release: hasRelease(release) ? 'unknown' : 'not-selected',
|
|
286
|
+
facts: null,
|
|
287
|
+
assertion: null,
|
|
288
|
+
reasons: [],
|
|
289
|
+
result: null,
|
|
290
|
+
layer: null,
|
|
291
|
+
others: 0
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/** The reading that best applies (applies, then unknown, then stale; the newest execution among equals), with how many were set aside. */
|
|
295
|
+
function best(readings: Assessment[]): Assessment {
|
|
296
|
+
readings.sort((a, b) => RANK[a.verdict] - RANK[b.verdict] || b.facts!.executedAtMs - a.facts!.executedAtMs);
|
|
297
|
+
return { ...readings[0]!, others: readings.length - 1 };
|
|
298
|
+
}
|
|
299
|
+
|
|
222
300
|
/**
|
|
223
301
|
* The reading of a behavior's evidence: every completed result is read and the one that best applies is kept
|
|
224
302
|
* (applies, then unknown, then stale; the newest execution among equals), so a recording on the release's platform is
|
|
@@ -227,12 +305,29 @@ export function assessResult(result: EvidenceResult, revisions: RevisionSource,
|
|
|
227
305
|
*/
|
|
228
306
|
export function assessBehavior(behavior: Behavior, revisions: RevisionSource, release: ReleaseIdentity): Assessment {
|
|
229
307
|
const completed = (behavior.slot?.outcomes ?? []).filter((result) => result.row.status === 'passed' || result.row.status === 'failed');
|
|
230
|
-
if (completed.length === 0)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
308
|
+
if (completed.length === 0) return NOTHING(release);
|
|
309
|
+
return best(completed.map((result) => assessResult(result, revisions, release)));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The reading of a test of the catalog from its current results: the spec's fingerprint now (`test.spec.fingerprint`) against
|
|
314
|
+
* the one each result recorded (`specRevision`), and the build, commit, platform and provenance each states. The same
|
|
315
|
+
* rules as {@link assessBehavior}; a result without a recorded revision, or a test whose spec has no fingerprint, is unknown.
|
|
316
|
+
* Only completed results (passed or failed) are read; a test with none has `none`. Statuses are unchanged by this.
|
|
317
|
+
*/
|
|
318
|
+
export function assessTest(test: TestSummary, release: ReleaseIdentity): Assessment {
|
|
319
|
+
const completed = test.outcomes.filter((outcome) => outcome.status === 'passed' || outcome.status === 'failed');
|
|
320
|
+
if (completed.length === 0) return NOTHING(release);
|
|
321
|
+
const current = test.spec?.fingerprint ?? null;
|
|
322
|
+
return best(completed.map((outcome) => read(ofOutcome(outcome), compareRevisions(current, outcome.specRevision), release, { result: null, layer: outcome.layer })));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The order the explorer lists tests in when sorted by evidence: what needs a look first (stale, then unknown, then
|
|
327
|
+
* without evidence), evidence that applies last. A rank for sorting only; it is never shown as a status.
|
|
328
|
+
*/
|
|
329
|
+
export function attentionRank(assessment: Assessment): number {
|
|
330
|
+
return { stale: 0, unknown: 1, none: 2, applies: 3 }[assessment.verdict];
|
|
236
331
|
}
|
|
237
332
|
|
|
238
333
|
/** Whether a behavior belongs under "stale or unverified": anything but evidence that applies. */
|