@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
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.17.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- read evidence applicability in the test explorer's list, sort and detail ([14181aa](https://github.com/crvouga/atlas/commit/14181aa391fb824c85364c4f5d93e1a6ccede176))
|
|
8
|
+
|
|
9
|
+
## 1.16.0 (2026-10-28)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- explain catalog counts and identity in a Catalog health tab with persistent root mappings (#71) ([0974e7a](https://github.com/crvouga/atlas/commit/0974e7a8d16b126c47e79a004974a629275122ad))
|
|
14
|
+
|
|
15
|
+
## 1.15.0 (2026-10-28)
|
|
4
16
|
|
|
5
17
|
### Features
|
|
6
18
|
|
|
@@ -3,10 +3,18 @@ import type { CatalogModel } from './test-catalog.js';
|
|
|
3
3
|
export declare const CATALOG_HEALTH_VERSION = 1;
|
|
4
4
|
/** The most paths listed in one finding; its `count` is always the whole. */
|
|
5
5
|
export declare const HEALTH_ITEM_LIMIT = 25;
|
|
6
|
-
/**
|
|
6
|
+
/** The label a producer puts on a check to give its behavior (the promise) an id that outlives renames and moves. */
|
|
7
|
+
export declare const BEHAVIOR_ID_LABEL = "behavior-id";
|
|
8
|
+
/** The label a producer puts on a check to give one example of a scenario outline an id of its own. */
|
|
9
|
+
export declare const EXAMPLE_ID_LABEL = "example-id";
|
|
10
|
+
/**
|
|
11
|
+
* Declares that paths under `from` are the same specs as paths under `to`, e.g. `{ from: '', to: 'specs/' }`. Empty `from`
|
|
12
|
+
* matches every path. `source` says where it was declared (the server's configuration or the request); it changes nothing.
|
|
13
|
+
*/
|
|
7
14
|
export type RootMapping = {
|
|
8
15
|
readonly from: string;
|
|
9
16
|
readonly to: string;
|
|
17
|
+
readonly source?: 'configuration' | 'request';
|
|
10
18
|
};
|
|
11
19
|
/** One thing worth a look. */
|
|
12
20
|
export type HealthItem = {
|
|
@@ -18,7 +26,7 @@ export type HealthItem = {
|
|
|
18
26
|
}[];
|
|
19
27
|
};
|
|
20
28
|
/** Codes of `HealthFinding.code`. The set grows; a reader treats an unknown code as informational. */
|
|
21
|
-
export type HealthCode = 'ambiguous-spec-path' | 'unmatched-spec-path' | 'duplicate-spec-content' | 'duplicate-spec-title' | 'generic-domain' | 'tests-without-spec' | 'no-spec-definitions' | 'fallback-identity' | 'unused-root-mapping' | 'spec-without-execution' | 'historical-only';
|
|
29
|
+
export type HealthCode = 'ambiguous-spec-path' | 'unmatched-spec-path' | 'duplicate-spec-content' | 'duplicate-spec-title' | 'generic-domain' | 'tests-without-spec' | 'no-spec-definitions' | 'fallback-identity' | 'unused-root-mapping' | 'declared-id-split' | 'spec-without-execution' | 'historical-only';
|
|
22
30
|
/** A finding: what is inconsistent, how many it affects, and what to change. */
|
|
23
31
|
export type HealthFinding = {
|
|
24
32
|
readonly code: HealthCode;
|
|
@@ -100,6 +108,10 @@ export type CatalogHealth = {
|
|
|
100
108
|
readonly checks: number;
|
|
101
109
|
/** Checks whose id equals their title: no stable id was given. */
|
|
102
110
|
readonly fallbackIdentity: number;
|
|
111
|
+
/** Checks that declare a `behavior-id` label. Optional: absent from documents written before it existed. */
|
|
112
|
+
readonly declaredBehaviorIds?: number;
|
|
113
|
+
/** Checks that declare an `example-id` label. */
|
|
114
|
+
readonly declaredExampleIds?: number;
|
|
103
115
|
readonly basis: string;
|
|
104
116
|
};
|
|
105
117
|
readonly domains: readonly {
|
|
@@ -110,6 +122,7 @@ export type CatalogHealth = {
|
|
|
110
122
|
readonly from: string;
|
|
111
123
|
readonly to: string;
|
|
112
124
|
readonly matched: number;
|
|
125
|
+
readonly source?: 'configuration' | 'request';
|
|
113
126
|
}[];
|
|
114
127
|
readonly aliases: readonly SpecAlias[];
|
|
115
128
|
readonly findings: readonly HealthFinding[];
|
|
@@ -7,6 +7,10 @@ export const HEALTH_ITEM_LIMIT = 25;
|
|
|
7
7
|
const CANDIDATE_LIMIT = 5;
|
|
8
8
|
/** Folder names that say where a spec lives, not what it is about. A domain with one of these names is likely a root folder. */
|
|
9
9
|
const GENERIC_DOMAINS = new Set(['specs', 'spec', 'product', 'features', 'feature', 'tests', 'test', 'e2e', 'src', 'app', 'docs', 'overview', 'root', 'default']);
|
|
10
|
+
/** The label a producer puts on a check to give its behavior (the promise) an id that outlives renames and moves. */
|
|
11
|
+
export const BEHAVIOR_ID_LABEL = 'behavior-id';
|
|
12
|
+
/** The label a producer puts on a check to give one example of a scenario outline an id of its own. */
|
|
13
|
+
export const EXAMPLE_ID_LABEL = 'example-id';
|
|
10
14
|
/** Parses `from>to` (as `?root=` gives it) into a mapping. Returns null when there is no `>`. Whitespace around each side is trimmed. */
|
|
11
15
|
export function parseRootMapping(text) {
|
|
12
16
|
const at = text.indexOf('>');
|
|
@@ -164,6 +168,21 @@ export function catalogHealth(model, roots = []) {
|
|
|
164
168
|
const fallback = model.tests.filter(test => test.id === test.title);
|
|
165
169
|
if (fallback.length)
|
|
166
170
|
findings.push(finding('fallback-identity', 'info', 'These checks use their title as their id. A check is its spec path and id, so renaming one starts a new check and splits its history. Have the producer send a stable `id`.', fallback.map(test => ({ path: test.id })), fallback.length));
|
|
171
|
+
const split = [];
|
|
172
|
+
for (const label of [BEHAVIOR_ID_LABEL, EXAMPLE_ID_LABEL]) {
|
|
173
|
+
const byId = new Map();
|
|
174
|
+
for (const test of model.tests) {
|
|
175
|
+
const id = test.labels[label];
|
|
176
|
+
if (id)
|
|
177
|
+
byId.set(id, [...(byId.get(id) ?? []), test]);
|
|
178
|
+
}
|
|
179
|
+
for (const [id, tests] of [...byId].sort(([a], [b]) => byText(a, b))) {
|
|
180
|
+
if (tests.length > 1)
|
|
181
|
+
split.push({ path: `${label}=${id}`, candidates: tests.map(test => test.specPath ? `${test.specPath} ${test.id}` : test.id).sort(byText).slice(0, CANDIDATE_LIMIT).map(path => ({ path, reason: `same ${label}` })) });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (split.length)
|
|
185
|
+
findings.push(finding('declared-id-split', 'info', 'These declared ids sit on more than one check: the promise was renamed or moved, or two checks claim it. Atlas keeps the checks apart (a check is its spec path and id), so their histories are separate.', split));
|
|
167
186
|
const unused = ordered.filter(mapping => !matched.get(mapping)?.size);
|
|
168
187
|
if (unused.length)
|
|
169
188
|
findings.push(finding('unused-root-mapping', 'warning', 'These root mappings matched no spec path. Check `from` against the paths in the report.', unused.map(mapping => ({ path: `${mapping.from}>${mapping.to}` }))));
|
|
@@ -213,10 +232,12 @@ export function catalogHealth(model, roots = []) {
|
|
|
213
232
|
identity: {
|
|
214
233
|
checks: model.tests.length,
|
|
215
234
|
fallbackIdentity: fallback.length,
|
|
216
|
-
|
|
235
|
+
declaredBehaviorIds: model.tests.filter(test => test.labels[BEHAVIOR_ID_LABEL]).length,
|
|
236
|
+
declaredExampleIds: model.tests.filter(test => test.labels[EXAMPLE_ID_LABEL]).length,
|
|
237
|
+
basis: 'A check is its spec path and its id. A check whose producer sent no id is known by its title, so a rename is a new check; a spec is known by its path, joined to another only by a declared root mapping. The labels behavior-id and example-id are counted and checked for ids shared by several checks; they do not change which check a result belongs to.'
|
|
217
238
|
},
|
|
218
239
|
domains,
|
|
219
|
-
roots: ordered.map(mapping => ({ from: mapping.from, to: mapping.to, matched: matched.get(mapping)?.size ?? 0 })),
|
|
240
|
+
roots: ordered.map(mapping => ({ from: mapping.from, to: mapping.to, matched: matched.get(mapping)?.size ?? 0, ...(mapping.source ? { source: mapping.source } : {}) })),
|
|
220
241
|
aliases,
|
|
221
242
|
findings
|
|
222
243
|
};
|
|
@@ -6,6 +6,13 @@ import { RecordedSpecs } from './recorded-specs.js';
|
|
|
6
6
|
export declare const TEST_CATALOG_VERSION = 1;
|
|
7
7
|
/** Recorded results kept per test, newest first. Older ones are counted in no statistic. */
|
|
8
8
|
export declare const HISTORY_PER_TEST = 100;
|
|
9
|
+
/**
|
|
10
|
+
* The open `labels` (see docs/extending.md) that say what a result's evidence ran against and who vouches for it: `build`,
|
|
11
|
+
* `platform`, `applies-to` (builds or commits it is asserted to still hold for), `applies-to-by` (the policy or person that
|
|
12
|
+
* asserts it), `published-at` and `enriched-at` (when the evidence was published or enriched, epoch milliseconds or an ISO
|
|
13
|
+
* date: never when the checks ran). A test's current results carry them as `provenance`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const APPLICABILITY_LABELS: readonly ["build", "platform", "applies-to", "applies-to-by", "published-at", "enriched-at"];
|
|
9
16
|
/** One recorded result of a test: when, where, how it went and how long it took, and the layer and run it came from. */
|
|
10
17
|
export type TestResultEntry = {
|
|
11
18
|
/** When it was recorded, epoch ms. */
|
|
@@ -32,6 +39,10 @@ export type TestResultEntry = {
|
|
|
32
39
|
readonly detail: string;
|
|
33
40
|
/** The fingerprint of its spec text the layer recorded, when it recorded one: results with different revisions were recorded against different promises. Absent means not recorded, never "the same". */
|
|
34
41
|
readonly specRevision?: string;
|
|
42
|
+
/** `modified`: recorded from a checkout with uncommitted changes. `unknown`: origin not captured. Absent for a clean checkout, and on results other than a test's current ones. Advisory; never changes a status. */
|
|
43
|
+
readonly freshness?: 'unknown' | 'modified';
|
|
44
|
+
/** The labels of its layer and check that say what it ran against and who vouches for it: only those named in {@link APPLICABILITY_LABELS}. Present on a test's current results when it has any. Read by the viewer's evidence applicability; never decides a result. */
|
|
45
|
+
readonly provenance?: Readonly<Record<string, string>>;
|
|
35
46
|
/** What it carried beyond its outcome (attachments, steps, retries, tags, labels), when anything. */
|
|
36
47
|
readonly evidence?: EvidenceSummary;
|
|
37
48
|
/** Its product-latency measurements (`kind: product-latency`), when it recorded any. Separate from `durationMs`, which is how long the test took (setup, waits and pacing included). */
|
|
@@ -82,6 +93,8 @@ export type CatalogSpec = {
|
|
|
82
93
|
/** The group it belongs to: the spec's own domain, else the folder under `specs/`. */
|
|
83
94
|
readonly domain: string;
|
|
84
95
|
readonly kind: string;
|
|
96
|
+
/** The fingerprint of the spec text now (the promise), when the spec set or a layer recorded one. A result's `specRevision` is compared with it: different means the promise changed after the result was recorded. Absent: unknown, never "the same". */
|
|
97
|
+
readonly fingerprint?: string;
|
|
85
98
|
};
|
|
86
99
|
/** One test: what it is, its current status per environment, and how it has gone. */
|
|
87
100
|
export type TestSummary = {
|
|
@@ -34,6 +34,13 @@ import { RecordedSpecs } from './recorded-specs.js';
|
|
|
34
34
|
export const TEST_CATALOG_VERSION = 1;
|
|
35
35
|
/** Recorded results kept per test, newest first. Older ones are counted in no statistic. */
|
|
36
36
|
export const HISTORY_PER_TEST = 100;
|
|
37
|
+
/**
|
|
38
|
+
* The open `labels` (see docs/extending.md) that say what a result's evidence ran against and who vouches for it: `build`,
|
|
39
|
+
* `platform`, `applies-to` (builds or commits it is asserted to still hold for), `applies-to-by` (the policy or person that
|
|
40
|
+
* asserts it), `published-at` and `enriched-at` (when the evidence was published or enriched, epoch milliseconds or an ISO
|
|
41
|
+
* date: never when the checks ran). A test's current results carry them as `provenance`.
|
|
42
|
+
*/
|
|
43
|
+
export const APPLICABILITY_LABELS = ['build', 'platform', 'applies-to', 'applies-to-by', 'published-at', 'enriched-at'];
|
|
37
44
|
const FAILURE_RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 };
|
|
38
45
|
/** How tests are ordered. */
|
|
39
46
|
export const TEST_SORTS = ['spec', 'title', 'recent', 'slowest', 'failures', 'flaky', 'runs'];
|
|
@@ -409,6 +416,7 @@ export function catalogSpec(path, known) {
|
|
|
409
416
|
title: entry?.title || (base ? base[0].toUpperCase() + base.slice(1) : path),
|
|
410
417
|
domain: entry?.domain || folder,
|
|
411
418
|
kind: entry?.kind || (/\.([^./]+)$/.exec(path)?.[1] ?? 'unknown'),
|
|
419
|
+
...(entry?.fingerprint ? { fingerprint: entry.fingerprint } : {}),
|
|
412
420
|
};
|
|
413
421
|
}
|
|
414
422
|
function statsOf(history, outcomes, tags, labels) {
|
|
@@ -464,11 +472,18 @@ function statsOf(history, outcomes, tags, labels) {
|
|
|
464
472
|
firstSeenMs, lastSeenMs, lastPassedAtMs, lastFailedAtMs, runs: runs.size, branches,
|
|
465
473
|
};
|
|
466
474
|
}
|
|
467
|
-
|
|
468
|
-
|
|
475
|
+
/** A current result with what the layer recorded about its spec and about what it ran against (see {@link APPLICABILITY_LABELS}). */
|
|
476
|
+
function outcomeOf(result, fingerprints = {}) {
|
|
477
|
+
const labels = { ...(result.layer.evidence?.labels ?? {}), ...(result.row.evidence?.labels ?? {}) };
|
|
478
|
+
const provenance = Object.fromEntries(APPLICABILITY_LABELS.filter(name => typeof labels[name] === 'string').map(name => [name, labels[name]]));
|
|
479
|
+
return {
|
|
480
|
+
...entryOf(result.layer, result.row, result.layer.backend, fingerprints),
|
|
481
|
+
...(result.layer.freshness ? { freshness: result.layer.freshness } : {}),
|
|
482
|
+
...(Object.keys(provenance).length ? { provenance } : {}),
|
|
483
|
+
};
|
|
469
484
|
}
|
|
470
|
-
function summaryOf(slot, history, known) {
|
|
471
|
-
const outcomes = slot.outcomes.map(outcomeOf);
|
|
485
|
+
function summaryOf(slot, history, known, fingerprintsOf) {
|
|
486
|
+
const outcomes = slot.outcomes.map(result => outcomeOf(result, fingerprintsOf(result.layer.id)));
|
|
472
487
|
const tags = new Set();
|
|
473
488
|
const artifactTags = new Set();
|
|
474
489
|
const inheritedTags = new Set();
|
|
@@ -586,7 +601,8 @@ export function catalogModel(layers, sources = {}) {
|
|
|
586
601
|
const known = (path) => recorded.get(path);
|
|
587
602
|
const combined = combineEvidence({ specs: [], sha: null, changedFiles: [] }, layers);
|
|
588
603
|
const slots = checkSlots(combined.unassigned);
|
|
589
|
-
const
|
|
604
|
+
const fingerprints = new Map(layers.map(entry => [entry.layer.id, entry.fingerprints]));
|
|
605
|
+
const tests = slots.map(slot => summaryOf(slot, history, known, id => fingerprints.get(id)));
|
|
590
606
|
const byKey = new Map(tests.map(test => [test.key, test]));
|
|
591
607
|
const haystacks = new Map(tests.map(test => [test.key, haystackOf(test)]));
|
|
592
608
|
const counts = emptyCounts();
|
package/dist/schema/src/api.d.ts
CHANGED
|
@@ -2255,6 +2255,11 @@ export declare const TestResultEntrySchema: z.ZodObject<{
|
|
|
2255
2255
|
backend: z.ZodString;
|
|
2256
2256
|
detail: z.ZodString;
|
|
2257
2257
|
specRevision: z.ZodOptional<z.ZodString>;
|
|
2258
|
+
freshness: z.ZodOptional<z.ZodEnum<{
|
|
2259
|
+
unknown: "unknown";
|
|
2260
|
+
modified: "modified";
|
|
2261
|
+
}>>;
|
|
2262
|
+
provenance: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
2258
2263
|
evidence: z.ZodOptional<z.ZodObject<{
|
|
2259
2264
|
attachments: z.ZodNumber;
|
|
2260
2265
|
steps: z.ZodNumber;
|
|
@@ -2329,6 +2334,7 @@ export declare const TestSummarySchema: z.ZodObject<{
|
|
|
2329
2334
|
title: z.ZodString;
|
|
2330
2335
|
domain: z.ZodString;
|
|
2331
2336
|
kind: z.ZodString;
|
|
2337
|
+
fingerprint: z.ZodOptional<z.ZodString>;
|
|
2332
2338
|
}, z.core.$loose>>;
|
|
2333
2339
|
status: z.ZodEnum<{
|
|
2334
2340
|
passed: "passed";
|
|
@@ -2359,6 +2365,11 @@ export declare const TestSummarySchema: z.ZodObject<{
|
|
|
2359
2365
|
backend: z.ZodString;
|
|
2360
2366
|
detail: z.ZodString;
|
|
2361
2367
|
specRevision: z.ZodOptional<z.ZodString>;
|
|
2368
|
+
freshness: z.ZodOptional<z.ZodEnum<{
|
|
2369
|
+
unknown: "unknown";
|
|
2370
|
+
modified: "modified";
|
|
2371
|
+
}>>;
|
|
2372
|
+
provenance: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
2362
2373
|
evidence: z.ZodOptional<z.ZodObject<{
|
|
2363
2374
|
attachments: z.ZodNumber;
|
|
2364
2375
|
steps: z.ZodNumber;
|
|
@@ -2586,6 +2597,7 @@ export declare const TestCatalogSchema: z.ZodObject<{
|
|
|
2586
2597
|
title: z.ZodString;
|
|
2587
2598
|
domain: z.ZodString;
|
|
2588
2599
|
kind: z.ZodString;
|
|
2600
|
+
fingerprint: z.ZodOptional<z.ZodString>;
|
|
2589
2601
|
tests: z.ZodNumber;
|
|
2590
2602
|
counts: z.ZodRecord<z.ZodEnum<{
|
|
2591
2603
|
passed: "passed";
|
|
@@ -2628,6 +2640,7 @@ export declare const TestCatalogSchema: z.ZodObject<{
|
|
|
2628
2640
|
title: z.ZodString;
|
|
2629
2641
|
domain: z.ZodString;
|
|
2630
2642
|
kind: z.ZodString;
|
|
2643
|
+
fingerprint: z.ZodOptional<z.ZodString>;
|
|
2631
2644
|
}, z.core.$loose>>;
|
|
2632
2645
|
status: z.ZodEnum<{
|
|
2633
2646
|
passed: "passed";
|
|
@@ -2658,6 +2671,11 @@ export declare const TestCatalogSchema: z.ZodObject<{
|
|
|
2658
2671
|
backend: z.ZodString;
|
|
2659
2672
|
detail: z.ZodString;
|
|
2660
2673
|
specRevision: z.ZodOptional<z.ZodString>;
|
|
2674
|
+
freshness: z.ZodOptional<z.ZodEnum<{
|
|
2675
|
+
unknown: "unknown";
|
|
2676
|
+
modified: "modified";
|
|
2677
|
+
}>>;
|
|
2678
|
+
provenance: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
2661
2679
|
evidence: z.ZodOptional<z.ZodObject<{
|
|
2662
2680
|
attachments: z.ZodNumber;
|
|
2663
2681
|
steps: z.ZodNumber;
|
|
@@ -2808,6 +2826,8 @@ export declare const CatalogHealthSchema: z.ZodObject<{
|
|
|
2808
2826
|
identity: z.ZodObject<{
|
|
2809
2827
|
checks: z.ZodNumber;
|
|
2810
2828
|
fallbackIdentity: z.ZodNumber;
|
|
2829
|
+
declaredBehaviorIds: z.ZodOptional<z.ZodNumber>;
|
|
2830
|
+
declaredExampleIds: z.ZodOptional<z.ZodNumber>;
|
|
2811
2831
|
basis: z.ZodString;
|
|
2812
2832
|
}, z.core.$loose>;
|
|
2813
2833
|
domains: z.ZodArray<z.ZodObject<{
|
|
@@ -2818,6 +2838,10 @@ export declare const CatalogHealthSchema: z.ZodObject<{
|
|
|
2818
2838
|
from: z.ZodString;
|
|
2819
2839
|
to: z.ZodString;
|
|
2820
2840
|
matched: z.ZodNumber;
|
|
2841
|
+
source: z.ZodOptional<z.ZodEnum<{
|
|
2842
|
+
request: "request";
|
|
2843
|
+
configuration: "configuration";
|
|
2844
|
+
}>>;
|
|
2821
2845
|
}, z.core.$loose>>;
|
|
2822
2846
|
aliases: z.ZodArray<z.ZodObject<{
|
|
2823
2847
|
path: z.ZodString;
|
|
@@ -2854,6 +2878,7 @@ export declare const TestHistorySchema: z.ZodObject<{
|
|
|
2854
2878
|
title: z.ZodString;
|
|
2855
2879
|
domain: z.ZodString;
|
|
2856
2880
|
kind: z.ZodString;
|
|
2881
|
+
fingerprint: z.ZodOptional<z.ZodString>;
|
|
2857
2882
|
}, z.core.$loose>>;
|
|
2858
2883
|
status: z.ZodEnum<{
|
|
2859
2884
|
passed: "passed";
|
|
@@ -2884,6 +2909,11 @@ export declare const TestHistorySchema: z.ZodObject<{
|
|
|
2884
2909
|
backend: z.ZodString;
|
|
2885
2910
|
detail: z.ZodString;
|
|
2886
2911
|
specRevision: z.ZodOptional<z.ZodString>;
|
|
2912
|
+
freshness: z.ZodOptional<z.ZodEnum<{
|
|
2913
|
+
unknown: "unknown";
|
|
2914
|
+
modified: "modified";
|
|
2915
|
+
}>>;
|
|
2916
|
+
provenance: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
2887
2917
|
evidence: z.ZodOptional<z.ZodObject<{
|
|
2888
2918
|
attachments: z.ZodNumber;
|
|
2889
2919
|
steps: z.ZodNumber;
|
|
@@ -3007,6 +3037,11 @@ export declare const TestHistorySchema: z.ZodObject<{
|
|
|
3007
3037
|
backend: z.ZodString;
|
|
3008
3038
|
detail: z.ZodString;
|
|
3009
3039
|
specRevision: z.ZodOptional<z.ZodString>;
|
|
3040
|
+
freshness: z.ZodOptional<z.ZodEnum<{
|
|
3041
|
+
unknown: "unknown";
|
|
3042
|
+
modified: "modified";
|
|
3043
|
+
}>>;
|
|
3044
|
+
provenance: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
3010
3045
|
evidence: z.ZodOptional<z.ZodObject<{
|
|
3011
3046
|
attachments: z.ZodNumber;
|
|
3012
3047
|
steps: z.ZodNumber;
|
package/dist/schema/src/api.js
CHANGED
|
@@ -432,13 +432,18 @@ export const TestResultEntrySchema = z.looseObject({
|
|
|
432
432
|
backend: z.string(),
|
|
433
433
|
detail: z.string().describe('Why it has its status, at most 500 characters'),
|
|
434
434
|
specRevision: z.string().optional().describe('The fingerprint of its spec text that the layer recorded, when it recorded one. Two results with different revisions were recorded against different promises. Absent: not recorded, never assumed to match'),
|
|
435
|
+
freshness: z.enum(['modified', 'unknown']).optional().describe('`modified`: recorded from a checkout with uncommitted changes. `unknown`: origin not captured. Absent for a clean checkout, on results other than a test\'s current ones, and from an older server. Advisory; never changes a status'),
|
|
436
|
+
provenance: z.record(z.string(), z.string()).optional().describe('The labels of its layer and check that say what it ran against and who vouches for it: `build`, `platform`, `applies-to`, `applies-to-by`, `published-at`, `enriched-at`. Present on a test\'s current results when it has any, and absent from an older server. Never decides a result'),
|
|
435
437
|
evidence: EvidenceSummarySchema.optional(),
|
|
436
438
|
latency: z.array(MeasurementSchema).optional().describe('Its product-latency measurements (`kind: product-latency`), when it recorded any. Separate from `durationMs`, which is how long the test took')
|
|
437
439
|
});
|
|
438
440
|
const DurationStatsSchema = z.looseObject({
|
|
439
441
|
count: z.number().int().nonnegative(), totalMs: z.number(), avgMs: z.number(), p50Ms: z.number(), p95Ms: z.number(), minMs: z.number(), maxMs: z.number(), lastMs: z.number()
|
|
440
442
|
});
|
|
441
|
-
const CatalogSpecSchema = z.looseObject({
|
|
443
|
+
const CatalogSpecSchema = z.looseObject({
|
|
444
|
+
path: z.string(), title: z.string(), domain: z.string(), kind: z.string(),
|
|
445
|
+
fingerprint: z.string().optional().describe('The fingerprint of the spec text now (the promise). Compare it with a result\'s `specRevision`: different means the promise changed after the result was recorded. Absent: unknown, never "the same". Absent from an older server')
|
|
446
|
+
});
|
|
442
447
|
/** One test of the catalog: what it is, its current result per environment, and how it has gone over its recorded history. */
|
|
443
448
|
export const TestSummarySchema = z.looseObject({
|
|
444
449
|
key: z.string().describe('The test\'s identity, `JSON.stringify([specPath, id])`. Pass it to `/api/reports/tests/history?key=`'),
|
|
@@ -533,9 +538,15 @@ export const CatalogHealthSchema = z.looseObject({
|
|
|
533
538
|
testsWithoutSpec: nonneg,
|
|
534
539
|
explanation: z.array(z.string())
|
|
535
540
|
}),
|
|
536
|
-
identity: z.looseObject({
|
|
541
|
+
identity: z.looseObject({
|
|
542
|
+
checks: nonneg,
|
|
543
|
+
fallbackIdentity: nonneg.describe('Checks whose id equals their title'),
|
|
544
|
+
declaredBehaviorIds: nonneg.optional().describe('Checks that declare a `behavior-id` label'),
|
|
545
|
+
declaredExampleIds: nonneg.optional().describe('Checks that declare an `example-id` label'),
|
|
546
|
+
basis: z.string()
|
|
547
|
+
}),
|
|
537
548
|
domains: z.array(z.looseObject({ name: z.string(), specs: nonneg })).describe('Specs per domain, most first'),
|
|
538
|
-
roots: z.array(z.looseObject({ from: z.string(), to: z.string(), matched: nonneg.describe('Distinct paths the mapping changed') })),
|
|
549
|
+
roots: z.array(z.looseObject({ from: z.string(), to: z.string(), matched: nonneg.describe('Distinct paths the mapping changed'), source: z.enum(['configuration', 'request']).optional().describe('Where it was declared: the server\'s `catalog.roots` setting or this request\'s `root` parameter') })),
|
|
539
550
|
aliases: z.array(z.looseObject({ path: z.string(), sources: z.array(z.string()) })).describe('Logical specs seen under more than one original path, joined by a root mapping'),
|
|
540
551
|
findings: z.array(z.looseObject({
|
|
541
552
|
code: z.string().describe('An open set: treat a code you do not know as informational'),
|
|
@@ -66,6 +66,9 @@ export declare const ServerConfigSchema: z.ZodObject<{
|
|
|
66
66
|
branches: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
67
67
|
ignoreModified: z.ZodOptional<z.ZodBoolean>;
|
|
68
68
|
}, z.core.$strict>>;
|
|
69
|
+
catalog: z.ZodDefault<z.ZodObject<{
|
|
70
|
+
roots: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
71
|
+
}, z.core.$strict>>;
|
|
69
72
|
refreshSeconds: z.ZodDefault<z.ZodNumber>;
|
|
70
73
|
limits: z.ZodDefault<z.ZodObject<{
|
|
71
74
|
maxObjectMiB: z.ZodDefault<z.ZodNumber>;
|
|
@@ -50,6 +50,11 @@ export const ServerConfigSchema = z.strictObject({
|
|
|
50
50
|
ignoreModified: z.boolean().optional().describe('Leave out layers from modified checkouts and unknown origins')
|
|
51
51
|
})
|
|
52
52
|
.default({}),
|
|
53
|
+
catalog: z
|
|
54
|
+
.strictObject({
|
|
55
|
+
roots: z.array(z.string().refine((value) => value.includes('>'), { message: 'Write a root mapping as from>to, e.g. ">specs/" (paths under `from` are the same specs as paths under `to`)' })).default([]).describe('Root mappings `from>to` that relate spec paths a producer publishes to the paths the specs are recorded under (an empty `from` matches every path). The catalog health report applies them to every request, beside any `root` the request adds')
|
|
56
|
+
})
|
|
57
|
+
.default({ roots: [] }),
|
|
53
58
|
refreshSeconds: z.number().min(1).default(10).describe('How often the backends are looked at for layers written by something other than this server'),
|
|
54
59
|
limits: z
|
|
55
60
|
.strictObject({
|
package/dist/server/src/app.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export type AtlasServerOptions = {
|
|
|
30
30
|
/** The built viewer. Without it, only the API is served. */
|
|
31
31
|
readonly site?: string;
|
|
32
32
|
readonly outlineBranch?: string;
|
|
33
|
+
/** Root mappings `from>to` the catalog health report always applies (the `catalog.roots` setting), beside any `root` a request adds. */
|
|
34
|
+
readonly catalogRoots?: readonly string[];
|
|
33
35
|
readonly include?: (layer: LayerHeader) => boolean;
|
|
34
36
|
readonly refreshMs?: number;
|
|
35
37
|
readonly limits?: {
|
package/dist/server/src/app.js
CHANGED
|
@@ -26,7 +26,7 @@ import { reportStatus } from '../../reports/src/report-status.js';
|
|
|
26
26
|
import { ReportIndex } from '../../reports/src/report-index.js';
|
|
27
27
|
import { listRunSummaries, readRunFile } from '../../reports/src/run-bundle.js';
|
|
28
28
|
import { latestSpecSet, readSpecBody } from '../../reports/src/spec-set.js';
|
|
29
|
-
import { catalogHealth, healthMarkdown, parseRootMappings } from '../../reports/src/catalog-health.js';
|
|
29
|
+
import { catalogHealth, healthMarkdown, parseRootMapping, parseRootMappings } from '../../reports/src/catalog-health.js';
|
|
30
30
|
import { parseArtifactFilter } from '../../reports/src/artifact-query.js';
|
|
31
31
|
import { filterUiCatalog, pageUiCatalog, parseUiCatalogQuery, uiCatalog, uiCatalogMarkdown } from '../../reports/src/ui-catalog.js';
|
|
32
32
|
import { parseReliabilityScope, reliability, reliabilityMarkdown } from '../../reports/src/reliability.js';
|
|
@@ -451,12 +451,16 @@ function createProjectApplication(options) {
|
|
|
451
451
|
const document = () => testCatalog(found, query);
|
|
452
452
|
return answer(c, found.version, document, () => catalogMarkdown(document()), `tests|${new URL(c.req.url).search}|${wantsMarkdown(c)}`);
|
|
453
453
|
});
|
|
454
|
+
const configuredRoots = (options.catalogRoots ?? []).flatMap(text => parseRootMapping(text) ?? []).map(mapping => ({ ...mapping, source: 'configuration' }));
|
|
454
455
|
app.get('/api/reports/tests/health', async (c) => {
|
|
455
456
|
if (!index.ready)
|
|
456
457
|
return notReady(c);
|
|
457
458
|
const found = await catalog();
|
|
458
459
|
const params = new URL(c.req.url).searchParams;
|
|
459
|
-
|
|
460
|
+
// The mappings the operator configured, then those this request adds (one already configured is not counted twice).
|
|
461
|
+
const requested = parseRootMappings(params).filter(mapping => !configuredRoots.some(item => item.from === mapping.from && item.to === mapping.to));
|
|
462
|
+
const roots = [...configuredRoots, ...requested.map(mapping => ({ ...mapping, source: 'request' }))];
|
|
463
|
+
const document = () => catalogHealth(found, roots);
|
|
460
464
|
return answer(c, found.version, document, () => healthMarkdown(document()), `health|${params.getAll('root').join('\u0001')}|${wantsMarkdown(c)}`);
|
|
461
465
|
});
|
|
462
466
|
app.get('/api/reports/tests/history', async (c) => {
|
|
@@ -57,6 +57,7 @@ export const SERVER_SETTINGS = [
|
|
|
57
57
|
{ summary: 'Whose specs the report is laid over', key: 'outlineBranch', flag: '--outline-branch <branch>', env: ['ATLAS_OUTLINE_BRANCH'], default: '`main`, then `master`, then the newest', example: { value: 'trunk', read: c => c.outlineBranch, expected: 'trunk' } },
|
|
58
58
|
{ summary: 'Only layers recorded on these branches take part', key: 'include.branches', flag: '--branch <name>` (repeatable)', env: ['ATLAS_ONLY_BRANCH'], default: 'all', example: { value: 'main', read: c => c.include.branches, expected: ['main'] } },
|
|
59
59
|
{ summary: 'Leave out layers from modified checkouts and unknown origins', key: 'include.ignoreModified', flag: '--ignore-modified', env: ['ATLAS_IGNORE_MODIFIED'], default: 'no', example: { value: '1', read: c => c.include.ignoreModified, expected: true } },
|
|
60
|
+
{ summary: 'Root mappings `from>to` that join spec paths a producer publishes to the paths the specs are recorded under, for the catalog health report (comma-separated; an empty `from` matches every path)', key: 'catalog.roots', flag: null, env: ['ATLAS_CATALOG_ROOTS'], default: 'none', example: { value: '>specs/,apps/>specs/web/', read: c => c.catalog.roots, expected: ['>specs/', 'apps/>specs/web/'] } },
|
|
60
61
|
{ summary: 'How often (seconds) the backends are looked at for layers written by something else', key: 'refreshSeconds', flag: '--refresh <seconds>', env: ['ATLAS_REFRESH_SECONDS'], default: '`10`', example: { value: '30', read: c => c.refreshSeconds, expected: 30 } },
|
|
61
62
|
{ summary: 'Let anyone look, not upload', key: 'auth.anonymousRead', flag: '--anonymous-read', env: ['ATLAS_ANONYMOUS_READ'], default: 'no', example: { value: '1', read: c => c.anonymousRead, expected: true } },
|
|
62
63
|
{ summary: 'Listen publicly with no tokens (something in front signs people in)', key: 'auth.insecure', flag: '--insecure', env: ['ATLAS_INSECURE'], default: 'no', example: { value: '1', read: c => c.insecure, expected: true } },
|
|
@@ -151,6 +152,9 @@ export function loadServerConfig(argv, env, cwd, read = file => readFileSync(fil
|
|
|
151
152
|
limits[key] = Number(env[variable]);
|
|
152
153
|
}
|
|
153
154
|
merged.limits = limits;
|
|
155
|
+
const roots = list(env.ATLAS_CATALOG_ROOTS);
|
|
156
|
+
if (roots.length)
|
|
157
|
+
merged.catalog = { ...raw.catalog, roots };
|
|
154
158
|
if (flags.insecure || truthy(env.ATLAS_INSECURE))
|
|
155
159
|
auth.insecure = true;
|
|
156
160
|
if (flags['anonymous-read'] || truthy(env.ATLAS_ANONYMOUS_READ))
|
package/dist/server/src/main.js
CHANGED
|
@@ -56,6 +56,7 @@ export function serverOptionsOf(config, backends, log) {
|
|
|
56
56
|
backends, write: S3_BACKEND_ID,
|
|
57
57
|
tokens: config.tokens, anonymousRead: config.anonymousRead,
|
|
58
58
|
...(config.outlineBranch ? { outlineBranch: config.outlineBranch } : {}), ...(include ? { include } : {}),
|
|
59
|
+
...(config.catalog.roots.length ? { catalogRoots: config.catalog.roots } : {}),
|
|
59
60
|
runTimeoutMs: config.runTimeoutSeconds * 1000, runStallMs: config.runStallSeconds * 1000, refreshMs: config.refreshSeconds * 1000, limits: config.limits, heartbeatMs: config.heartbeatSeconds * 1000,
|
|
60
61
|
...(config.log && log ? { log } : {})
|
|
61
62
|
};
|
|
@@ -313,10 +313,10 @@ const BASE_OPERATIONS = [
|
|
|
313
313
|
'',
|
|
314
314
|
'`counts` keeps the denominators apart: spec files, defined specs, specs with an executed test (and those without: the defined coverage gaps), tests, outlines, examples and per-environment observations. `reconciliation` walks from the Specs count to the Tests count. `findings` name ambiguous and unmatched spec paths, identical or same-titled specs, a domain shared by nearly every spec, tests with no spec, checks that fall back to their title as identity and root mappings that matched nothing.',
|
|
315
315
|
'',
|
|
316
|
-
'Two paths are one logical spec only when a `root` mapping relates them (`from>to`: paths under `from` are the same specs as paths under `to`; an empty `from` matches every path). Without one, similar paths are reported as ambiguous and never merged. A check\'s label `outline` names the outline its example belongs to. Send `format=markdown` for Markdown.'
|
|
316
|
+
'Two paths are one logical spec only when a `root` mapping relates them (`from>to`: paths under `from` are the same specs as paths under `to`; an empty `from` matches every path). Without one, similar paths are reported as ambiguous and never merged. A check\'s label `outline` names the outline its example belongs to; the labels `behavior-id` and `example-id` declare stable ids, which are counted and checked for ids shared by several checks. Send `format=markdown` for Markdown.'
|
|
317
317
|
].join('\n'),
|
|
318
318
|
parameters: [
|
|
319
|
-
{ name: 'root', in: 'query', description: 'A root mapping `from>to`, URL-encoded. Repeatable or comma-separated.', schema: str, example: '>specs/' },
|
|
319
|
+
{ name: 'root', in: 'query', description: 'A root mapping `from>to`, URL-encoded. Repeatable or comma-separated. It is added to the mappings the server is configured with (`catalog.roots`, `ATLAS_CATALOG_ROOTS`); `roots[].source` says which is which.', schema: str, example: '>specs/' },
|
|
320
320
|
{ name: 'format', in: 'query', description: '`markdown` for a Markdown report instead of JSON.', schema: { type: 'string', enum: ['json', 'markdown'] } }
|
|
321
321
|
],
|
|
322
322
|
responses: { '200': { ...json('The diagnostics.', 'CatalogHealth'), headers: { ETag: { description: 'Changes with the report, its history and the mappings.', schema: str } } }, '304': { description: 'Unchanged since the `If-None-Match` ETag.' }, '401': UNAUTHORIZED, '503': NOT_READY }
|
package/docs/extending.md
CHANGED
|
@@ -30,7 +30,7 @@ carry any of these. Every one is optional.
|
|
|
30
30
|
| `annotations[]` | Notes about the result: a warning, a deprecation, an accessibility finding, a coverage figure. |
|
|
31
31
|
| `attempts[]` | Every try, when the check was retried. |
|
|
32
32
|
| `tags[]` | Free labels to filter by: `smoke`, `payments`. |
|
|
33
|
-
| `labels{}` | Facts about the result that are not its identity: `owner`, `severity`, `browser`, `build`. The viewer reads `build`, `platform`, `applies-to` and `applies-to-by` to tell whether evidence applies to a release (see [the viewer conventions](ui.md#behaviors-read-watch-inspect)). |
|
|
33
|
+
| `labels{}` | Facts about the result that are not its identity: `owner`, `severity`, `browser`, `build`. The viewer reads `build`, `platform`, `applies-to` and `applies-to-by` to tell whether evidence applies to a release, and `published-at` and `enriched-at` (epoch milliseconds or an ISO date) to show when it was published or enriched apart from when the checks ran (see [the viewer conventions](ui.md#behaviors-read-watch-inspect)). `behavior-id` and `example-id` declare a stable id for the behavior and for one example of an outline, and `outline` names the outline; the [catalog health](server.md#reading-the-report-as-an-agent) report counts them and names an id shared by several checks. They never change which check a result belongs to. |
|
|
34
34
|
| `scope{}` | What the evidence does and does not show: surface, device kind, synthetic data, scripted dependencies, audio, pacing, what was asserted and its limits. Unknown when absent. See [Evidence scope](rich-evidence.md#evidence-scope). |
|
|
35
35
|
| `links[]` | Where to read more: the CI run, the issue, the trace client. |
|
|
36
36
|
| `extensions{}` | Structured data of your own or a tool's, under a name you own. |
|
|
@@ -550,9 +550,9 @@ Diagnoses the report's metadata, so a domain filter or a coverage percentage can
|
|
|
550
550
|
|
|
551
551
|
`counts` keeps the denominators apart: spec files, defined specs, specs with an executed test (and those without: the defined coverage gaps), tests, outlines, examples and per-environment observations. `reconciliation` walks from the Specs count to the Tests count. `findings` name ambiguous and unmatched spec paths, identical or same-titled specs, a domain shared by nearly every spec, tests with no spec, checks that fall back to their title as identity and root mappings that matched nothing.
|
|
552
552
|
|
|
553
|
-
Two paths are one logical spec only when a `root` mapping relates them (`from>to`: paths under `from` are the same specs as paths under `to`; an empty `from` matches every path). Without one, similar paths are reported as ambiguous and never merged. A check's label `outline` names the outline its example belongs to. Send `format=markdown` for Markdown.
|
|
553
|
+
Two paths are one logical spec only when a `root` mapping relates them (`from>to`: paths under `from` are the same specs as paths under `to`; an empty `from` matches every path). Without one, similar paths are reported as ambiguous and never merged. A check's label `outline` names the outline its example belongs to; the labels `behavior-id` and `example-id` declare stable ids, which are counted and checked for ids shared by several checks. Send `format=markdown` for Markdown.
|
|
554
554
|
|
|
555
|
-
- `root` (query): A root mapping `from>to`, URL-encoded. Repeatable or comma-separated.
|
|
555
|
+
- `root` (query): A root mapping `from>to`, URL-encoded. Repeatable or comma-separated. It is added to the mappings the server is configured with (`catalog.roots`, `ATLAS_CATALOG_ROOTS`); `roots[].source` says which is which.
|
|
556
556
|
- `format` (query): `markdown` for a Markdown report instead of JSON.
|
|
557
557
|
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
558
558
|
|
|
@@ -566,12 +566,12 @@ Diagnoses the report's metadata, so a domain filter or a coverage percentage can
|
|
|
566
566
|
|
|
567
567
|
`counts` keeps the denominators apart: spec files, defined specs, specs with an executed test (and those without: the defined coverage gaps), tests, outlines, examples and per-environment observations. `reconciliation` walks from the Specs count to the Tests count. `findings` name ambiguous and unmatched spec paths, identical or same-titled specs, a domain shared by nearly every spec, tests with no spec, checks that fall back to their title as identity and root mappings that matched nothing.
|
|
568
568
|
|
|
569
|
-
Two paths are one logical spec only when a `root` mapping relates them (`from>to`: paths under `from` are the same specs as paths under `to`; an empty `from` matches every path). Without one, similar paths are reported as ambiguous and never merged. A check's label `outline` names the outline its example belongs to. Send `format=markdown` for Markdown.
|
|
569
|
+
Two paths are one logical spec only when a `root` mapping relates them (`from>to`: paths under `from` are the same specs as paths under `to`; an empty `from` matches every path). Without one, similar paths are reported as ambiguous and never merged. A check's label `outline` names the outline its example belongs to; the labels `behavior-id` and `example-id` declare stable ids, which are counted and checked for ids shared by several checks. Send `format=markdown` for Markdown.
|
|
570
570
|
|
|
571
571
|
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
572
572
|
|
|
573
573
|
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
574
|
-
- `root` (query): A root mapping `from>to`, URL-encoded. Repeatable or comma-separated.
|
|
574
|
+
- `root` (query): A root mapping `from>to`, URL-encoded. Repeatable or comma-separated. It is added to the mappings the server is configured with (`catalog.roots`, `ATLAS_CATALOG_ROOTS`); `roots[].source` says which is which.
|
|
575
575
|
- `format` (query): `markdown` for a Markdown report instead of JSON.
|
|
576
576
|
|
|
577
577
|
Responses: `200` The diagnostics.; `304` Unchanged since the `If-None-Match` ETag.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
@@ -806,6 +806,10 @@ Explains why the Specs and the Tests of a report may disagree, and what a produc
|
|
|
806
806
|
- **Fallback identity.** A check whose id is its title has no stable id; renaming it starts a new check. Counted, not
|
|
807
807
|
guessed at.
|
|
808
808
|
|
|
809
|
+
- **Declared ids.** A producer may say what a check is with the labels `behavior-id` (the promise) and `example-id`
|
|
810
|
+
(one example of an outline). Atlas counts them and names ids that sit on more than one check; it does not join the
|
|
811
|
+
checks, because a check is still its spec path and id.
|
|
812
|
+
|
|
809
813
|
It only reads: it never changes a status, a slot or a winner, so it cannot disagree with the report it describes.
|
|
810
814
|
Optional label `outline` on a check names the scenario outline its example belongs to.
|
|
811
815
|
|
|
@@ -827,13 +831,30 @@ export const HEALTH_ITEM_LIMIT = …
|
|
|
827
831
|
|
|
828
832
|
The most paths listed in one finding; its `count` is always the whole.
|
|
829
833
|
|
|
834
|
+
#### `BEHAVIOR_ID_LABEL`
|
|
835
|
+
|
|
836
|
+
```ts
|
|
837
|
+
export const BEHAVIOR_ID_LABEL = …
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
The label a producer puts on a check to give its behavior (the promise) an id that outlives renames and moves.
|
|
841
|
+
|
|
842
|
+
#### `EXAMPLE_ID_LABEL`
|
|
843
|
+
|
|
844
|
+
```ts
|
|
845
|
+
export const EXAMPLE_ID_LABEL = …
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
The label a producer puts on a check to give one example of a scenario outline an id of its own.
|
|
849
|
+
|
|
830
850
|
#### `RootMapping`
|
|
831
851
|
|
|
832
852
|
```ts
|
|
833
|
-
export type RootMapping = { readonly from: string; readonly to: string };
|
|
853
|
+
export type RootMapping = { readonly from: string; readonly to: string; readonly source?: 'configuration' | 'request' };
|
|
834
854
|
```
|
|
835
855
|
|
|
836
|
-
Declares that paths under `from` are the same specs as paths under `to`, e.g. `{ from: '', to: 'specs/' }`. Empty `from`
|
|
856
|
+
Declares that paths under `from` are the same specs as paths under `to`, e.g. `{ from: '', to: 'specs/' }`. Empty `from`
|
|
857
|
+
matches every path. `source` says where it was declared (the server's configuration or the request); it changes nothing.
|
|
837
858
|
|
|
838
859
|
#### `HealthItem`
|
|
839
860
|
|
|
@@ -860,6 +881,7 @@ export type HealthCode =
|
|
|
860
881
|
| 'no-spec-definitions'
|
|
861
882
|
| 'fallback-identity'
|
|
862
883
|
| 'unused-root-mapping'
|
|
884
|
+
| 'declared-id-split'
|
|
863
885
|
| 'spec-without-execution'
|
|
864
886
|
| 'historical-only';
|
|
865
887
|
```
|
|
@@ -9556,6 +9578,17 @@ export const HISTORY_PER_TEST = …
|
|
|
9556
9578
|
|
|
9557
9579
|
Recorded results kept per test, newest first. Older ones are counted in no statistic.
|
|
9558
9580
|
|
|
9581
|
+
#### `APPLICABILITY_LABELS`
|
|
9582
|
+
|
|
9583
|
+
```ts
|
|
9584
|
+
export const APPLICABILITY_LABELS = …
|
|
9585
|
+
```
|
|
9586
|
+
|
|
9587
|
+
The open `labels` (see docs/extending.md) that say what a result's evidence ran against and who vouches for it: `build`,
|
|
9588
|
+
`platform`, `applies-to` (builds or commits it is asserted to still hold for), `applies-to-by` (the policy or person that
|
|
9589
|
+
asserts it), `published-at` and `enriched-at` (when the evidence was published or enriched, epoch milliseconds or an ISO
|
|
9590
|
+
date: never when the checks ran). A test's current results carry them as `provenance`.
|
|
9591
|
+
|
|
9559
9592
|
#### `TestResultEntry`
|
|
9560
9593
|
|
|
9561
9594
|
```ts
|
|
@@ -9584,6 +9617,10 @@ export type TestResultEntry = {
|
|
|
9584
9617
|
readonly detail: string;
|
|
9585
9618
|
/** The fingerprint of its spec text the layer recorded, when it recorded one: results with different revisions were recorded against different promises. Absent means not recorded, never "the same". */
|
|
9586
9619
|
readonly specRevision?: string;
|
|
9620
|
+
/** `modified`: recorded from a checkout with uncommitted changes. `unknown`: origin not captured. Absent for a clean checkout, and on results other than a test's current ones. Advisory; never changes a status. */
|
|
9621
|
+
readonly freshness?: 'unknown' | 'modified';
|
|
9622
|
+
/** The labels of its layer and check that say what it ran against and who vouches for it: only those named in {@link APPLICABILITY_LABELS}. Present on a test's current results when it has any. Read by the viewer's evidence applicability; never decides a result. */
|
|
9623
|
+
readonly provenance?: Readonly<Record<string, string>>;
|
|
9587
9624
|
/** What it carried beyond its outcome (attachments, steps, retries, tags, labels), when anything. */
|
|
9588
9625
|
readonly evidence?: EvidenceSummary;
|
|
9589
9626
|
/** Its product-latency measurements (`kind: product-latency`), when it recorded any. Separate from `durationMs`, which is how long the test took (setup, waits and pacing included). */
|
|
@@ -9658,6 +9695,8 @@ export type CatalogSpec = {
|
|
|
9658
9695
|
/** The group it belongs to: the spec's own domain, else the folder under `specs/`. */
|
|
9659
9696
|
readonly domain: string;
|
|
9660
9697
|
readonly kind: string;
|
|
9698
|
+
/** The fingerprint of the spec text now (the promise), when the spec set or a layer recorded one. A result's `specRevision` is compared with it: different means the promise changed after the result was recorded. Absent: unknown, never "the same". */
|
|
9699
|
+
readonly fingerprint?: string;
|
|
9661
9700
|
};
|
|
9662
9701
|
```
|
|
9663
9702
|
|
package/docs/reference/server.md
CHANGED
|
@@ -31,6 +31,8 @@ export type AtlasServerOptions = {
|
|
|
31
31
|
/** The built viewer. Without it, only the API is served. */
|
|
32
32
|
readonly site?: string;
|
|
33
33
|
readonly outlineBranch?: string;
|
|
34
|
+
/** Root mappings `from>to` the catalog health report always applies (the `catalog.roots` setting), beside any `root` a request adds. */
|
|
35
|
+
readonly catalogRoots?: readonly string[];
|
|
34
36
|
readonly include?: (layer: LayerHeader) => boolean;
|
|
35
37
|
readonly refreshMs?: number;
|
|
36
38
|
readonly limits?: { readonly maxObjectMiB?: number; readonly maxJsonMiB?: number; readonly maxStreams?: number };
|