@crvouga/atlas 0.14.0 → 0.15.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 (39) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/reports/src/artifact-tag-store.d.ts +20 -0
  3. package/dist/reports/src/artifact-tag-store.js +70 -0
  4. package/dist/reports/src/artifact-tags.d.ts +96 -0
  5. package/dist/reports/src/artifact-tags.js +96 -0
  6. package/dist/reports/src/evidence-detail.d.ts +1 -1
  7. package/dist/reports/src/evidence-detail.js +6 -2
  8. package/dist/reports/src/evidence-types.d.ts +2 -0
  9. package/dist/reports/src/reports-explorer-route.d.ts +8 -0
  10. package/dist/reports/src/reports-route.d.ts +8 -0
  11. package/dist/reports/src/test-catalog.d.ts +11 -23
  12. package/dist/reports/src/test-catalog.js +42 -4
  13. package/dist/schema/src/api.d.ts +79 -0
  14. package/dist/schema/src/api.js +31 -1
  15. package/dist/schema/src/layer.d.ts +33 -0
  16. package/dist/schema/src/layer.js +11 -1
  17. package/dist/server/src/app.js +32 -1
  18. package/dist/server/src/openapi.d.ts +1 -1
  19. package/dist/server/src/openapi.js +18 -2
  20. package/docs/extending.md +3 -0
  21. package/docs/reference/http-api.md +48 -0
  22. package/docs/reference/reports.md +217 -2
  23. package/docs/reference/schema.md +32 -0
  24. package/docs/rich-evidence.md +28 -0
  25. package/docs/server.md +24 -0
  26. package/llms-full.txt +103 -0
  27. package/package.json +1 -1
  28. package/packages/schema/schemas/check-detail.schema.json +28 -0
  29. package/packages/schema/schemas/effective-delta.schema.json +14 -0
  30. package/packages/schema/schemas/effective-evidence.schema.json +14 -0
  31. package/packages/schema/schemas/evidence-page.schema.json +7 -0
  32. package/packages/schema/schemas/layer-rows.schema.json +7 -0
  33. package/packages/schema/schemas/layer.schema.json +70 -0
  34. package/packages/schema/schemas/publish-checks.schema.json +35 -0
  35. package/packages/schema/schemas/test-catalog.schema.json +42 -0
  36. package/packages/schema/schemas/test-history.schema.json +28 -0
  37. package/packages/server/openapi.json +732 -1
  38. package/packages/visualizer/src/evidence/artifact-tags.tsx +285 -0
  39. package/packages/visualizer/src/evidence/artifacts.tsx +18 -12
@@ -1,3 +1,26 @@
1
+ /**
2
+ * @module Test catalog, history and runs
3
+ *
4
+ * What a person or an agent asks of a report once it holds more than a handful of checks: every test with where it ran,
5
+ * how it went over time and how long it takes; the specs, domains, environments and producers they fall under; the CI
6
+ * runs that produced them and which tests each one ran.
7
+ *
8
+ * - `TestHistory` keeps every recorded result of every test (bounded per test) and every run's results, folded in one
9
+ * layer at a time, in any order, with the same outcome. It never decides a status: the current status of a test is
10
+ * always the report's winner (`combineEvidence`), and history only describes how it got there.
11
+ * - `testCatalog` turns the report's winners (plus a history, when there is one) into a `TestCatalog`: a summary with
12
+ * insights (slowest, flaky, failing), facets to filter by, and the tests that match a `TestQuery`.
13
+ * - `testHistory`, `runList` and `runDetail` answer for one test, every run, and one run.
14
+ * - `catalogMarkdown`, `historyMarkdown`, `runsMarkdown` and `runMarkdown` say the same in Markdown, for an agent or a
15
+ * terminal.
16
+ *
17
+ * A test is a check identity (its spec path and id) regardless of producer, environment and parameters; each of those
18
+ * combinations is one of its results. A result that is not `passed` is never counted as a pass, and a test with no
19
+ * completed result has no pass rate.
20
+ *
21
+ * @see docs/server.md#reading-the-report-as-an-agent
22
+ */
23
+ import { displayTag, normalizeTag, normalizeTags } from './artifact-tags.js';
1
24
  import { checkSlots } from './evidence-view.js';
