@crvouga/atlas 1.2.0 → 1.3.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 (43) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/reports/src/evidence-detail.d.ts +14 -3
  3. package/dist/reports/src/evidence-detail.js +33 -19
  4. package/dist/reports/src/evidence-scope.d.ts +83 -0
  5. package/dist/reports/src/evidence-scope.js +129 -0
  6. package/dist/reports/src/evidence-types.d.ts +3 -1
  7. package/dist/reports/src/explorer-types.d.ts +1 -1
  8. package/dist/reports/src/layer-merge.js +3 -2
  9. package/dist/reports/src/layer-types.d.ts +1 -1
  10. package/dist/reports/src/publish-checks.js +1 -0
  11. package/dist/reports/src/reporter.d.ts +1 -1
  12. package/dist/reports/src/reports-explorer-route.d.ts +276 -0
  13. package/dist/reports/src/reports-route.d.ts +276 -0
  14. package/dist/reports/src/test-catalog.d.ts +7 -1
  15. package/dist/reports/src/test-catalog.js +15 -3
  16. package/dist/schema/src/api.d.ts +520 -0
  17. package/dist/schema/src/api.js +7 -4
  18. package/dist/schema/src/layer.d.ts +981 -0
  19. package/dist/schema/src/layer.js +43 -4
  20. package/dist/server/src/openapi.js +1 -0
  21. package/docs/extending.md +1 -0
  22. package/docs/reference/http-api.md +2 -0
  23. package/docs/reference/reports.md +179 -7
  24. package/docs/reference/schema.md +23 -0
  25. package/docs/rich-evidence.md +47 -0
  26. package/llms-full.txt +50 -0
  27. package/package.json +1 -1
  28. package/packages/schema/schemas/check-detail.schema.json +673 -1
  29. package/packages/schema/schemas/effective-delta.schema.json +192 -0
  30. package/packages/schema/schemas/effective-evidence.schema.json +192 -0
  31. package/packages/schema/schemas/evidence-page.schema.json +96 -0
  32. package/packages/schema/schemas/layer-rows.schema.json +96 -0
  33. package/packages/schema/schemas/layer.schema.json +1598 -62
  34. package/packages/schema/schemas/publish-checks.schema.json +768 -0
  35. package/packages/schema/schemas/report-run.schema.json +96 -0
  36. package/packages/schema/schemas/test-catalog.schema.json +128 -0
  37. package/packages/schema/schemas/test-history.schema.json +202 -0
  38. package/packages/server/openapi.json +2581 -23
  39. package/packages/visualizer/src/evidence/artifacts.tsx +24 -18
  40. package/packages/visualizer/src/evidence/evidence-scope.tsx +106 -0
  41. package/packages/visualizer/src/evidence/format.ts +11 -0
  42. package/packages/visualizer/src/explorer/Explorer.tsx +19 -0
  43. package/packages/visualizer/src/explorer/search.ts +5 -1
@@ -105,6 +105,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
105
105
  } | null;
