@crvouga/atlas 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -1
- package/dist/reports/src/catalog-health.d.ts +15 -2
- package/dist/reports/src/catalog-health.js +23 -2
- package/dist/schema/src/api.d.ts +6 -0
- package/dist/schema/src/api.js +8 -2
- 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 +24 -2
- package/docs/reference/server.md +2 -0
- package/docs/server.md +11 -2
- package/docs/ui.md +20 -1
- package/llms-full.txt +36 -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/server/openapi.json +24 -4
- package/packages/visualizer/src/explorer/Explorer.tsx +9 -2
- package/packages/visualizer/src/explorer/HealthView.tsx +402 -0
- package/packages/visualizer/src/explorer/catalog.tsx +33 -0
- package/packages/visualizer/src/explorer/search.ts +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.16.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- explain catalog counts and identity in a Catalog health tab with persistent root mappings (#71) ([0974e7a](https://github.com/crvouga/atlas/commit/0974e7a8d16b126c47e79a004974a629275122ad))
|
|
8
|
+
|
|
9
|
+
## 1.15.0 (2026-10-28)
|
|
4
10
|
|
|
5
11
|
### Features
|
|
6
12
|
|
|
@@ -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
|
};
|
package/dist/schema/src/api.d.ts
CHANGED
|
@@ -2808,6 +2808,8 @@ export declare const CatalogHealthSchema: z.ZodObject<{
|
|
|
2808
2808
|
identity: z.ZodObject<{
|
|
2809
2809
|
checks: z.ZodNumber;
|
|
2810
2810
|
fallbackIdentity: z.ZodNumber;
|
|
2811
|
+
declaredBehaviorIds: z.ZodOptional<z.ZodNumber>;
|
|
2812
|
+
declaredExampleIds: z.ZodOptional<z.ZodNumber>;
|
|
2811
2813
|
basis: z.ZodString;
|
|
2812
2814
|
}, z.core.$loose>;
|
|
2813
2815
|
domains: z.ZodArray<z.ZodObject<{
|
|
@@ -2818,6 +2820,10 @@ export declare const CatalogHealthSchema: z.ZodObject<{
|
|
|
2818
2820
|
from: z.ZodString;
|
|
2819
2821
|
to: z.ZodString;
|
|
2820
2822
|
matched: z.ZodNumber;
|
|
2823
|
+
source: z.ZodOptional<z.ZodEnum<{
|
|
2824
|
+
request: "request";
|
|
2825
|
+
configuration: "configuration";
|
|
2826
|
+
}>>;
|
|
2821
2827
|
}, z.core.$loose>>;
|
|
2822
2828
|
aliases: z.ZodArray<z.ZodObject<{
|
|
2823
2829
|
path: z.ZodString;
|
package/dist/schema/src/api.js
CHANGED
|
@@ -533,9 +533,15 @@ export const CatalogHealthSchema = z.looseObject({
|
|
|
533
533
|
testsWithoutSpec: nonneg,
|
|
534
534
|
explanation: z.array(z.string())
|
|
535
535
|
}),
|
|
536
|
-
identity: z.looseObject({
|
|
536
|
+
identity: z.looseObject({
|
|
537
|
+
checks: nonneg,
|
|
538
|
+
fallbackIdentity: nonneg.describe('Checks whose id equals their title'),
|
|
539
|
+
declaredBehaviorIds: nonneg.optional().describe('Checks that declare a `behavior-id` label'),
|
|
540
|
+
declaredExampleIds: nonneg.optional().describe('Checks that declare an `example-id` label'),
|
|
541
|
+
basis: z.string()
|
|
542
|
+
}),
|
|
537
543
|
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') })),
|
|
544
|
+
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
545
|
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
546
|
findings: z.array(z.looseObject({
|
|
541
547
|
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 (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
|
```
|
package/docs/reference/server.md
CHANGED
|
@@ -31,6 +31,8 @@ export type AtlasServerOptions = {
|
|
|
31
31
|
/** The built viewer. Without it, only the API is served. */
|
|
32
32
|
readonly site?: string;
|
|
33
33
|
readonly outlineBranch?: string;
|
|
34
|
+
/** Root mappings `from>to` the catalog health report always applies (the `catalog.roots` setting), beside any `root` a request adds. */
|
|
35
|
+
readonly catalogRoots?: readonly string[];
|
|
34
36
|
readonly include?: (layer: LayerHeader) => boolean;
|
|
35
37
|
readonly refreshMs?: number;
|
|
36
38
|
readonly limits?: { readonly maxObjectMiB?: number; readonly maxJsonMiB?: number; readonly maxStreams?: number };
|
package/docs/server.md
CHANGED
|
@@ -202,6 +202,7 @@ is said in words and the server does not start. The file has a published
|
|
|
202
202
|
| Whose specs the report is laid over | `outlineBranch` | `--outline-branch <branch>` | `ATLAS_OUTLINE_BRANCH` | `main`, then `master`, then the newest |
|
|
203
203
|
| Only layers recorded on these branches take part | `include.branches` | `--branch <name>` (repeatable) | `ATLAS_ONLY_BRANCH` | all |
|
|
204
204
|
| Leave out layers from modified checkouts and unknown origins | `include.ignoreModified` | `--ignore-modified` | `ATLAS_IGNORE_MODIFIED` | no |
|
|
205
|
+
| Root mappings `from>to` that join spec paths a producer publishes to the paths the specs are recorded under, for the catalog health report (comma-separated; an empty `from` matches every path) | `catalog.roots` | | `ATLAS_CATALOG_ROOTS` | none |
|
|
205
206
|
| How often (seconds) the backends are looked at for layers written by something else | `refreshSeconds` | `--refresh <seconds>` | `ATLAS_REFRESH_SECONDS` | `10` |
|
|
206
207
|
| Let anyone look, not upload | `auth.anonymousRead` | `--anonymous-read` | `ATLAS_ANONYMOUS_READ` | no |
|
|
207
208
|
| Listen publicly with no tokens (something in front signs people in) | `auth.insecure` | `--insecure` | `ATLAS_INSECURE` | no |
|
|
@@ -442,8 +443,16 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
|
|
|
442
443
|
the Tests total. Two spec paths are one logical spec only when a mapping says so
|
|
443
444
|
(`?root=<from>><to>`, for example `?root=%3Especs/` to put `checkout/cart.feature` under `specs/`); similar paths
|
|
444
445
|
are reported as ambiguous and never merged, and a similar title is never an identity. Give a check the label
|
|
445
|
-
`outline` to say which scenario outline an example belongs to
|
|
446
|
-
|
|
446
|
+
`outline` to say which scenario outline an example belongs to, and `behavior-id` / `example-id` to give a behavior
|
|
447
|
+
or one example a stable id of your own: Atlas counts them and names an id that sits on more than one check (a
|
|
448
|
+
rename or a move), but a check is still its spec path and id, so nothing is joined by them. A domain shared by nearly
|
|
449
|
+
every spec is flagged, but explicit domain metadata always stays authoritative. The document only reads: it changes
|
|
450
|
+
no status.
|
|
451
|
+
- Mappings that should always apply are a setting, not a query parameter: `catalog.roots` in `atlas.server.json` (a list
|
|
452
|
+
of `from>to`) or `ATLAS_CATALOG_ROOTS` (comma-separated; it replaces the file's list). The health document applies
|
|
453
|
+
them, then adds the request's `?root=`, and says in `roots[].source` which is which. A mapping with no `>` stops the
|
|
454
|
+
server starting. The viewer's **Catalog health** tab shows the same document, and its mapping form (the `root`
|
|
455
|
+
address parameter) tries further mappings without changing the server.
|
|
447
456
|
- The server keeps the newest 100 results of every test (progress reported as `running` is not history) and every run's last result per test, in memory, as layers
|
|
448
457
|
arrive; it is rebuilt from the layers on restart. History never changes a result.
|
|
449
458
|
- Without a published spec set, tests are grouped under the specs their layers recorded (`specs[]`: path, domain,
|
package/docs/ui.md
CHANGED
|
@@ -96,6 +96,25 @@ when Watch is on screen.
|
|
|
96
96
|
- The spec index searches titles, paths and declared domains, and can group by the domain from spec metadata (specs with
|
|
97
97
|
none are listed under "No domain declared"; the Tests view reports domain problems).
|
|
98
98
|
|
|
99
|
+
## Catalog health
|
|
100
|
+
|
|
101
|
+
The **Catalog health** tab (`explorer/HealthView.tsx`, `?tab=health`; `@crvouga/atlas/reports/catalog-health`) explains why the Specs
|
|
102
|
+
and the Tests disagree. The Specs list links to it ("Why do these counts differ from the Tests?"). On a server it reads
|
|
103
|
+
`GET /api/reports/tests/health`; elsewhere the same document is worked out in the page from the specs and results it holds, and an
|
|
104
|
+
older server without the route is told so in words.
|
|
105
|
+
|
|
106
|
+
- Denominators are never one number: spec files, defined specs, specs with tests, specs executed and defined but not executed
|
|
107
|
+
(the coverage gaps, which add to no passed count), tests, outlines, examples and observations (an outline, its examples and
|
|
108
|
+
their per-environment results are told apart). Each figure has its meaning in its `title`.
|
|
109
|
+
- A table walks from the Specs total to the Tests total, step by step, with the sentences below it. Findings are an `Accordion`
|
|
110
|
+
(warnings open), each with its severity as text, its code, how many it affects and the paths. Paths that might be one promise
|
|
111
|
+
are shown as "may be the same promise as ...; not merged": a similar title never joins two specs.
|
|
112
|
+
- Root mappings (`from>to`) join spec paths. Those the server is configured with (`catalog.roots`, `ATLAS_CATALOG_ROOTS`) are
|
|
113
|
+
labelled "Configured on the server"; others are tried in the form and kept in the address (`root`), labelled "Tried here", and
|
|
114
|
+
change nothing on the server. A mapping that matched no path is a warning. Joined specs list every original path.
|
|
115
|
+
- Domains are listed as declared; a domain shared by nearly every spec is a finding, never rewritten. Identity states how many
|
|
116
|
+
checks are known only by title, how many declare `behavior-id` / `example-id`, and the limits of both.
|
|
117
|
+
|
|
99
118
|
## UI catalog
|
|
100
119
|
|
|
101
120
|
`/catalog` (`catalog/`) is the viewer's inventory of captured UI states ([the guide](ui-catalog.md)). It is computed in the page from
|
|
@@ -320,7 +339,7 @@ The narrowly scoped exceptions are:
|
|
|
320
339
|
| --- | --- | --- |
|
|
321
340
|
| Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
|
|
322
341
|
| UI catalog | Captured UI states per profile as cards with coverage badges, filters in the index column, surface coverage, an open state with its profiles side by side and where each picture came from, manifest download | AppShell navbar, SimpleGrid, Card, Image, MultiSelect, TextInput, Drawer, Accordion, Table, Badge, Alert |
|
|
323
|
-
| Project home | Tests/runs/artifacts/compare tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
|
|
342
|
+
| Project home | Tests/runs/artifacts/compare/catalog health tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
|
|
324
343
|
| Test detail | Current outcomes, statistics, duration graph, product latency with budgets and baselines, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
|
|
325
344
|
| Specs | All six existing kinds, overview/index with title search and folder or declared-domain grouping, Gherkin rules/scenarios/examples with a coverage summary and Read / Watch / Inspect per behavior, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Tabs, Table, CodeHighlightTabs, ResultRow |
|
|
326
345
|
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|
package/llms-full.txt
CHANGED
|
@@ -1850,6 +1850,7 @@ is said in words and the server does not start. The file has a published
|
|
|
1850
1850
|
| Whose specs the report is laid over | `outlineBranch` | `--outline-branch <branch>` | `ATLAS_OUTLINE_BRANCH` | `main`, then `master`, then the newest |
|
|
1851
1851
|
| Only layers recorded on these branches take part | `include.branches` | `--branch <name>` (repeatable) | `ATLAS_ONLY_BRANCH` | all |
|
|
1852
1852
|
| Leave out layers from modified checkouts and unknown origins | `include.ignoreModified` | `--ignore-modified` | `ATLAS_IGNORE_MODIFIED` | no |
|
|
1853
|
+
| Root mappings `from>to` that join spec paths a producer publishes to the paths the specs are recorded under, for the catalog health report (comma-separated; an empty `from` matches every path) | `catalog.roots` | | `ATLAS_CATALOG_ROOTS` | none |
|
|
1853
1854
|
| How often (seconds) the backends are looked at for layers written by something else | `refreshSeconds` | `--refresh <seconds>` | `ATLAS_REFRESH_SECONDS` | `10` |
|
|
1854
1855
|
| Let anyone look, not upload | `auth.anonymousRead` | `--anonymous-read` | `ATLAS_ANONYMOUS_READ` | no |
|
|
1855
1856
|
| Listen publicly with no tokens (something in front signs people in) | `auth.insecure` | `--insecure` | `ATLAS_INSECURE` | no |
|
|
@@ -2090,8 +2091,16 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
|
|
|
2090
2091
|
the Tests total. Two spec paths are one logical spec only when a mapping says so
|
|
2091
2092
|
(`?root=<from>><to>`, for example `?root=%3Especs/` to put `checkout/cart.feature` under `specs/`); similar paths
|
|
2092
2093
|
are reported as ambiguous and never merged, and a similar title is never an identity. Give a check the label
|
|
2093
|
-
`outline` to say which scenario outline an example belongs to
|
|
2094
|
-
|
|
2094
|
+
`outline` to say which scenario outline an example belongs to, and `behavior-id` / `example-id` to give a behavior
|
|
2095
|
+
or one example a stable id of your own: Atlas counts them and names an id that sits on more than one check (a
|
|
2096
|
+
rename or a move), but a check is still its spec path and id, so nothing is joined by them. A domain shared by nearly
|
|
2097
|
+
every spec is flagged, but explicit domain metadata always stays authoritative. The document only reads: it changes
|
|
2098
|
+
no status.
|
|
2099
|
+
- Mappings that should always apply are a setting, not a query parameter: `catalog.roots` in `atlas.server.json` (a list
|
|
2100
|
+
of `from>to`) or `ATLAS_CATALOG_ROOTS` (comma-separated; it replaces the file's list). The health document applies
|
|
2101
|
+
them, then adds the request's `?root=`, and says in `roots[].source` which is which. A mapping with no `>` stops the
|
|
2102
|
+
server starting. The viewer's **Catalog health** tab shows the same document, and its mapping form (the `root`
|
|
2103
|
+
address parameter) tries further mappings without changing the server.
|
|
2095
2104
|
- The server keeps the newest 100 results of every test (progress reported as `running` is not history) and every run's last result per test, in memory, as layers
|
|
2096
2105
|
arrive; it is rebuilt from the layers on restart. History never changes a result.
|
|
2097
2106
|
- Without a published spec set, tests are grouped under the specs their layers recorded (`specs[]`: path, domain,
|
|
@@ -3195,7 +3204,7 @@ carry any of these. Every one is optional.
|
|
|
3195
3204
|
| `annotations[]` | Notes about the result: a warning, a deprecation, an accessibility finding, a coverage figure. |
|
|
3196
3205
|
| `attempts[]` | Every try, when the check was retried. |
|
|
3197
3206
|
| `tags[]` | Free labels to filter by: `smoke`, `payments`. |
|
|
3198
|
-
| `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)). |
|
|
3207
|
+
| `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)). `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. |
|
|
3199
3208
|
| `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). |
|
|
3200
3209
|
| `links[]` | Where to read more: the CI run, the issue, the trace client. |
|
|
3201
3210
|
| `extensions{}` | Structured data of your own or a tool's, under a name you own. |
|
|
@@ -4515,6 +4524,25 @@ when Watch is on screen.
|
|
|
4515
4524
|
- The spec index searches titles, paths and declared domains, and can group by the domain from spec metadata (specs with
|
|
4516
4525
|
none are listed under "No domain declared"; the Tests view reports domain problems).
|
|
4517
4526
|
|
|
4527
|
+
## Catalog health
|
|
4528
|
+
|
|
4529
|
+
The **Catalog health** tab (`explorer/HealthView.tsx`, `?tab=health`; `@crvouga/atlas/reports/catalog-health`) explains why the Specs
|
|
4530
|
+
and the Tests disagree. The Specs list links to it ("Why do these counts differ from the Tests?"). On a server it reads
|
|
4531
|
+
`GET /api/reports/tests/health`; elsewhere the same document is worked out in the page from the specs and results it holds, and an
|
|
4532
|
+
older server without the route is told so in words.
|
|
4533
|
+
|
|
4534
|
+
- Denominators are never one number: spec files, defined specs, specs with tests, specs executed and defined but not executed
|
|
4535
|
+
(the coverage gaps, which add to no passed count), tests, outlines, examples and observations (an outline, its examples and
|
|
4536
|
+
their per-environment results are told apart). Each figure has its meaning in its `title`.
|
|
4537
|
+
- A table walks from the Specs total to the Tests total, step by step, with the sentences below it. Findings are an `Accordion`
|
|
4538
|
+
(warnings open), each with its severity as text, its code, how many it affects and the paths. Paths that might be one promise
|
|
4539
|
+
are shown as "may be the same promise as ...; not merged": a similar title never joins two specs.
|
|
4540
|
+
- Root mappings (`from>to`) join spec paths. Those the server is configured with (`catalog.roots`, `ATLAS_CATALOG_ROOTS`) are
|
|
4541
|
+
labelled "Configured on the server"; others are tried in the form and kept in the address (`root`), labelled "Tried here", and
|
|
4542
|
+
change nothing on the server. A mapping that matched no path is a warning. Joined specs list every original path.
|
|
4543
|
+
- Domains are listed as declared; a domain shared by nearly every spec is a finding, never rewritten. Identity states how many
|
|
4544
|
+
checks are known only by title, how many declare `behavior-id` / `example-id`, and the limits of both.
|
|
4545
|
+
|
|
4518
4546
|
## UI catalog
|
|
4519
4547
|
|
|
4520
4548
|
`/catalog` (`catalog/`) is the viewer's inventory of captured UI states ([the guide](ui-catalog.md)). It is computed in the page from
|
|
@@ -4739,7 +4767,7 @@ The narrowly scoped exceptions are:
|
|
|
4739
4767
|
| --- | --- | --- |
|
|
4740
4768
|
| Shared shell | Project switching, spec navigation, run selection, connection status, diagnostics, mobile index | AppShell, Burger, NavLink, Menu, Popover, NativeSelect, Drawer |
|
|
4741
4769
|
| UI catalog | Captured UI states per profile as cards with coverage badges, filters in the index column, surface coverage, an open state with its profiles side by side and where each picture came from, manifest download | AppShell navbar, SimpleGrid, Card, Image, MultiSelect, TextInput, Drawer, Accordion, Table, Badge, Alert |
|
|
4742
|
-
| Project home | Tests/runs/artifacts/compare tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
|
|
4770
|
+
| Project home | Tests/runs/artifacts/compare/catalog health tabs, a two-run comparison with promise, outcome, recording and figure tabs per behavior, a run's Artifacts (files of its layers, shared recordings, direct links), Watch playlist of a filtered set, metrics, search, status/environment/domain/artifact-tag filters, run and artifact facets with saved views, grouping, sorting, show more | Container, Tabs, SimpleGrid, Paper, TextInput, MultiSelect, Popover, Button, Accordion, Modal, Grid, Switch |
|
|
4743
4771
|
| Test detail | Current outcomes, statistics, duration graph, product latency with budgets and baselines, history, artifacts, recording; the `layer` address parameter pins it to one run's result, labelled "This run" against "Latest combined evidence" | Drawer, Table, Badge, Alert, Stage |
|
|
4744
4772
|
| Specs | All six existing kinds, overview/index with title search and folder or declared-domain grouping, Gherkin rules/scenarios/examples with a coverage summary and Read / Watch / Inspect per behavior, Quint declarations/traces, DMN tables, source text | SpecHeader, Accordion, Tabs, Table, CodeHighlightTabs, ResultRow |
|
|
4745
4773
|
| Statecharts | Graph, screen/event/journey details, developer metadata, chart test results | Mantine cards/navigation and product compositions; graph exception above |
|
|
@@ -5829,9 +5857,9 @@ Diagnoses the report's metadata, so a domain filter or a coverage percentage can
|
|
|
5829
5857
|
|
|
5830
5858
|
`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.
|
|
5831
5859
|
|
|
5832
|
-
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.
|
|
5860
|
+
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.
|
|
5833
5861
|
|
|
5834
|
-
- `root` (query): A root mapping `from>to`, URL-encoded. Repeatable or comma-separated.
|
|
5862
|
+
- `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.
|
|
5835
5863
|
- `format` (query): `markdown` for a Markdown report instead of JSON.
|
|
5836
5864
|
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
5837
5865
|
|
|
@@ -5845,12 +5873,12 @@ Diagnoses the report's metadata, so a domain filter or a coverage percentage can
|
|
|
5845
5873
|
|
|
5846
5874
|
`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.
|
|
5847
5875
|
|
|
5848
|
-
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.
|
|
5876
|
+
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.
|
|
5849
5877
|
|
|
5850
5878
|
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.
|
|
5851
5879
|
|
|
5852
5880
|
- `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.
|
|
5853
|
-
- `root` (query): A root mapping `from>to`, URL-encoded. Repeatable or comma-separated.
|
|
5881
|
+
- `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.
|
|
5854
5882
|
- `format` (query): `markdown` for a Markdown report instead of JSON.
|
|
5855
5883
|
|
|
5856
5884
|
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`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crvouga/atlas",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Atlas: specs of any kind, reported as layered evidence. Publish checks from any runner (JUnit, CTRF, a script), combine them across nodes, branches, environments and backends, gate CI on the result, and run statecharts against the real app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -229,6 +229,18 @@
|
|
|
229
229
|
"maximum": 9007199254740991,
|
|
230
230
|
"description": "Checks whose id equals their title"
|
|
231
231
|
},
|
|
232
|
+
"declaredBehaviorIds": {
|
|
233
|
+
"description": "Checks that declare a `behavior-id` label",
|
|
234
|
+
"type": "integer",
|
|
235
|
+
"minimum": 0,
|
|
236
|
+
"maximum": 9007199254740991
|
|
237
|
+
},
|
|
238
|
+
"declaredExampleIds": {
|
|
239
|
+
"description": "Checks that declare an `example-id` label",
|
|
240
|
+
"type": "integer",
|
|
241
|
+
"minimum": 0,
|
|
242
|
+
"maximum": 9007199254740991
|
|
243
|
+
},
|
|
232
244
|
"basis": {
|
|
233
245
|
"type": "string"
|
|
234
246
|
}
|
|
@@ -278,6 +290,14 @@
|
|
|
278
290
|
"minimum": 0,
|
|
279
291
|
"maximum": 9007199254740991,
|
|
280
292
|
"description": "Distinct paths the mapping changed"
|
|
293
|
+
},
|
|
294
|
+
"source": {
|
|
295
|
+
"description": "Where it was declared: the server's `catalog.roots` setting or this request's `root` parameter",
|
|
296
|
+
"type": "string",
|
|
297
|
+
"enum": [
|
|
298
|
+
"configuration",
|
|
299
|
+
"request"
|
|
300
|
+
]
|
|
281
301
|
}
|
|
282
302
|
},
|
|
283
303
|
"required": [
|
|
@@ -102,6 +102,23 @@
|
|
|
102
102
|
},
|
|
103
103
|
"additionalProperties": false
|
|
104
104
|
},
|
|
105
|
+
"catalog": {
|
|
106
|
+
"default": {
|
|
107
|
+
"roots": []
|
|
108
|
+
},
|
|
109
|
+
"type": "object",
|
|
110
|
+
"properties": {
|
|
111
|
+
"roots": {
|
|
112
|
+
"default": [],
|
|
113
|
+
"description": "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",
|
|
114
|
+
"type": "array",
|
|
115
|
+
"items": {
|
|
116
|
+
"type": "string"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"additionalProperties": false
|
|
121
|
+
},
|
|
105
122
|
"refreshSeconds": {
|
|
106
123
|
"default": 10,
|
|
107
124
|
"description": "How often the backends are looked at for layers written by something other than this server",
|
|
@@ -3699,7 +3699,7 @@
|
|
|
3699
3699
|
"Report"
|
|
3700
3700
|
],
|
|
3701
3701
|
"summary": "Why specs, domains and counts disagree",
|
|
3702
|
-
"description": "Diagnoses the report's metadata, so a domain filter or a coverage percentage can be trusted or fixed at its source. It reads; it never changes a status, a slot or a winner.\n\n`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.\n\nTwo 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.\n\nRole needed: `read`.",
|
|
3702
|
+
"description": "Diagnoses the report's metadata, so a domain filter or a coverage percentage can be trusted or fixed at its source. It reads; it never changes a status, a slot or a winner.\n\n`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.\n\nTwo 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.\n\nRole needed: `read`.",
|
|
3703
3703
|
"x-atlas-role": "read",
|
|
3704
3704
|
"security": [
|
|
3705
3705
|
{
|
|
@@ -3714,7 +3714,7 @@
|
|
|
3714
3714
|
"required": false,
|
|
3715
3715
|
"name": "root",
|
|
3716
3716
|
"in": "query",
|
|
3717
|
-
"description": "A root mapping `from>to`, URL-encoded. Repeatable or comma-separated.",
|
|
3717
|
+
"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.",
|
|
3718
3718
|
"example": ">specs/",
|
|
3719
3719
|
"schema": {
|
|
3720
3720
|
"type": "string"
|
|
@@ -3798,7 +3798,7 @@
|
|
|
3798
3798
|
"Report"
|
|
3799
3799
|
],
|
|
3800
3800
|
"summary": "Why specs, domains and counts disagree",
|
|
3801
|
-
"description": "Diagnoses the report's metadata, so a domain filter or a coverage percentage can be trusted or fixed at its source. It reads; it never changes a status, a slot or a winner.\n\n`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.\n\nTwo 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.\n\nScoped 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.\n\nRole needed: `read`.",
|
|
3801
|
+
"description": "Diagnoses the report's metadata, so a domain filter or a coverage percentage can be trusted or fixed at its source. It reads; it never changes a status, a slot or a winner.\n\n`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.\n\nTwo 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.\n\nScoped 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.\n\nRole needed: `read`.",
|
|
3802
3802
|
"x-atlas-role": "read",
|
|
3803
3803
|
"security": [
|
|
3804
3804
|
{
|
|
@@ -3825,7 +3825,7 @@
|
|
|
3825
3825
|
"required": false,
|
|
3826
3826
|
"name": "root",
|
|
3827
3827
|
"in": "query",
|
|
3828
|
-
"description": "A root mapping `from>to`, URL-encoded. Repeatable or comma-separated.",
|
|
3828
|
+
"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.",
|
|
3829
3829
|
"example": ">specs/",
|
|
3830
3830
|
"schema": {
|
|
3831
3831
|
"type": "string"
|
|
@@ -21772,6 +21772,18 @@
|
|
|
21772
21772
|
"maximum": 9007199254740991,
|
|
21773
21773
|
"description": "Checks whose id equals their title"
|
|
21774
21774
|
},
|
|
21775
|
+
"declaredBehaviorIds": {
|
|
21776
|
+
"description": "Checks that declare a `behavior-id` label",
|
|
21777
|
+
"type": "integer",
|
|
21778
|
+
"minimum": 0,
|
|
21779
|
+
"maximum": 9007199254740991
|
|
21780
|
+
},
|
|
21781
|
+
"declaredExampleIds": {
|
|
21782
|
+
"description": "Checks that declare an `example-id` label",
|
|
21783
|
+
"type": "integer",
|
|
21784
|
+
"minimum": 0,
|
|
21785
|
+
"maximum": 9007199254740991
|
|
21786
|
+
},
|
|
21775
21787
|
"basis": {
|
|
21776
21788
|
"type": "string"
|
|
21777
21789
|
}
|
|
@@ -21821,6 +21833,14 @@
|
|
|
21821
21833
|
"minimum": 0,
|
|
21822
21834
|
"maximum": 9007199254740991,
|
|
21823
21835
|
"description": "Distinct paths the mapping changed"
|
|
21836
|
+
},
|
|
21837
|
+
"source": {
|
|
21838
|
+
"description": "Where it was declared: the server's `catalog.roots` setting or this request's `root` parameter",
|
|
21839
|
+
"type": "string",
|
|
21840
|
+
"enum": [
|
|
21841
|
+
"configuration",
|
|
21842
|
+
"request"
|
|
21843
|
+
]
|
|
21824
21844
|
}
|
|
21825
21845
|
},
|
|
21826
21846
|
"required": [
|
|
@@ -59,6 +59,7 @@ import { patched, useExplorerNavigate, useExplorerSearch } from './navigate';
|
|
|
59
59
|
import { Badges, EnvironmentChips, projectTitle, runName } from './parts';
|
|
60
60
|
import { ArtifactsView } from './ArtifactsView';
|
|
61
61
|
import { CompareView } from './CompareView';
|
|
62
|
+
import { HealthView } from './HealthView';
|
|
62
63
|
import { CollectionsView } from './collections';
|
|
63
64
|
import { RunsView } from './RunsView';
|
|
64
65
|
import { SearchNotes, TypedFilters } from './search-notes';
|
|
@@ -170,7 +171,7 @@ export function Explorer() {
|
|
|
170
171
|
const model = catalog.model;
|
|
171
172
|
const selected = search.test ? (model?.byKey.get(search.test) ?? null) : null;
|
|
172
173
|
const empty = model !== null && model.tests.length === 0;
|
|
173
|
-
const split = !!search.test && search.tab !== 'runs' && search.tab !== 'artifacts' && search.tab !== 'collections' && search.tab !== 'compare';
|
|
174
|
+
const split = !!search.test && search.tab !== 'runs' && search.tab !== 'artifacts' && search.tab !== 'collections' && search.tab !== 'compare' && search.tab !== 'health';
|
|
174
175
|
return (
|
|
175
176
|
<Container size="xl" py="xl">
|
|
176
177
|
<Box aria-label="Test explorer" component="article" miw={0}>
|
|
@@ -181,7 +182,7 @@ export function Explorer() {
|
|
|
181
182
|
{model && (model.tests.length > 0 || model.summary.runs > 0) && (
|
|
182
183
|
<Tabs
|
|
183
184
|
value={search.tab ?? 'tests'}
|
|
184
|
-
onChange={(value) => go({ tab: value === 'runs' || value === 'artifacts' || value === 'collections' || value === 'compare' ? value : undefined, test: undefined, openRun: undefined, layer: undefined, artifact: undefined, collection: undefined, rev: undefined, pair: undefined })}
|
|
185
|
+
onChange={(value) => go({ tab: value === 'runs' || value === 'artifacts' || value === 'collections' || value === 'compare' || value === 'health' ? value : undefined, test: undefined, openRun: undefined, layer: undefined, artifact: undefined, collection: undefined, rev: undefined, pair: undefined })}
|
|
185
186
|
>
|
|
186
187
|
<Tabs.List aria-label="Explorer view">
|
|
187
188
|
<Tabs.Tab
|
|
@@ -206,6 +207,7 @@ export function Explorer() {
|
|
|
206
207
|
</Tabs.Tab>
|
|
207
208
|
<Tabs.Tab value="artifacts">Artifacts</Tabs.Tab>
|
|
208
209
|
<Tabs.Tab value="compare">Compare</Tabs.Tab>
|
|
210
|
+
<Tabs.Tab value="health">Catalog health</Tabs.Tab>
|
|
209
211
|
{!staticEvidence && <Tabs.Tab value="collections">Collections</Tabs.Tab>}
|
|
210
212
|
</Tabs.List>
|
|
211
213
|
</Tabs>
|
|
@@ -243,6 +245,9 @@ export function Explorer() {
|
|
|
243
245
|
</List.Item>
|
|
244
246
|
))}
|
|
245
247
|
</List>
|
|
248
|
+
<Anchor mt="xs" display="inline-block" size="sm" renderRoot={(props) => <Link {...props} to="/" search={patched({ tab: 'health', test: undefined })} resetScroll={false} activeOptions={{ exact: true }} />}>
|
|
249
|
+
Why do these counts differ from the Tests?
|
|
250
|
+
</Anchor>
|
|
246
251
|
</Box>
|
|
247
252
|
)}
|
|
248
253
|
{!model && !evidence.unavailable && (
|
|
@@ -273,6 +278,8 @@ export function Explorer() {
|
|
|
273
278
|
<ArtifactsView />
|
|
274
279
|
) : search.tab === 'compare' ? (
|
|
275
280
|
<CompareView />
|
|
281
|
+
) : search.tab === 'health' ? (
|
|
282
|
+
<HealthView />
|
|
276
283
|
) : search.tab === 'collections' ? (
|
|
277
284
|
<CollectionsView />
|
|
278
285
|
) : empty ? null : (
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Catalog health view
|
|
3
|
+
*
|
|
4
|
+
* The Catalog health tab: why the Specs and the Tests of a report disagree, and what a producer or an operator can change.
|
|
5
|
+
* It shows the denominators apart (spec files, defined specs, specs with an executed test, tests, outlines, examples and
|
|
6
|
+
* per-environment observations), the steps from the Specs count to the Tests count, the domains in use, specs seen under
|
|
7
|
+
* more than one path, and every finding with the paths it affects. Two paths are one logical spec only when a root mapping
|
|
8
|
+
* relates them: the server's configured mappings are shown as such, and further `from>to` mappings can be tried here
|
|
9
|
+
* (they are the address's `root`, so a link reproduces the figures). Ambiguous paths are never merged by similar title.
|
|
10
|
+
* The tab only reads: it never changes a status, a count of passed checks or a domain.
|
|
11
|
+
*/
|
|
12
|
+
import { parseRootMappings, type CatalogHealth, type HealthFinding, type RootMapping } from '@crvouga/atlas/reports/catalog-health';
|
|
13
|
+
import { Accordion, ActionIcon, Alert, Badge, Box, Button, Code, Group, List, Paper, SimpleGrid, Stack, Table, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
|
14
|
+
import { IconTrash } from '@tabler/icons-react';
|
|
15
|
+
import { useMemo, useState } from 'react';
|
|
16
|
+
|
|
17
|
+
import { plural } from '../evidence/format';
|
|
18
|
+
import { useCatalogHealth } from './catalog';
|
|
19
|
+
import { useExplorerNavigate, useExplorerSearch } from './navigate';
|
|
20
|
+
|
|
21
|
+
const label = (root: { readonly from: string; readonly to: string }) => `${root.from}>${root.to}`;
|
|
22
|
+
|
|
23
|
+
function Figure({ name, value, hint }: { name: string; value: number; hint: string }) {
|
|
24
|
+
return (
|
|
25
|
+
<Paper withBorder p="sm" title={hint}>
|
|
26
|
+
<Text size="xs" c="dimmed" component="div">
|
|
27
|
+
{name}
|
|
28
|
+
</Text>
|
|
29
|
+
<Text fw={700} size="xl" component="div" data-health-figure={name}>
|
|
30
|
+
{value}
|
|
31
|
+
</Text>
|
|
32
|
+
</Paper>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Counts({ health }: { health: CatalogHealth }) {
|
|
37
|
+
const { specs, tests, scenarios } = health.counts;
|
|
38
|
+
return (
|
|
39
|
+
<Stack gap="md">
|
|
40
|
+
<Box component="section" aria-label="Spec counts">
|
|
41
|
+
<Title order={4} mb="xs">
|
|
42
|
+
Specs
|
|
43
|
+
</Title>
|
|
44
|
+
<SimpleGrid cols={{ base: 2, sm: 3, md: 5 }}>
|
|
45
|
+
<Figure name="Spec files" value={specs.files} hint="Files the report is laid over, before any root mapping. This is the number the Specs view shows." />
|
|
46
|
+
<Figure name="Defined specs" value={specs.defined} hint="Spec files, with those joined by a declared root mapping counted once." />
|
|
47
|
+
<Figure name="Specs with tests" value={specs.withTests} hint="Specs, defined or not, with at least one test. This is what the Tests view groups by." />
|
|
48
|
+
<Figure name="Specs executed" value={specs.executed} hint="Defined specs with a test that passed or failed: the denominator of execution coverage." />
|
|
49
|
+
<Figure name="Defined, not executed" value={specs.withoutExecution} hint="Defined specs with no passed or failed test: the coverage gaps. They add to no passed count." />
|
|
50
|
+
</SimpleGrid>
|
|
51
|
+
</Box>
|
|
52
|
+
<Box component="section" aria-label="Test counts">
|
|
53
|
+
<Title order={4} mb="xs">
|
|
54
|
+
Tests
|
|
55
|
+
</Title>
|
|
56
|
+
<SimpleGrid cols={{ base: 2, sm: 3, md: 5 }}>
|
|
57
|
+
<Figure name="Tests" value={tests.total} hint="Check identities: a spec path and an id, however many environments ran them." />
|
|
58
|
+
<Figure name="Tests passed" value={tests.passed} hint="Tests whose current status is passed." />
|
|
59
|
+
<Figure name="Tests with a spec" value={tests.withSpec} hint="Tests that name the spec they belong to." />
|
|
60
|
+
<Figure name="Tests without a spec" value={tests.withoutSpec} hint="Tests that name no spec; they are in no spec or domain group." />
|
|
61
|
+
<Figure name="Only in history" value={tests.historicalOnly} hint="Tests the history holds but the report has no current result for (removed, renamed or not re-run)." />
|
|
62
|
+
</SimpleGrid>
|
|
63
|
+
</Box>
|
|
64
|
+
<Box component="section" aria-label="Scenario counts">
|
|
65
|
+
<Title order={4} mb="xs">
|
|
66
|
+
Outlines, examples and observations
|
|
67
|
+
</Title>
|
|
68
|
+
<SimpleGrid cols={{ base: 2, sm: 3, md: 5 }}>
|
|
69
|
+
<Figure name="Outlines" value={scenarios.outlines} hint="Tests sharing the label outline count once; a test without it is its own outline." />
|
|
70
|
+
<Figure name="Examples" value={scenarios.examples} hint="Concrete examples: tests, however many environments ran them." />
|
|
71
|
+
<Figure name="Observations" value={scenarios.observations} hint="Current results per producer, environment and parameter set." />
|
|
72
|
+
<Figure name="Observations executed" value={scenarios.executedObservations} hint="Observations that passed or failed." />
|
|
73
|
+
<Figure name="Observations passed" value={scenarios.passedObservations} hint="Observations that passed." />
|
|
74
|
+
</SimpleGrid>
|
|
75
|
+
</Box>
|
|
76
|
+
</Stack>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function Reconciliation({ health }: { health: CatalogHealth }) {
|
|
81
|
+
const { reconciliation } = health;
|
|
82
|
+
let total = reconciliation.specs;
|
|
83
|
+
const rows = reconciliation.steps.map((step) => {
|
|
84
|
+
total += step.delta;
|
|
85
|
+
return (
|
|
86
|
+
<Table.Tr key={step.code}>
|
|
87
|
+
<Table.Td>{step.label}</Table.Td>
|
|
88
|
+
<Table.Td ta="right">{step.delta > 0 ? `+${step.delta}` : step.delta}</Table.Td>
|
|
89
|
+
<Table.Td ta="right">{total}</Table.Td>
|
|
90
|
+
</Table.Tr>
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
return (
|
|
94
|
+
<Box component="section" aria-label="Why Specs and Tests differ">
|
|
95
|
+
<Title order={3} mb="xs">
|
|
96
|
+
Why Specs and Tests differ
|
|
97
|
+
</Title>
|
|
98
|
+
<Table.ScrollContainer minWidth={420}>
|
|
99
|
+
<Table data-health-reconciliation>
|
|
100
|
+
<Table.Thead>
|
|
101
|
+
<Table.Tr>
|
|
102
|
+
<Table.Th scope="col">Step</Table.Th>
|
|
103
|
+
<Table.Th scope="col" ta="right">
|
|
104
|
+
Change
|
|
105
|
+
</Table.Th>
|
|
106
|
+
<Table.Th scope="col" ta="right">
|
|
107
|
+
Total
|
|
108
|
+
</Table.Th>
|
|
109
|
+
</Table.Tr>
|
|
110
|
+
</Table.Thead>
|
|
111
|
+
<Table.Tbody>
|
|
112
|
+
<Table.Tr>
|
|
113
|
+
<Table.Td>Spec files the Specs view lists</Table.Td>
|
|
114
|
+
<Table.Td />
|
|
115
|
+
<Table.Td ta="right">{reconciliation.specs}</Table.Td>
|
|
116
|
+
</Table.Tr>
|
|
117
|
+
{rows}
|
|
118
|
+
<Table.Tr>
|
|
119
|
+
<Table.Td fw={600}>Specs the Tests view groups its tests into</Table.Td>
|
|
120
|
+
<Table.Td />
|
|
121
|
+
<Table.Td ta="right" fw={600}>
|
|
122
|
+
{reconciliation.testSpecs}
|
|
123
|
+
</Table.Td>
|
|
124
|
+
</Table.Tr>
|
|
125
|
+
</Table.Tbody>
|
|
126
|
+
</Table>
|
|
127
|
+
</Table.ScrollContainer>
|
|
128
|
+
<List size="sm" mt="sm" data-health-explanation>
|
|
129
|
+
{reconciliation.explanation.map((line) => (
|
|
130
|
+
<List.Item key={line}>{line}</List.Item>
|
|
131
|
+
))}
|
|
132
|
+
</List>
|
|
133
|
+
</Box>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function Finding({ item }: { item: HealthFinding }) {
|
|
138
|
+
return (
|
|
139
|
+
<Accordion.Item value={item.code} data-health-finding={item.code}>
|
|
140
|
+
<Accordion.Control>
|
|
141
|
+
<Group gap="xs" wrap="wrap">
|
|
142
|
+
<Badge variant="light" color={item.severity === 'warning' ? 'orange' : 'blue'} tt="none">
|
|
143
|
+
{item.severity === 'warning' ? 'Warning' : 'Note'}
|
|
144
|
+
</Badge>
|
|
145
|
+
<Code>{item.code}</Code>
|
|
146
|
+
<Text span size="sm" c="dimmed">
|
|
147
|
+
{item.count} affected
|
|
148
|
+
</Text>
|
|
149
|
+
</Group>
|
|
150
|
+
</Accordion.Control>
|
|
151
|
+
<Accordion.Panel>
|
|
152
|
+
<Stack gap="xs">
|
|
153
|
+
<Text component="div" size="sm">
|
|
154
|
+
{item.message}
|
|
155
|
+
</Text>
|
|
156
|
+
{item.items.length > 0 && (
|
|
157
|
+
<List size="sm" listStyleType="none">
|
|
158
|
+
{item.items.map((entry) => (
|
|
159
|
+
<List.Item key={entry.path}>
|
|
160
|
+
<Code>{entry.path}</Code>
|
|
161
|
+
{entry.candidates?.map((candidate) => (
|
|
162
|
+
<Text key={candidate.path} component="div" size="xs" c="dimmed" ml="md">
|
|
163
|
+
may be the same promise as <Code>{candidate.path}</Code> ({candidate.reason}); not merged
|
|
164
|
+
</Text>
|
|
165
|
+
))}
|
|
166
|
+
</List.Item>
|
|
167
|
+
))}
|
|
168
|
+
</List>
|
|
169
|
+
)}
|
|
170
|
+
{item.count > item.items.length && (
|
|
171
|
+
<Text size="xs" c="dimmed" component="div">
|
|
172
|
+
and {item.count - item.items.length} more
|
|
173
|
+
</Text>
|
|
174
|
+
)}
|
|
175
|
+
</Stack>
|
|
176
|
+
</Accordion.Panel>
|
|
177
|
+
</Accordion.Item>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function Mappings({ health, tried }: { health: CatalogHealth | undefined; tried: readonly RootMapping[] }) {
|
|
182
|
+
const go = useExplorerNavigate();
|
|
183
|
+
const [from, setFrom] = useState('');
|
|
184
|
+
const [to, setTo] = useState('');
|
|
185
|
+
const configured = (health?.roots ?? []).filter((root) => root.source === 'configuration');
|
|
186
|
+
const write = (list: readonly RootMapping[]) => go({ root: list.length ? list.map(label).join(',') : undefined }, { replace: true });
|
|
187
|
+
const known = [...configured, ...tried].some((root) => root.from === from.trim() && root.to === to.trim());
|
|
188
|
+
const matched = (root: RootMapping) => health?.roots.find((item) => item.from === root.from && item.to === root.to && item.source !== 'configuration')?.matched;
|
|
189
|
+
return (
|
|
190
|
+
<Box component="section" aria-label="Root mappings">
|
|
191
|
+
<Title order={3} mb="xs">
|
|
192
|
+
Root mappings
|
|
193
|
+
</Title>
|
|
194
|
+
<Text size="sm" c="dimmed" mb="sm">
|
|
195
|
+
Two spec paths are one spec only when a mapping says so. A mapping states that paths under one folder are the same specs as paths under another; Atlas never merges paths by a similar title. A mapping tried here is kept in the address and changes nothing on the server. To keep one, set <Code>catalog.roots</Code> or <Code>ATLAS_CATALOG_ROOTS</Code> on the server.
|
|
196
|
+
</Text>
|
|
197
|
+
{configured.length + tried.length > 0 && (
|
|
198
|
+
<List size="sm" listStyleType="none" mb="sm" aria-label="Declared root mappings">
|
|
199
|
+
{configured.map((root) => (
|
|
200
|
+
<List.Item key={`c${label(root)}`}>
|
|
201
|
+
<Group gap="xs" wrap="wrap">
|
|
202
|
+
<Code>{label(root)}</Code>
|
|
203
|
+
<Badge variant="light" tt="none">
|
|
204
|
+
Configured on the server
|
|
205
|
+
</Badge>
|
|
206
|
+
<Text span size="xs" c="dimmed">
|
|
207
|
+
changed {plural(root.matched, 'path', 'paths')}
|
|
208
|
+
</Text>
|
|
209
|
+
</Group>
|
|
210
|
+
</List.Item>
|
|
211
|
+
))}
|
|
212
|
+
{tried.map((root) => (
|
|
213
|
+
<List.Item key={`t${label(root)}`}>
|
|
214
|
+
<Group gap="xs" wrap="wrap">
|
|
215
|
+
<Code>{label(root)}</Code>
|
|
216
|
+
<Badge variant="light" color="gray" tt="none">
|
|
217
|
+
Tried here
|
|
218
|
+
</Badge>
|
|
219
|
+
{matched(root) !== undefined && (
|
|
220
|
+
<Text span size="xs" c="dimmed">
|
|
221
|
+
changed {plural(matched(root)!, 'path', 'paths')}
|
|
222
|
+
</Text>
|
|
223
|
+
)}
|
|
224
|
+
<Tooltip label="Stop trying this mapping">
|
|
225
|
+
<ActionIcon variant="subtle" color="gray" aria-label={`Stop trying ${label(root)}`} onClick={() => write(tried.filter((item) => item !== root))}>
|
|
226
|
+
<IconTrash size={16} aria-hidden="true" />
|
|
227
|
+
</ActionIcon>
|
|
228
|
+
</Tooltip>
|
|
229
|
+
</Group>
|
|
230
|
+
</List.Item>
|
|
231
|
+
))}
|
|
232
|
+
</List>
|
|
233
|
+
)}
|
|
234
|
+
<form
|
|
235
|
+
onSubmit={(event) => {
|
|
236
|
+
event.preventDefault();
|
|
237
|
+
if (!to.trim() || known) return;
|
|
238
|
+
write([...tried, { from: from.trim(), to: to.trim() }]);
|
|
239
|
+
setFrom('');
|
|
240
|
+
setTo('');
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
<Group align="flex-end" gap="md" wrap="wrap">
|
|
244
|
+
<TextInput label="Paths under" description="Leave empty to match every path" placeholder="checkout/" value={from} onChange={(event) => setFrom(event.currentTarget.value)} />
|
|
245
|
+
<TextInput label="are the same specs as paths under" description="Where the specs are recorded" placeholder="specs/checkout/" value={to} onChange={(event) => setTo(event.currentTarget.value)} />
|
|
246
|
+
<Button type="submit" disabled={!to.trim() || known}>
|
|
247
|
+
Try this mapping
|
|
248
|
+
</Button>
|
|
249
|
+
</Group>
|
|
250
|
+
</form>
|
|
251
|
+
</Box>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The Catalog health tab. */
|
|
256
|
+
export function HealthView() {
|
|
257
|
+
const search = useExplorerSearch();
|
|
258
|
+
const tried = useMemo(() => (search.root ? parseRootMappings(new URLSearchParams([['root', search.root]])) : []), [search.root]);
|
|
259
|
+
const answer = useCatalogHealth(tried);
|
|
260
|
+
const health = answer.data && !answer.data.unsupported ? answer.data.health : undefined;
|
|
261
|
+
const warnings = health?.findings.filter((item) => item.severity === 'warning') ?? [];
|
|
262
|
+
return (
|
|
263
|
+
<Box aria-label="Catalog health" component="section" mb="lg">
|
|
264
|
+
<Title order={2} mb="sm">
|
|
265
|
+
Catalog health
|
|
266
|
+
</Title>
|
|
267
|
+
<Text component="div" c="dimmed" mb="md">
|
|
268
|
+
Specs, domains and coverage counts can disagree because of how specs are named and described, not because of a defect in a test. This explains each number, where the totals differ, and what to change at the source. It only reads: it changes no result.
|
|
269
|
+
</Text>
|
|
270
|
+
<Stack gap="lg">
|
|
271
|
+
<Mappings health={health} tried={tried} />
|
|
272
|
+
{answer.isPending && (
|
|
273
|
+
<Text role="status" component="div" c="dimmed">
|
|
274
|
+
Reading the catalog…
|
|
275
|
+
</Text>
|
|
276
|
+
)}
|
|
277
|
+
{answer.isError && (
|
|
278
|
+
<Alert role="alert" color="red">
|
|
279
|
+
The catalog health could not be read. {answer.error.message}
|
|
280
|
+
<Group mt="xs">
|
|
281
|
+
<Button type="button" variant="light" onClick={() => void answer.refetch()}>
|
|
282
|
+
Try again
|
|
283
|
+
</Button>
|
|
284
|
+
</Group>
|
|
285
|
+
</Alert>
|
|
286
|
+
)}
|
|
287
|
+
{answer.data?.unsupported && (
|
|
288
|
+
<Alert role="status" color="blue">
|
|
289
|
+
This server is older than the catalog health report, so it cannot explain these counts. Upgrade the server to read it.
|
|
290
|
+
</Alert>
|
|
291
|
+
)}
|
|
292
|
+
{health && (
|
|
293
|
+
<>
|
|
294
|
+
<Alert role="status" color={health.ok ? 'green' : 'orange'} title={health.ok ? 'No warnings' : plural(warnings.length, 'warning', 'warnings')} data-health-summary>
|
|
295
|
+
{health.ok ? 'Nothing found that makes a count or a domain filter misleading.' : 'These make a count or a domain filter misleading. Each says what to change at its source.'}
|
|
296
|
+
</Alert>
|
|
297
|
+
<Reconciliation health={health} />
|
|
298
|
+
<Counts health={health} />
|
|
299
|
+
{health.aliases.length > 0 && (
|
|
300
|
+
<Box component="section" aria-label="Specs under more than one path">
|
|
301
|
+
<Title order={3} mb="xs">
|
|
302
|
+
Specs under more than one path
|
|
303
|
+
</Title>
|
|
304
|
+
<Text size="sm" c="dimmed" mb="xs">
|
|
305
|
+
Joined because a root mapping relates them. Every original path stays inspectable.
|
|
306
|
+
</Text>
|
|
307
|
+
<Table.ScrollContainer minWidth={420}>
|
|
308
|
+
<Table data-health-aliases>
|
|
309
|
+
<Table.Thead>
|
|
310
|
+
<Table.Tr>
|
|
311
|
+
<Table.Th scope="col">Spec</Table.Th>
|
|
312
|
+
<Table.Th scope="col">Original paths</Table.Th>
|
|
313
|
+
</Table.Tr>
|
|
314
|
+
</Table.Thead>
|
|
315
|
+
<Table.Tbody>
|
|
316
|
+
{health.aliases.map((alias) => (
|
|
317
|
+
<Table.Tr key={alias.path}>
|
|
318
|
+
<Table.Td>
|
|
319
|
+
<Code>{alias.path}</Code>
|
|
320
|
+
</Table.Td>
|
|
321
|
+
<Table.Td>
|
|
322
|
+
<Stack gap={2}>
|
|
323
|
+
{alias.sources.map((source) => (
|
|
324
|
+
<Code key={source}>{source}</Code>
|
|
325
|
+
))}
|
|
326
|
+
</Stack>
|
|
327
|
+
</Table.Td>
|
|
328
|
+
</Table.Tr>
|
|
329
|
+
))}
|
|
330
|
+
</Table.Tbody>
|
|
331
|
+
</Table>
|
|
332
|
+
</Table.ScrollContainer>
|
|
333
|
+
</Box>
|
|
334
|
+
)}
|
|
335
|
+
<Box component="section" aria-label="Domains">
|
|
336
|
+
<Title order={3} mb="xs">
|
|
337
|
+
Domains
|
|
338
|
+
</Title>
|
|
339
|
+
<Text size="sm" c="dimmed" mb="xs">
|
|
340
|
+
A domain is what a spec's own metadata says. Explicit metadata is always authoritative; a domain shared by nearly every spec is reported as a finding below, not changed.
|
|
341
|
+
</Text>
|
|
342
|
+
{health.domains.length === 0 ? (
|
|
343
|
+
<Text component="div" c="dimmed">
|
|
344
|
+
No spec declares a domain.
|
|
345
|
+
</Text>
|
|
346
|
+
) : (
|
|
347
|
+
<Table.ScrollContainer minWidth={320}>
|
|
348
|
+
<Table data-health-domains>
|
|
349
|
+
<Table.Thead>
|
|
350
|
+
<Table.Tr>
|
|
351
|
+
<Table.Th scope="col">Domain</Table.Th>
|
|
352
|
+
<Table.Th scope="col" ta="right">
|
|
353
|
+
Specs
|
|
354
|
+
</Table.Th>
|
|
355
|
+
</Table.Tr>
|
|
356
|
+
</Table.Thead>
|
|
357
|
+
<Table.Tbody>
|
|
358
|
+
{health.domains.map((domain) => (
|
|
359
|
+
<Table.Tr key={domain.name}>
|
|
360
|
+
<Table.Td>{domain.name}</Table.Td>
|
|
361
|
+
<Table.Td ta="right">{domain.specs}</Table.Td>
|
|
362
|
+
</Table.Tr>
|
|
363
|
+
))}
|
|
364
|
+
</Table.Tbody>
|
|
365
|
+
</Table>
|
|
366
|
+
</Table.ScrollContainer>
|
|
367
|
+
)}
|
|
368
|
+
</Box>
|
|
369
|
+
<Box component="section" aria-label="Findings">
|
|
370
|
+
<Title order={3} mb="xs">
|
|
371
|
+
Findings
|
|
372
|
+
</Title>
|
|
373
|
+
{health.findings.length === 0 ? (
|
|
374
|
+
<Text component="div" c="dimmed">
|
|
375
|
+
Nothing to report.
|
|
376
|
+
</Text>
|
|
377
|
+
) : (
|
|
378
|
+
<Accordion multiple defaultValue={warnings.map((item) => item.code)}>
|
|
379
|
+
{health.findings.map((item) => (
|
|
380
|
+
<Finding key={item.code} item={item} />
|
|
381
|
+
))}
|
|
382
|
+
</Accordion>
|
|
383
|
+
)}
|
|
384
|
+
</Box>
|
|
385
|
+
<Box component="section" aria-label="Identity">
|
|
386
|
+
<Title order={3} mb="xs">
|
|
387
|
+
Identity
|
|
388
|
+
</Title>
|
|
389
|
+
<Text size="sm" component="div" data-health-identity>
|
|
390
|
+
Fallback identity (known by title only): {health.identity.fallbackIdentity} of {health.identity.checks} checks.
|
|
391
|
+
{health.identity.declaredBehaviorIds !== undefined && ` Declared behavior ids: ${health.identity.declaredBehaviorIds}. Declared example ids: ${health.identity.declaredExampleIds ?? 0}.`}
|
|
392
|
+
</Text>
|
|
393
|
+
<Text size="sm" c="dimmed" component="div" mt="xs">
|
|
394
|
+
{health.identity.basis}
|
|
395
|
+
</Text>
|
|
396
|
+
</Box>
|
|
397
|
+
</>
|
|
398
|
+
)}
|
|
399
|
+
</Stack>
|
|
400
|
+
</Box>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type FilterName,
|
|
27
27
|
type QueryFacets
|
|
28
28
|
} from '@crvouga/atlas/reports/artifact-query';
|
|
29
|
+
import { catalogHealth, type CatalogHealth, type RootMapping } from '@crvouga/atlas/reports/catalog-health';
|
|
29
30
|
import {
|
|
30
31
|
artifactList,
|
|
31
32
|
catalogModel,
|
|
@@ -371,3 +372,35 @@ export function useRunDetail(key: string | undefined) {
|
|
|
371
372
|
retry: false
|
|
372
373
|
});
|
|
373
374
|
}
|
|
375
|
+
|
|
376
|
+
/** What the Catalog health tab reads: the diagnostics, or that this server is too old to give them. */
|
|
377
|
+
export type HealthAnswer = { readonly unsupported: false; readonly health: CatalogHealth } | { readonly unsupported: true };
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Why the Specs, domains and counts of the report disagree. From the server when it has one (it also applies the root
|
|
381
|
+
* mappings it is configured with, and says so in `roots[].source`); otherwise worked out here from the specs and
|
|
382
|
+
* results this page holds. A server without the route answers `unsupported`, and the page says so.
|
|
383
|
+
*/
|
|
384
|
+
export function useCatalogHealth(roots: readonly RootMapping[]) {
|
|
385
|
+
const catalog = useTestCatalog();
|
|
386
|
+
const asked = roots.map((root) => `${root.from}>${root.to}`);
|
|
387
|
+
return useQuery({
|
|
388
|
+
queryKey: ['catalog-health', catalog.version, asked.join('\u0001')],
|
|
389
|
+
queryFn: async ({ signal }): Promise<HealthAnswer> => {
|
|
390
|
+
if (catalog.source === 'server') {
|
|
391
|
+
const params = new URLSearchParams(asked.map((root) => ['root', root]));
|
|
392
|
+
try {
|
|
393
|
+
return { unsupported: false, health: await reportJson<CatalogHealth>(`/tests/health${params.size ? `?${params}` : ''}`, signal) };
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (notFound(error)) return { unsupported: true };
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { unsupported: false, health: catalogHealth(catalog.model!, roots) };
|
|
400
|
+
},
|
|
401
|
+
enabled: catalog.model !== null,
|
|
402
|
+
placeholderData: keepPreviousData,
|
|
403
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
404
|
+
retry: false
|
|
405
|
+
});
|
|
406
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module Explorer address
|
|
3
3
|
*
|
|
4
4
|
* Everything the test explorer shows is in the address of the `/` route, so a link reproduces the view: the open test,
|
|
5
|
-
* the words searched, the filters, the grouping, the order, the Runs tab with the run opened in it, the Compare tab with its two pinned runs (`base`, `cand`) and the behavior opened (`pair`), the exact layer a test's detail is pinned to, the recording and position of a shared moment in it (`clip`, `clipAt`), and the one file of a run's layer its Artifacts select. Filters that
|
|
5
|
+
* the words searched, the filters, the grouping, the order, the Runs tab with the run opened in it, the Compare tab with its two pinned runs (`base`, `cand`) and the behavior opened (`pair`), the Catalog health tab with the root mappings it tries (`root`), the exact layer a test's detail is pinned to, the recording and position of a shared moment in it (`clip`, `clipAt`), and the one file of a run's layer its Artifacts select. Filters that
|
|
6
6
|
* take several values are comma-separated (`status=failed,flaky`); a comma or backslash inside a value is written `\,`
|
|
7
7
|
* or `\\`, and the router encodes `#` and spaces. Anything unreadable is dropped, never an error.
|
|
8
8
|
*
|
|
@@ -63,7 +63,9 @@ export const ExplorerSearchFields = {
|
|
|
63
63
|
inRun: text,
|
|
64
64
|
group: z.enum(TEST_GROUPS).optional().catch(undefined),
|
|
65
65
|
sort: z.enum(EXPLORER_SORTS).optional().catch(undefined),
|
|
66
|
-
tab: z.enum(['runs', 'artifacts', 'collections', 'compare']).optional().catch(undefined),
|
|
66
|
+
tab: z.enum(['runs', 'artifacts', 'collections', 'compare', 'health']).optional().catch(undefined),
|
|
67
|
+
/** The root mappings (`from>to`, comma-separated) the Catalog health tab tries, beside those the server is configured with. */
|
|
68
|
+
root: text,
|
|
67
69
|
/** The review collection opened in the Collections tab (its stable id), and the revision of it (default: the newest). */
|
|
68
70
|
collection: text,
|
|
69
71
|
rev: z.coerce.number().int().positive().optional().catch(undefined),
|
|
@@ -127,7 +129,8 @@ export type ExplorerSearch = {
|
|
|
127
129
|
readonly inRun?: string;
|
|
128
130
|
readonly group?: TestGroup;
|
|
129
131
|
readonly sort?: ExplorerSort;
|
|
130
|
-
readonly tab?: 'runs' | 'artifacts' | 'collections' | 'compare';
|
|
132
|
+
readonly tab?: 'runs' | 'artifacts' | 'collections' | 'compare' | 'health';
|
|
133
|
+
readonly root?: string;
|
|
131
134
|
readonly collection?: string;
|
|
132
135
|
readonly rev?: number;
|
|
133
136
|
readonly base?: string;
|