2
25
  import { combineEvidence, emptyCounts, slotKey, specPath as normalSpecPath } from './layer-merge.js';
3
26
  import { RecordedSpecs } from './recorded-specs.js';
@@ -337,15 +360,22 @@ function outcomeOf(result) {
337
360
  function summaryOf(slot, history, known) {
338
361
  const outcomes = slot.outcomes.map(outcomeOf);
339
362
  const tags = new Set();
363
+ const artifactTags = new Set();
364
+ const inheritedTags = new Set();
340
365
  const labels = {};
341
366
  let attachments = 0, steps = 0, attempts = 0;
342
367
  const roles = new Set();
343
- for (const outcome of outcomes) {
368
+ for (const [at, outcome] of outcomes.entries()) {
344
369
  const evidence = outcome.evidence;
345
370
  if (!evidence)
346
371
  continue;
372
+ if (evidence.attachments > 0)
373
+ for (const tag of normalizeTags([...evidence.tags, ...(slot.outcomes[at]?.layer.evidence?.tags ?? [])]))
374
+ inheritedTags.add(tag);
347
375
  for (const tag of evidence.tags)
348
376
  tags.add(tag);
377
+ for (const tag of evidence.artifactTags ?? [])
378
+ artifactTags.add(tag);
349
379
  Object.assign(labels, evidence.labels);
350
380
  attachments += evidence.attachments;
351
381
  steps += evidence.steps;
@@ -366,6 +396,8 @@ function summaryOf(slot, history, known) {
366
396
  outcomes,
367
397
  stats: statsOf(history?.of(slot.key) ?? [], outcomes, tagList, labels),
368
398
  tags: tagList,
399
+ ...(artifactTags.size ? { artifactTags: [...artifactTags].sort() } : {}),
400
+ ...([...inheritedTags].some(tag => !artifactTags.has(tag)) ? { inheritedArtifactTags: [...inheritedTags].filter(tag => !artifactTags.has(tag)).sort() } : {}),
369
401
  labels,
370
402
  evidence: { attachments, steps, attempts, roles: [...roles].sort() },
371
403
  lastRecordedAtMs: slot.latestAtMs,
@@ -375,7 +407,7 @@ function summaryOf(slot, history, known) {
375
407
  /** The lowercase text a search matches a test against: title, id, spec, domain, tags, labels, environments and producers. */
376
408
  export function haystackOf(test) {
377
409
  return [
378
- test.title, test.id, test.specPath ?? '', test.spec?.title ?? '', test.spec?.domain ?? '', ...test.tags,
410
+ test.title, test.id, test.specPath ?? '', test.spec?.title ?? '', test.spec?.domain ?? '', ...test.tags, ...(test.artifactTags ?? []).map(displayTag),
379
411
  ...Object.entries(test.labels).flatMap(([key, value]) => [key, value]), ...test.environments, ...test.producers,
380
412
  ].join('\n').toLowerCase();
381
413
  }
@@ -481,6 +513,7 @@ export function catalogModel(layers, sources = {}) {
481
513
  domains: facet(tests, test => [test.spec?.domain || '(no spec)']),
482
514
  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)),
483
515
  tags: facet(tests, test => test.tags),
516
+ artifactTags: facet(tests, test => test.artifactTags ?? []),
484
517
  branches: facet(tests, test => test.stats.branches),
485
518
  labels: Object.fromEntries([...labelKeys].sort().map(key => [key, facet(tests, test => (test.labels[key] === undefined ? [] : [test.labels[key]]))])),
486
519
  };
@@ -515,6 +548,8 @@ export function matchesQuery(model, test, query, inRun) {
515
548
  return false;
516
549
  if (query.tag?.length && !query.tag.some(tag => test.tags.includes(tag)))
517
550
  return false;
551
+ 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))); }))
552
+ return false;
518
553
  if (inRun && !inRun.has(test.key))
519
554
  return false;
520
555
  return true;