106
106
  parts: readonly {
107
107
  tags?: string[] | undefined;
108
+ scope?: {
109
+ [x: string]: import("hono/utils/types").JSONValue;
110
+ surface?: string | undefined;
111
+ deviceKind?: string | undefined;
112
+ syntheticData?: boolean | undefined;
113
+ dependencies?: string | undefined;
114
+ audioCaptured?: boolean | undefined;
115
+ pacing?: string | undefined;
116
+ asserted?: string[] | undefined;
117
+ limitations?: string[] | undefined;
118
+ declaredBy?: string | undefined;
119
+ verified?: string[] | undefined;
120
+ verifiedBy?: string | undefined;
121
+ provenance?: {
122
+ [x: string]: import("hono/utils/types").JSONValue;
123
+ url: string;
124
+ name?: string | undefined;
125
+ rel?: string | undefined;
126
+ }[] | undefined;
127
+ extensions?: {
128
+ [x: string]: import("hono/utils/types").JSONValue;
129
+ } | undefined;
130
+ } | undefined;
108
131
  attachments?: {
109
132
  [x: string]: import("hono/utils/types").JSONValue;
110
133
  name: string;
@@ -121,6 +144,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
121
144
  durationMs?: number | undefined;
122
145
  description?: string | undefined;
123
146
  omitted?: string | undefined;
147
+ scope?: {
148
+ [x: string]: import("hono/utils/types").JSONValue;
149
+ surface?: string | undefined;
150
+ deviceKind?: string | undefined;
151
+ syntheticData?: boolean | undefined;
152
+ dependencies?: string | undefined;
153
+ audioCaptured?: boolean | undefined;
154
+ pacing?: string | undefined;
155
+ asserted?: string[] | undefined;
156
+ limitations?: string[] | undefined;
157
+ declaredBy?: string | undefined;
158
+ verified?: string[] | undefined;
159
+ verifiedBy?: string | undefined;
160
+ provenance?: {
161
+ [x: string]: import("hono/utils/types").JSONValue;
162
+ url: string;
163
+ name?: string | undefined;
164
+ rel?: string | undefined;
165
+ }[] | undefined;
166
+ extensions?: {
167
+ [x: string]: import("hono/utils/types").JSONValue;
168
+ } | undefined;
169
+ } | undefined;
124
170
  tags?: string[] | undefined;
125
171
  at?: {
126
172
  [x: string]: import("hono/utils/types").JSONValue;
@@ -140,6 +186,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
140
186
  durationMs?: number | undefined;
141
187
  startedAtMs?: number | undefined;
142
188
  detail?: string | undefined;
189
+ scope?: {
190
+ [x: string]: import("hono/utils/types").JSONValue;
191
+ surface?: string | undefined;
192
+ deviceKind?: string | undefined;
193
+ syntheticData?: boolean | undefined;
194
+ dependencies?: string | undefined;
195
+ audioCaptured?: boolean | undefined;
196
+ pacing?: string | undefined;
197
+ asserted?: string[] | undefined;
198
+ limitations?: string[] | undefined;
199
+ declaredBy?: string | undefined;
200
+ verified?: string[] | undefined;
201
+ verifiedBy?: string | undefined;
202
+ provenance?: {
203
+ [x: string]: import("hono/utils/types").JSONValue;
204
+ url: string;
205
+ name?: string | undefined;
206
+ rel?: string | undefined;
207
+ }[] | undefined;
208
+ extensions?: {
209
+ [x: string]: import("hono/utils/types").JSONValue;
210
+ } | undefined;
211
+ } | undefined;
143
212
  steps?: {
144
213
  [x: string]: import("hono/utils/types").JSONValue;
145
214
  name: string;
@@ -166,6 +235,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
166
235
  durationMs?: number | undefined;
167
236
  description?: string | undefined;
168
237
  omitted?: string | undefined;
238
+ scope?: {
239
+ [x: string]: import("hono/utils/types").JSONValue;
240
+ surface?: string | undefined;
241
+ deviceKind?: string | undefined;
242
+ syntheticData?: boolean | undefined;
243
+ dependencies?: string | undefined;
244
+ audioCaptured?: boolean | undefined;
245
+ pacing?: string | undefined;
246
+ asserted?: string[] | undefined;
247
+ limitations?: string[] | undefined;
248
+ declaredBy?: string | undefined;
249
+ verified?: string[] | undefined;
250
+ verifiedBy?: string | undefined;
251
+ provenance?: {
252
+ [x: string]: import("hono/utils/types").JSONValue;
253
+ url: string;
254
+ name?: string | undefined;
255
+ rel?: string | undefined;
256
+ }[] | undefined;
257
+ extensions?: {
258
+ [x: string]: import("hono/utils/types").JSONValue;
259
+ } | undefined;
260
+ } | undefined;
169
261
  tags?: string[] | undefined;
170
262
  at?: {
171
263
  [x: string]: import("hono/utils/types").JSONValue;
@@ -224,6 +316,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
224
316
  durationMs?: number | undefined;
225
317
  description?: string | undefined;
226
318
  omitted?: string | undefined;
319
+ scope?: {
320
+ [x: string]: import("hono/utils/types").JSONValue;
321
+ surface?: string | undefined;
322
+ deviceKind?: string | undefined;
323
+ syntheticData?: boolean | undefined;
324
+ dependencies?: string | undefined;
325
+ audioCaptured?: boolean | undefined;
326
+ pacing?: string | undefined;
327
+ asserted?: string[] | undefined;
328
+ limitations?: string[] | undefined;
329
+ declaredBy?: string | undefined;
330
+ verified?: string[] | undefined;
331
+ verifiedBy?: string | undefined;
332
+ provenance?: {
333
+ [x: string]: import("hono/utils/types").JSONValue;
334
+ url: string;
335
+ name?: string | undefined;
336
+ rel?: string | undefined;
337
+ }[] | undefined;
338
+ extensions?: {
339
+ [x: string]: import("hono/utils/types").JSONValue;
340
+ } | undefined;
341
+ } | undefined;
227
342
  tags?: string[] | undefined;
228
343
  at?: {
229
344
  [x: string]: import("hono/utils/types").JSONValue;
@@ -292,6 +407,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
292
407
  durationMs?: number | undefined;
293
408
  description?: string | undefined;
294
409
  omitted?: string | undefined;
410
+ scope?: {
411
+ [x: string]: import("hono/utils/types").JSONValue;
412
+ surface?: string | undefined;
413
+ deviceKind?: string | undefined;
414
+ syntheticData?: boolean | undefined;
415
+ dependencies?: string | undefined;
416
+ audioCaptured?: boolean | undefined;
417
+ pacing?: string | undefined;
418
+ asserted?: string[] | undefined;
419
+ limitations?: string[] | undefined;
420
+ declaredBy?: string | undefined;
421
+ verified?: string[] | undefined;
422
+ verifiedBy?: string | undefined;
423
+ provenance?: {
424
+ [x: string]: import("hono/utils/types").JSONValue;
425
+ url: string;
426
+ name?: string | undefined;
427
+ rel?: string | undefined;
428
+ }[] | undefined;
429
+ extensions?: {
430
+ [x: string]: import("hono/utils/types").JSONValue;
431
+ } | undefined;
432
+ } | undefined;
295
433
  tags?: string[] | undefined;
296
434
  at?: {
297
435
  [x: string]: import("hono/utils/types").JSONValue;
@@ -383,6 +521,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
383
521
  readonly recordedAtMs: number;
384
522
  readonly checks: readonly {
385
523
  tags?: string[] | undefined;
524
+ scope?: {
525
+ [x: string]: import("hono/utils/types").JSONValue;
526
+ surface?: string | undefined;
527
+ deviceKind?: string | undefined;
528
+ syntheticData?: boolean | undefined;
529
+ dependencies?: string | undefined;
530
+ audioCaptured?: boolean | undefined;
531
+ pacing?: string | undefined;
532
+ asserted?: string[] | undefined;
533
+ limitations?: string[] | undefined;
534
+ declaredBy?: string | undefined;
535
+ verified?: string[] | undefined;
536
+ verifiedBy?: string | undefined;
537
+ provenance?: {
538
+ [x: string]: import("hono/utils/types").JSONValue;
539
+ url: string;
540
+ name?: string | undefined;
541
+ rel?: string | undefined;
542
+ }[] | undefined;
543
+ extensions?: {
544
+ [x: string]: import("hono/utils/types").JSONValue;
545
+ } | undefined;
546
+ } | undefined;
386
547
  attachments?: {
387
548
  [x: string]: import("hono/utils/types").JSONValue;
388
549
  name: string;
@@ -399,6 +560,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
399
560
  durationMs?: number | undefined;
400
561
  description?: string | undefined;
401
562
  omitted?: string | undefined;
563
+ scope?: {
564
+ [x: string]: import("hono/utils/types").JSONValue;
565
+ surface?: string | undefined;
566
+ deviceKind?: string | undefined;
567
+ syntheticData?: boolean | undefined;
568
+ dependencies?: string | undefined;
569
+ audioCaptured?: boolean | undefined;
570
+ pacing?: string | undefined;
571
+ asserted?: string[] | undefined;
572
+ limitations?: string[] | undefined;
573
+ declaredBy?: string | undefined;
574
+ verified?: string[] | undefined;
575
+ verifiedBy?: string | undefined;
576
+ provenance?: {
577
+ [x: string]: import("hono/utils/types").JSONValue;
578
+ url: string;
579
+ name?: string | undefined;
580
+ rel?: string | undefined;
581
+ }[] | undefined;
582
+ extensions?: {
583
+ [x: string]: import("hono/utils/types").JSONValue;
584
+ } | undefined;
585
+ } | undefined;
402
586
  tags?: string[] | undefined;
403
587
  at?: {
404
588
  [x: string]: import("hono/utils/types").JSONValue;
@@ -418,6 +602,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
418
602
  durationMs?: number | undefined;
419
603
  startedAtMs?: number | undefined;
420
604
  detail?: string | undefined;
605
+ scope?: {
606
+ [x: string]: import("hono/utils/types").JSONValue;
607
+ surface?: string | undefined;
608
+ deviceKind?: string | undefined;
609
+ syntheticData?: boolean | undefined;
610
+ dependencies?: string | undefined;
611
+ audioCaptured?: boolean | undefined;
612
+ pacing?: string | undefined;
613
+ asserted?: string[] | undefined;
614
+ limitations?: string[] | undefined;
615
+ declaredBy?: string | undefined;
616
+ verified?: string[] | undefined;
617
+ verifiedBy?: string | undefined;
618
+ provenance?: {
619
+ [x: string]: import("hono/utils/types").JSONValue;
620
+ url: string;
621
+ name?: string | undefined;
622
+ rel?: string | undefined;
623
+ }[] | undefined;
624
+ extensions?: {
625
+ [x: string]: import("hono/utils/types").JSONValue;
626
+ } | undefined;
627
+ } | undefined;
421
628
  steps?: {
422
629
  [x: string]: import("hono/utils/types").JSONValue;
423
630
  name: string;
@@ -444,6 +651,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
444
651
  durationMs?: number | undefined;
445
652
  description?: string | undefined;
446
653
  omitted?: string | undefined;
654
+ scope?: {
655
+ [x: string]: import("hono/utils/types").JSONValue;
656
+ surface?: string | undefined;
657
+ deviceKind?: string | undefined;
658
+ syntheticData?: boolean | undefined;
659
+ dependencies?: string | undefined;
660
+ audioCaptured?: boolean | undefined;
661
+ pacing?: string | undefined;
662
+ asserted?: string[] | undefined;
663
+ limitations?: string[] | undefined;
664
+ declaredBy?: string | undefined;
665
+ verified?: string[] | undefined;
666
+ verifiedBy?: string | undefined;
667
+ provenance?: {
668
+ [x: string]: import("hono/utils/types").JSONValue;
669
+ url: string;
670
+ name?: string | undefined;
671
+ rel?: string | undefined;
672
+ }[] | undefined;
673
+ extensions?: {
674
+ [x: string]: import("hono/utils/types").JSONValue;
675
+ } | undefined;
676
+ } | undefined;
447
677
  tags?: string[] | undefined;
448
678
  at?: {
449
679
  [x: string]: import("hono/utils/types").JSONValue;
@@ -502,6 +732,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
502
732
  durationMs?: number | undefined;
503
733
  description?: string | undefined;
504
734
  omitted?: string | undefined;
735
+ scope?: {
736
+ [x: string]: import("hono/utils/types").JSONValue;
737
+ surface?: string | undefined;
738
+ deviceKind?: string | undefined;
739
+ syntheticData?: boolean | undefined;
740
+ dependencies?: string | undefined;
741
+ audioCaptured?: boolean | undefined;
742
+ pacing?: string | undefined;
743
+ asserted?: string[] | undefined;
744
+ limitations?: string[] | undefined;
745
+ declaredBy?: string | undefined;
746
+ verified?: string[] | undefined;
747
+ verifiedBy?: string | undefined;
748
+ provenance?: {
749
+ [x: string]: import("hono/utils/types").JSONValue;
750
+ url: string;
751
+ name?: string | undefined;
752
+ rel?: string | undefined;
753
+ }[] | undefined;
754
+ extensions?: {
755
+ [x: string]: import("hono/utils/types").JSONValue;
756
+ } | undefined;
757
+ } | undefined;
505
758
  tags?: string[] | undefined;
506
759
  at?: {
507
760
  [x: string]: import("hono/utils/types").JSONValue;
@@ -570,6 +823,29 @@ export declare function createReportsApp(deps: ReportsAppDeps): import("hono/hon
570
823
  durationMs?: number | undefined;
571
824
  description?: string | undefined;
572
825
  omitted?: string | undefined;
826
+ scope?: {
827
+ [x: string]: import("hono/utils/types").JSONValue;
828
+ surface?: string | undefined;
829
+ deviceKind?: string | undefined;
830
+ syntheticData?: boolean | undefined;
831
+ dependencies?: string | undefined;
832
+ audioCaptured?: boolean | undefined;
833
+ pacing?: string | undefined;
834
+ asserted?: string[] | undefined;
835
+ limitations?: string[] | undefined;
836
+ declaredBy?: string | undefined;
837
+ verified?: string[] | undefined;
838
+ verifiedBy?: string | undefined;
839
+ provenance?: {
840
+ [x: string]: import("hono/utils/types").JSONValue;
841
+ url: string;
842
+ name?: string | undefined;
843
+ rel?: string | undefined;
844
+ }[] | undefined;
845
+ extensions?: {
846
+ [x: string]: import("hono/utils/types").JSONValue;
847
+ } | undefined;
848
+ } | undefined;
573
849
  tags?: string[] | undefined;
574
850
  at?: {
575
851
  [x: string]: import("hono/utils/types").JSONValue;
@@ -99,6 +99,8 @@ export type TestSummary = {
99
99
  readonly artifactTags?: readonly string[];
100
100
  /** The normalized tags its attachments inherit from their check and run (those of the results that have attachments), apart from `artifactTags`. What an inherited artifact query also matches. */
101
101
  readonly inheritedArtifactTags?: readonly string[];
102
+ /** What each distinct playable recording of its current results declares it shows, as filter tokens (`device:physical`, `audio:silent`; see `scopeTokens`). A recording that declared nothing is `['unknown']`. Absent when it has no playable recording. A query's `scope` must hold for one recording, so evidence from two different devices never combines. */
103
+ readonly scopeProfiles?: readonly (readonly string[])[];
102
104
  readonly labels: Readonly<Record<string, string>>;
103
105
  /** What its current results carry: attachments, steps, retries, and the kinds of attachment. */
104
106
  readonly evidence: {
@@ -172,6 +174,8 @@ export type CatalogFacets = {
172
174
  readonly tags: readonly FacetValue[];
173
175
  /** The tags of attachments, normalized. Absent in documents of older servers. */
174
176
  readonly artifactTags?: readonly FacetValue[];
177
+ /** The scope tokens of its recordings (`device:simulator`, `audio:recorded`, `unknown`), with how many tests have a recording that states each. Absent in documents of older servers. */
178
+ readonly scopes?: readonly FacetValue[];
175
179
  readonly branches: readonly FacetValue[];
176
180
  readonly labels: Readonly<Record<string, readonly FacetValue[]>>;
177
181
  };
@@ -195,6 +199,8 @@ export type TestQuery = {
195
199
  readonly artifactTag?: readonly string[];
196
200
  /** With `artifactTag`: also match the tags an attachment inherits from its check and run (a recording without a tag of its own under a run tagged `demo`). Default false: the exact query. */
197
201
  readonly inherited?: boolean;
202
+ /** Only tests with one playable recording that states every one of these (`device:physical`, `audio:recorded`, `data:synthetic`, `dependencies:live`, `surface:browser`, `pacing:paced`; see `scopeTokens`). Evidence that did not declare a value never matches it; `unknown` matches only a recording that declared no scope. */
203
+ readonly scope?: readonly string[];
198
204
  /** Only tests recorded in this run (a key from `runList`). */
199
205
  readonly run?: string;
200
206
  readonly sort?: TestSort;
@@ -499,7 +505,7 @@ export declare function artifactList(history: TestHistory, version: string, quer
499
505
  }): ArtifactList;
500
506
  /** One run with every result it recorded, or null when no run has that key. */
501
507
  export declare function runDetail(history: TestHistory, version: string, key: string): RunDetail | null;
502
- /** Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`, `artifactTag`, and `inherited=1`. */
508
+ /** Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`, `artifactTag`, `scope` (every token must hold for one recording), and `inherited=1`. */
503
509
  export declare function parseTestQuery(params: URLSearchParams): TestQuery;
504
510
  /** A duration for a person: `850 ms`, `12.3 s`, `4m 05s`. */
505
511
  export declare function formatMs(ms: number | null | undefined): string;
@@ -25,6 +25,7 @@
25
25
  import { displayTag, normalizeTag, normalizeTags } from './artifact-tags.js';
26
26
  import { facetsOf, forRuns, labelList, matchesFilter, outcomeOfCounts, splitList, usedFilters } from './artifact-query.js';
27
27
  import { videoCounts } from './evidence-detail.js';
28
+ import { matchesScope, scopeTokens, UNKNOWN_SCOPE } from './evidence-scope.js';
28
29
  import { checkSlots } from './evidence-view.js';
29
30
  import { combineEvidence, emptyCounts, slotKey, specPath as normalSpecPath } from './layer-merge.js';
30
31
  import { RecordedSpecs } from './recorded-specs.js';
@@ -448,10 +449,16 @@ function summaryOf(slot, history, known) {
448
449
  const labels = {};
449
450
  let attachments = 0, steps = 0, attempts = 0;
450
451
  const roles = new Set();
452
+ const profiles = new Map();
451
453
  for (const [at, outcome] of outcomes.entries()) {
452
454
  const evidence = outcome.evidence;
453
455
  if (!evidence)
454
456
  continue;
457
+ for (const video of evidence.videos ?? [])
458
+ if (video.available) {
459
+ const tokens = scopeTokens(video.scope);
460
+ profiles.set(tokens.join('\n'), tokens);
461
+ }
455
462
  if (evidence.attachments > 0)
456
463
  for (const tag of normalizeTags([...evidence.tags, ...(slot.outcomes[at]?.layer.evidence?.tags ?? [])]))
457
464
  inheritedTags.add(tag);
@@ -481,6 +488,7 @@ function summaryOf(slot, history, known) {
481
488
  tags: tagList,
482
489
  ...(artifactTags.size ? { artifactTags: [...artifactTags].sort() } : {}),
483
490
  ...([...inheritedTags].some(tag => !artifactTags.has(tag)) ? { inheritedArtifactTags: [...inheritedTags].filter(tag => !artifactTags.has(tag)).sort() } : {}),
491
+ ...(profiles.size ? { scopeProfiles: [...profiles.values()] } : {}),
484
492
  labels,
485
493
  evidence: { attachments, steps, attempts, roles: [...roles].sort() },
486
494
  lastRecordedAtMs: slot.latestAtMs,
@@ -490,7 +498,7 @@ function summaryOf(slot, history, known) {
490
498
  /** The lowercase text a search matches a test against: title, id, spec, domain, tags, labels, environments and producers. */
491
499
  export function haystackOf(test) {
492
500
  return [
493
- test.title, test.id, test.specPath ?? '', test.spec?.title ?? '', test.spec?.domain ?? '', ...test.tags, ...(test.artifactTags ?? []).map(displayTag),
501
+ test.title, test.id, test.specPath ?? '', test.spec?.title ?? '', test.spec?.domain ?? '', ...test.tags, ...(test.artifactTags ?? []).map(displayTag), ...(test.scopeProfiles ?? []).flat().filter(token => token !== UNKNOWN_SCOPE),
494
502
  ...Object.entries(test.labels).flatMap(([key, value]) => [key, value]), ...test.environments, ...test.producers,
495
503
  ].join('\n').toLowerCase();
496
504
  }
@@ -597,6 +605,7 @@ export function catalogModel(layers, sources = {}) {
597
605
  specs: [...specGroups].map(([, list]) => { const g = group(list[0].spec.path, list); return { ...list[0].spec, tests: g.tests, counts: g.counts }; }).sort((a, b) => compareText(a.domain, b.domain) || compareText(a.title, b.title)),
598
606
  tags: facet(tests, test => test.tags),
599
607
  artifactTags: facet(tests, test => test.artifactTags ?? []),
608
+ scopes: facet(tests, test => (test.scopeProfiles ?? []).flat()),
600
609
  branches: facet(tests, test => test.stats.branches),
601
610
  labels: Object.fromEntries([...labelKeys].sort().map(key => [key, facet(tests, test => (test.labels[key] === undefined ? [] : [test.labels[key]]))])),
602
611
  };
@@ -633,6 +642,8 @@ export function matchesQuery(model, test, query, inRun) {
633
642
  return false;
634
643
  if (query.artifactTag?.length && !query.artifactTag.some(tag => { const wanted = normalizeTag(tag); return wanted !== null && ((test.artifactTags?.includes(wanted) ?? false) || (query.inherited === true && (test.inheritedArtifactTags?.includes(wanted) ?? false))); }))
635
644
  return false;
645
+ if (query.scope?.length && !(test.scopeProfiles ?? []).some(tokens => matchesScope(tokens, query.scope)))
646
+ return false;
636
647
  if (inRun && !inRun.has(test.key))
637
648
  return false;
638
649
  return true;
@@ -725,7 +736,7 @@ export function runDetail(history, version, key) {
725
736
  const found = history.run(key);
726
737
  return found ? { schemaVersion: TEST_CATALOG_VERSION, version, ...found } : null;
727
738
  }
728
- /** Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`, `artifactTag`, and `inherited=1`. */
739
+ /** Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`, `artifactTag`, `scope` (every token must hold for one recording), and `inherited=1`. */
729
740
  export function parseTestQuery(params) {
730
741
  const many = (name) => params.getAll(name).flatMap(splitList);
731
742
  const number = (name) => { const value = params.get(name); return value !== null && /^\d+$/.test(value) ? Number(value) : undefined; };
@@ -734,7 +745,7 @@ export function parseTestQuery(params) {
734
745
  const q = params.get('q');
735
746
  if (q)
736
747
  query.q = q;
737
- for (const name of ['status', 'environment', 'producer', 'domain', 'spec', 'tag', 'artifactTag']) {
748
+ for (const name of ['status', 'environment', 'producer', 'domain', 'spec', 'tag', 'artifactTag', 'scope']) {
738
749
  const values = many(name);
739
750
  if (values.length)
740
751
  query[name] = values;
@@ -815,6 +826,7 @@ export function historyMarkdown(document) {
815
826
  `- Duration: ${d ? `avg ${formatMs(d.avgMs)}, p50 ${formatMs(d.p50Ms)}, p95 ${formatMs(d.p95Ms)}, min ${formatMs(d.minMs)}, max ${formatMs(d.maxMs)}, last ${formatMs(d.lastMs)}` : 'not measured'}`,
816
827
  ...(test.tags.length ? [`- Tags: ${test.tags.join(', ')}`] : []),
817
828
  ...(test.artifactTags?.length ? [`- Artifact tags: ${test.artifactTags.map(displayTag).join(', ')}`] : []),
829
+ ...(test.scopeProfiles?.length ? [`- Recording scope: ${test.scopeProfiles.map(tokens => tokens.join(' ')).join('; ')}`] : []),
818
830
  ...(Object.keys(test.labels).length ? [`- Labels: ${Object.entries(test.labels).map(([k, v]) => `${k}=${v}`).join(', ')}`] : []),
819
831
  `- Evidence: ${test.evidence.attachments} attachments${test.evidence.roles.length ? ` (${test.evidence.roles.join(', ')})` : ''}, ${test.evidence.steps} steps`,
820
832
  '',