@crvouga/atlas 1.15.0 → 1.17.0

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