@@ -546,7 +581,7 @@ export function runDetail(history, version, key) {
546
581
  const found = history.run(key);
547
582
  return found ? { schemaVersion: TEST_CATALOG_VERSION, version, ...found } : null;
548
583
  }
549
- /** Reads a `TestQuery` from URL search parameters: `q`, `sort`, `offset`, `limit`, `run`, and repeatable or comma-separated `status`, `environment`, `producer`, `domain`, `spec`, `tag`. */
584
+ /** 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`. */
550
585
  export function parseTestQuery(params) {
551
586
  const many = (name) => params.getAll(name).flatMap(value => value.split(',')).map(value => value.trim()).filter(Boolean);
552
587
  const number = (name) => { const value = params.get(name); return value !== null && /^\d+$/.test(value) ? Number(value) : undefined; };
@@ -555,11 +590,13 @@ export function parseTestQuery(params) {
555
590
  const q = params.get('q');
556
591
  if (q)
557
592
  query.q = q;
558
- for (const name of ['status', 'environment', 'producer', 'domain', 'spec', 'tag']) {
593
+ for (const name of ['status', 'environment', 'producer', 'domain', 'spec', 'tag', 'artifactTag']) {
559
594
  const values = many(name);
560
595
  if (values.length)
561
596
  query[name] = values;
562
597
  }
598
+ if (['1', 'true'].includes(params.get('inherited') ?? ''))
599
+ query.inherited = true;
563
600
  const run = params.get('run');
564
601
  if (run)
565
602
  query.run = run;
@@ -633,6 +670,7 @@ export function historyMarkdown(document) {
633
670
  `- Results: ${test.stats.results} recorded, ${test.stats.passed} passed, ${test.stats.failed} failed${test.stats.passRate === null ? '' : `, pass rate ${Math.round(test.stats.passRate * 100)}%`}, in ${test.stats.runs} runs`,
634
671
  `- 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'}`,
635
672
  ...(test.tags.length ? [`- Tags: ${test.tags.join(', ')}`] : []),
673
+ ...(test.artifactTags?.length ? [`- Artifact tags: ${test.artifactTags.map(displayTag).join(', ')}`] : []),
636
674
  ...(Object.keys(test.labels).length ? [`- Labels: ${Object.entries(test.labels).map(([k, v]) => `${k}=${v}`).join(', ')}`] : []),
637
675
  `- Evidence: ${test.evidence.attachments} attachments${test.evidence.roles.length ? ` (${test.evidence.roles.join(', ')})` : ''}, ${test.evidence.steps} steps`,
638
676
  '',
@@ -88,6 +88,7 @@ export declare const EvidenceSummarySchema: z.ZodObject<{
88
88
  attempts: z.ZodNumber;
89
89
  links: z.ZodNumber;
90
90
  tags: z.ZodArray<z.ZodString>;
91
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
91
92
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
92
93
  roles: z.ZodArray<z.ZodString>;
93
94
  }, z.core.$loose>;
@@ -129,6 +130,7 @@ export declare const LayerHeaderSchema: z.ZodObject<{
129
130
  attempts: z.ZodNumber;
130
131
  links: z.ZodNumber;
131
132
  tags: z.ZodArray<z.ZodString>;
133
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
132
134
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
133
135
  roles: z.ZodArray<z.ZodString>;
134
136
  }, z.core.$loose>>;
@@ -162,6 +164,7 @@ export declare const LayerRowSchema: z.ZodObject<{
162
164
  attempts: z.ZodNumber;
163
165
  links: z.ZodNumber;
164
166
  tags: z.ZodArray<z.ZodString>;
167
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
165
168
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
166
169
  roles: z.ZodArray<z.ZodString>;
167
170
  }, z.core.$loose>>;
@@ -197,6 +200,7 @@ export declare const LayerRowsSchema: z.ZodObject<{
197
200
  attempts: z.ZodNumber;
198
201
  links: z.ZodNumber;
199
202
  tags: z.ZodArray<z.ZodString>;
203
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
200
204
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
201
205
  roles: z.ZodArray<z.ZodString>;
202
206
  }, z.core.$loose>>;
@@ -234,6 +238,7 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
234
238
  attempts: z.ZodNumber;
235
239
  links: z.ZodNumber;
236
240
  tags: z.ZodArray<z.ZodString>;
241
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
237
242
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
238
243
  roles: z.ZodArray<z.ZodString>;
239
244
  }, z.core.$loose>>;
@@ -276,6 +281,7 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
276
281
  attempts: z.ZodNumber;
277
282
  links: z.ZodNumber;
278
283
  tags: z.ZodArray<z.ZodString>;
284
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
279
285
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
280
286
  roles: z.ZodArray<z.ZodString>;
281
287
  }, z.core.$loose>>;
@@ -318,6 +324,7 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
318
324
  attempts: z.ZodNumber;
319
325
  links: z.ZodNumber;
320
326
  tags: z.ZodArray<z.ZodString>;
327
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
321
328
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
322
329
  roles: z.ZodArray<z.ZodString>;
323
330
  }, z.core.$loose>>;
@@ -360,6 +367,7 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
360
367
  attempts: z.ZodNumber;
361
368
  links: z.ZodNumber;
362
369
  tags: z.ZodArray<z.ZodString>;
370
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
363
371
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
364
372
  roles: z.ZodArray<z.ZodString>;
365
373
  }, z.core.$loose>>;
@@ -423,6 +431,7 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
423
431
  attempts: z.ZodNumber;
424
432
  links: z.ZodNumber;
425
433
  tags: z.ZodArray<z.ZodString>;
434
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
426
435
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
427
436
  roles: z.ZodArray<z.ZodString>;
428
437
  }, z.core.$loose>>;
@@ -465,6 +474,7 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
465
474
  attempts: z.ZodNumber;
466
475
  links: z.ZodNumber;
467
476
  tags: z.ZodArray<z.ZodString>;
477
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
468
478
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
469
479
  roles: z.ZodArray<z.ZodString>;
470
480
  }, z.core.$loose>>;
@@ -531,6 +541,7 @@ export declare const EvidencePageSchema: z.ZodObject<{
531
541
  attempts: z.ZodNumber;
532
542
  links: z.ZodNumber;
533
543
  tags: z.ZodArray<z.ZodString>;
544
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
534
545
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
535
546
  roles: z.ZodArray<z.ZodString>;
536
547
  }, z.core.$loose>>;
@@ -573,6 +584,7 @@ export declare const CheckDetailSchema: z.ZodObject<{
573
584
  height: z.ZodOptional<z.ZodNumber>;
574
585
  durationMs: z.ZodOptional<z.ZodNumber>;
575
586
  description: z.ZodOptional<z.ZodString>;
587
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
576
588
  at: z.ZodOptional<z.ZodObject<{
577
589
  step: z.ZodOptional<z.ZodString>;
578
590
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -637,6 +649,7 @@ export declare const CheckDetailSchema: z.ZodObject<{
637
649
  height: z.ZodOptional<z.ZodNumber>;
638
650
  durationMs: z.ZodOptional<z.ZodNumber>;
639
651
  description: z.ZodOptional<z.ZodString>;
652
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
640
653
  at: z.ZodOptional<z.ZodObject<{
641
654
  step: z.ZodOptional<z.ZodString>;
642
655
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -681,6 +694,7 @@ export declare const CheckDetailSchema: z.ZodObject<{
681
694
  height: z.ZodOptional<z.ZodNumber>;
682
695
  durationMs: z.ZodOptional<z.ZodNumber>;
683
696
  description: z.ZodOptional<z.ZodString>;
697
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
684
698
  at: z.ZodOptional<z.ZodObject<{
685
699
  step: z.ZodOptional<z.ZodString>;
686
700
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -732,6 +746,7 @@ export declare const CheckDetailSchema: z.ZodObject<{
732
746
  height: z.ZodOptional<z.ZodNumber>;
733
747
  durationMs: z.ZodOptional<z.ZodNumber>;
734
748
  description: z.ZodOptional<z.ZodString>;
749
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
735
750
  at: z.ZodOptional<z.ZodObject<{
736
751
  step: z.ZodOptional<z.ZodString>;
737
752
  offsetMs: z.ZodOptional<z.ZodNumber>;
@@ -799,6 +814,55 @@ export declare const ChecksResultSchema: z.ZodObject<{
799
814
  id: z.ZodString;
800
815
  checks: z.ZodNumber;
801
816
  }, z.core.$loose>;
817
+ /** Which attachment a reviewer's tags belong to: one attachment of one check of one archived layer. */
818
+ export declare const ArtifactTargetSchema: z.ZodObject<{
819
+ layer: z.ZodString;
820
+ check: z.ZodString;
821
+ kind: z.ZodOptional<z.ZodString>;
822
+ artifact: z.ZodString;
823
+ }, z.core.$loose>;
824
+ /** The body of `POST /api/reports/artifact-tags`: tags to add to and to remove from one attachment. At least one tag overall. */
825
+ export declare const ArtifactTagEditSchema: z.ZodObject<{
826
+ target: z.ZodObject<{
827
+ layer: z.ZodString;
828
+ check: z.ZodString;
829
+ kind: z.ZodOptional<z.ZodString>;
830
+ artifact: z.ZodString;
831
+ }, z.core.$loose>;
832
+ add: z.ZodOptional<z.ZodArray<z.ZodString>>;
833
+ remove: z.ZodOptional<z.ZodArray<z.ZodString>>;
834
+ }, z.core.$loose>;
835
+ /** One recorded edit of an attachment's reviewer tags. Entries are never changed or removed: they are the audit trail. */
836
+ export declare const ArtifactTagEntrySchema: z.ZodObject<{
837
+ id: z.ZodString;
838
+ target: z.ZodObject<{
839
+ layer: z.ZodString;
840
+ check: z.ZodString;
841
+ kind: z.ZodOptional<z.ZodString>;
842
+ artifact: z.ZodString;
843
+ }, z.core.$loose>;
844
+ add: z.ZodArray<z.ZodString>;
845
+ remove: z.ZodArray<z.ZodString>;
846
+ by: z.ZodString;
847
+ atMs: z.ZodNumber;
848
+ }, z.core.$loose>;
849
+ /** `GET /api/reports/artifact-tags`: every recorded edit, oldest first. Fold them with `foldArtifactTags`. */
850
+ export declare const ArtifactTagsSchema: z.ZodObject<{
851
+ schemaVersion: z.ZodLiteral<1>;
852
+ entries: z.ZodArray<z.ZodObject<{
853
+ id: z.ZodString;
854
+ target: z.ZodObject<{
855
+ layer: z.ZodString;
856
+ check: z.ZodString;
857
+ kind: z.ZodOptional<z.ZodString>;
858
+ artifact: z.ZodString;
859
+ }, z.core.$loose>;
860
+ add: z.ZodArray<z.ZodString>;
861
+ remove: z.ZodArray<z.ZodString>;
862
+ by: z.ZodString;
863
+ atMs: z.ZodNumber;
864
+ }, z.core.$loose>>;
865
+ }, z.core.$loose>;
802
866
  /** The data of the `hello` event: sent first on `/api/events`. */
803
867
  export declare const HelloEventSchema: z.ZodObject<{
804
868
  version: z.ZodString;
@@ -856,6 +920,7 @@ export declare const TestResultEntrySchema: z.ZodObject<{
856
920
  attempts: z.ZodNumber;
857
921
  links: z.ZodNumber;
858
922
  tags: z.ZodArray<z.ZodString>;
923
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
859
924
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
860
925
  roles: z.ZodArray<z.ZodString>;
861
926
  }, z.core.$loose>>;
@@ -907,6 +972,7 @@ export declare const TestSummarySchema: z.ZodObject<{
907
972
  attempts: z.ZodNumber;
908
973
  links: z.ZodNumber;
909
974
  tags: z.ZodArray<z.ZodString>;
975
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
910
976
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
911
977
  roles: z.ZodArray<z.ZodString>;
912
978
  }, z.core.$loose>>;
@@ -937,6 +1003,8 @@ export declare const TestSummarySchema: z.ZodObject<{
937
1003
  branches: z.ZodArray<z.ZodString>;
938
1004
  }, z.core.$loose>;
939
1005
  tags: z.ZodArray<z.ZodString>;
1006
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1007
+ inheritedArtifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
940
1008
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
941
1009
  evidence: z.ZodObject<{
942
1010
  attachments: z.ZodNumber;
@@ -1084,6 +1152,10 @@ export declare const TestCatalogSchema: z.ZodObject<{
1084
1152
  value: z.ZodString;
1085
1153
  count: z.ZodNumber;
1086
1154
  }, z.core.$loose>>;
1155
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodObject<{
1156
+ value: z.ZodString;
1157
+ count: z.ZodNumber;
1158
+ }, z.core.$loose>>>;
1087
1159
  branches: z.ZodArray<z.ZodObject<{
1088
1160
  value: z.ZodString;
1089
1161
  count: z.ZodNumber;
@@ -1142,6 +1214,7 @@ export declare const TestCatalogSchema: z.ZodObject<{
1142
1214
  attempts: z.ZodNumber;
1143
1215
  links: z.ZodNumber;
1144
1216
  tags: z.ZodArray<z.ZodString>;
1217
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1145
1218
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
1146
1219
  roles: z.ZodArray<z.ZodString>;
1147
1220
  }, z.core.$loose>>;
@@ -1172,6 +1245,8 @@ export declare const TestCatalogSchema: z.ZodObject<{
1172
1245
  branches: z.ZodArray<z.ZodString>;
1173
1246
  }, z.core.$loose>;
1174
1247
  tags: z.ZodArray<z.ZodString>;
1248
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1249
+ inheritedArtifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1175
1250
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
1176
1251
  evidence: z.ZodObject<{
1177
1252
  attachments: z.ZodNumber;
@@ -1312,6 +1387,7 @@ export declare const TestHistorySchema: z.ZodObject<{
1312
1387
  attempts: z.ZodNumber;
1313
1388
  links: z.ZodNumber;
1314
1389
  tags: z.ZodArray<z.ZodString>;
1390
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1315
1391
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
1316
1392
  roles: z.ZodArray<z.ZodString>;
1317
1393
  }, z.core.$loose>>;
@@ -1342,6 +1418,8 @@ export declare const TestHistorySchema: z.ZodObject<{
1342
1418
  branches: z.ZodArray<z.ZodString>;
1343
1419
  }, z.core.$loose>;
1344
1420
  tags: z.ZodArray<z.ZodString>;
1421
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1422
+ inheritedArtifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1345
1423
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
1346
1424
  evidence: z.ZodObject<{
1347
1425
  attachments: z.ZodNumber;
@@ -1379,6 +1457,7 @@ export declare const TestHistorySchema: z.ZodObject<{
1379
1457
  attempts: z.ZodNumber;
1380
1458
  links: z.ZodNumber;
1381
1459
  tags: z.ZodArray<z.ZodString>;
1460
+ artifactTags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1382
1461
  labels: z.ZodRecord<z.ZodString, z.ZodString>;
1383
1462
  roles: z.ZodArray<z.ZodString>;
1384
1463
  }, z.core.$loose>>;
@@ -70,6 +70,7 @@ export const EvidenceSummarySchema = z.looseObject({
70
70
  attempts: z.number().int().nonnegative(),
71
71
  links: z.number().int().nonnegative(),
72
72
  tags: z.array(z.string()),
73
+ artifactTags: z.array(z.string()).optional().describe('Normalized tags of the attachments themselves, sorted and distinct. Absent when no attachment is tagged; never inherited from a check or run'),
73
74
  labels: z.record(z.string(), z.string()),
74
75
  roles: z.array(z.string())
75
76
  });
@@ -186,6 +187,33 @@ export const ChecksResultSchema = z.looseObject({
186
187
  id: z.string().describe('The layer id'),
187
188
  checks: z.number().int().nonnegative()
188
189
  });
190
+ /** Which attachment a reviewer's tags belong to: one attachment of one check of one archived layer. */
191
+ export const ArtifactTargetSchema = z.looseObject({
192
+ layer: z.string().regex(/^[0-9a-f]{64}$/).describe('The id of the archived layer that holds the check'),
193
+ check: z.string().min(1).describe('The check\'s id'),
194
+ kind: z.string().min(1).optional().describe('The kind of the part holding the check, when the same id exists in several parts'),
195
+ artifact: z.string().min(1).max(1024).describe('The attachment: `id:<id>` when it has an id, else `key:<bundleKey>`, else `name:<name>` (see `artifactRef`)')
196
+ });
197
+ /** The body of `POST /api/reports/artifact-tags`: tags to add to and to remove from one attachment. At least one tag overall. */
198
+ export const ArtifactTagEditSchema = z.looseObject({
199
+ target: ArtifactTargetSchema,
200
+ add: z.array(z.string()).max(20).optional().describe('Tags to add. `demo` and `#demo` are the same tag'),
201
+ remove: z.array(z.string()).max(20).optional().describe('Reviewer tags to remove. Tags the producer wrote are part of the immutable layer and stay')
202
+ });
203
+ /** One recorded edit of an attachment's reviewer tags. Entries are never changed or removed: they are the audit trail. */
204
+ export const ArtifactTagEntrySchema = z.looseObject({
205
+ id: z.string().describe('Unique and time-ordered'),
206
+ target: ArtifactTargetSchema,
207
+ add: z.array(z.string()).describe('Normalized tags added'),
208
+ remove: z.array(z.string()).describe('Normalized tags removed'),
209
+ by: z.string().describe('The name of the token that made the edit (never the token)'),
210
+ atMs: z.number().describe('When the server recorded it, epoch milliseconds')
211
+ });
212
+ /** `GET /api/reports/artifact-tags`: every recorded edit, oldest first. Fold them with `foldArtifactTags`. */
213
+ export const ArtifactTagsSchema = z.looseObject({
214
+ schemaVersion: z.literal(1),
215
+ entries: z.array(ArtifactTagEntrySchema)
216
+ });
189
217
  /** The data of the `hello` event: sent first on `/api/events`. */
190
218
  export const HelloEventSchema = z.looseObject({ version: z.string(), ready: z.boolean(), time: z.number(), heartbeatMs: z.number().positive().optional() });
191
219
  /** The data of the `report` event: layers arrived. Enough to say who sent what, without asking. */
@@ -243,6 +271,8 @@ export const TestSummarySchema = z.looseObject({
243
271
  runs: z.number().int().nonnegative(), branches: z.array(z.string())
244
272
  }),
245
273
  tags: z.array(z.string()),
274
+ artifactTags: z.array(z.string()).optional().describe('Normalized tags of the attachments of its current results, such as `demo` (shown `#demo`). Not inherited from a check or run, unlike `tags`'),
275
+ inheritedArtifactTags: z.array(z.string()).optional().describe('Normalized tags its attachments inherit from their check and run, apart from `artifactTags`: what `?artifactTag=…&inherited=1` also matches'),
246
276
  labels: z.record(z.string(), z.string()),
247
277
  evidence: z.looseObject({ attachments: z.number().int().nonnegative(), steps: z.number().int().nonnegative(), attempts: z.number().int().nonnegative(), roles: z.array(z.string()) }),
248
278
  lastRecordedAtMs: z.number(),
@@ -269,7 +299,7 @@ export const TestCatalogSchema = z.looseObject({
269
299
  facets: z.looseObject({
270
300
  statuses: z.array(FacetValueSchema), environments: z.array(FacetValueSchema), producers: z.array(FacetValueSchema), domains: z.array(FacetValueSchema),
271
301
  specs: z.array(CatalogSpecSchema.extend({ tests: z.number().int().nonnegative(), counts: StatusCountsSchema })),
272
- tags: z.array(FacetValueSchema), branches: z.array(FacetValueSchema), labels: z.record(z.string(), z.array(FacetValueSchema))
302
+ tags: z.array(FacetValueSchema), artifactTags: z.array(FacetValueSchema).optional(), branches: z.array(FacetValueSchema), labels: z.record(z.string(), z.array(FacetValueSchema))
273
303
  }),
274
304
  total: z.number().int().nonnegative().describe('Tests the query matched, before offset and limit'),
275
305
  offset: z.number().int().nonnegative(),