@crvouga/atlas 0.13.0 → 0.14.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 (35) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/playwright-reporter/src/index.d.ts +11 -0
  3. package/dist/playwright-reporter/src/index.js +4 -2
  4. package/dist/reports/src/evidence-types.d.ts +3 -0
  5. package/dist/reports/src/layer-merge.d.ts +1 -1
  6. package/dist/reports/src/layer-merge.js +3 -2
  7. package/dist/reports/src/layer-types.d.ts +3 -0
  8. package/dist/reports/src/publish-checks.js +1 -0
  9. package/dist/reports/src/report-cli.d.ts +1 -1
  10. package/dist/reports/src/report-cli.js +30 -2
  11. package/dist/reports/src/reporter.d.ts +1 -1
  12. package/dist/reports/src/test-catalog.d.ts +20 -4
  13. package/dist/reports/src/test-catalog.js +31 -8
  14. package/dist/schema/src/api.d.ts +49 -1
  15. package/dist/schema/src/api.js +7 -3
  16. package/dist/schema/src/layer.d.ts +26 -0
  17. package/dist/schema/src/layer.js +17 -0
  18. package/docs/integrating.md +8 -0
  19. package/docs/reference/cli.md +7 -0
  20. package/docs/reference/playwright-reporter.md +6 -6
  21. package/docs/reference/reports.md +18 -6
  22. package/docs/reference/schema.md +20 -1
  23. package/docs/report-contract.md +1 -0
  24. package/llms-full.txt +16 -0
  25. package/package.json +1 -1
  26. package/packages/schema/schemas/effective-delta.schema.json +30 -0
  27. package/packages/schema/schemas/effective-evidence.schema.json +30 -0
  28. package/packages/schema/schemas/evidence-page.schema.json +30 -0
  29. package/packages/schema/schemas/layer.schema.json +30 -0
  30. package/packages/schema/schemas/publish-checks.schema.json +30 -0
  31. package/packages/schema/schemas/report-run.schema.json +17 -1
  32. package/packages/schema/schemas/report-runs.schema.json +17 -1
  33. package/packages/server/openapi.json +154 -2
  34. package/packages/visualizer/src/explorer/RunsView.tsx +23 -4
  35. package/packages/visualizer/src/explorer/parts.tsx +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog: @crvouga/atlas
2
2
 
3
+ ## 0.14.0 (2026-10-28)
4
+
5
+ ### Features
6
+
7
+ - identify local demo executions separately from branch and commit ([543bf9f](https://github.com/crvouga/atlas/commit/543bf9f00bacb3deb5d32d78c3b61437e04497df))
8
+
3
9
  ## 0.13.0 (2026-10-28)
4
10
 
5
11
  ### Features
@@ -35,6 +35,17 @@ export type AtlasPlaywrightReporterOptions = {
35
35
  readonly branch?: string;
36
36
  /** Where to read the CI run. Default the GitHub Actions run (or the CI's own run URL variable). */
37
37
  readonly runUrl?: string;
38
+ /**
39
+ * Names this execution, for a local or manual run with no CI run URL: every shard and project that gets the same `id`
40
+ * is one run, and the next invocation gets another. Default: `ATLAS_RUN_ID` (and `ATLAS_RUN_TITLE`) when set. Only
41
+ * groups and labels runs; it never changes what a check is or which result wins.
42
+ */
43
+ readonly run?: {
44
+ readonly id: string;
45
+ readonly title?: string;
46
+ readonly startedAtMs?: number;
47
+ readonly endedAtMs?: number;
48
+ };
38
49
  /** The spec a test belongs to, relative to the repository. Default: the `.feature` of a playwright-bdd test, else the test file. */
39
50
  readonly specPath?: (test: TestCase) => string | undefined;
40
51
  /** Paths are relative to this directory. Default the git root above Playwright's `rootDir`, else `rootDir`. */
@@ -123,15 +123,16 @@ export default class AtlasPlaywrightReporter {
123
123
  }
124
124
  if (!this.store)
125
125
  return;
126
+ const run = options.run ?? (env.ATLAS_RUN_ID ? { id: env.ATLAS_RUN_ID, ...(env.ATLAS_RUN_TITLE ? { title: env.ATLAS_RUN_TITLE } : {}) } : undefined);
126
127
  try {
127
128
  const explicitSha = options.sha ?? env.ATLAS_SHA;
128
129
  const origin = detectOrigin(env);
129
130
  const sha = explicitSha ?? origin.sha;
130
131
  const freshness = detectFreshness({ sha, ci: origin.ci && !explicitSha });
131
- this.identity = { sha, branch: options.branch ?? env.ATLAS_BRANCH ?? origin.branch, runUrl: options.runUrl ?? origin.runUrl, ...(freshness ? { freshness } : {}) };
132
+ this.identity = { sha, branch: options.branch ?? env.ATLAS_BRANCH ?? origin.branch, runUrl: options.runUrl ?? origin.runUrl, ...(run ? { run } : {}), ...(freshness ? { freshness } : {}) };
132
133
  }
133
134
  catch {
134
- this.identity = { sha: options.sha ?? env.ATLAS_SHA ?? null, branch: options.branch ?? env.ATLAS_BRANCH ?? null, runUrl: options.runUrl ?? null, freshness: 'unknown' };
135
+ this.identity = { sha: options.sha ?? env.ATLAS_SHA ?? null, branch: options.branch ?? env.ATLAS_BRANCH ?? null, runUrl: options.runUrl ?? null, ...(run ? { run } : {}), freshness: 'unknown' };
135
136
  }
136
137
  }
137
138
  specPathOf(test) {
@@ -195,6 +196,7 @@ export default class AtlasPlaywrightReporter {
195
196
  sha: this.identity.sha,
196
197
  branch: this.identity.branch,
197
198
  runUrl: this.identity.runUrl,
199
+ ...(this.identity.run ? { run: this.identity.run } : {}),
198
200
  ...(this.identity.freshness ? { freshness: this.identity.freshness } : {}),
199
201
  environment,
200
202
  parameters: { ...parameters },
@@ -9,6 +9,7 @@
9
9
  * @see docs/report-contract.md
10
10
  */
11
11
  import type { EvidenceMeasurement, EvidenceStatus } from './explorer-types.js';
12
+ import type { LayerRun } from '../../schema/src/layer.js';
12
13
  import type { LayerParameters } from './layer-types.js';
13
14
  /** Where a recorded test's timeline and captured files are read from. */
14
15
  export type RecordingAddress = {
@@ -77,6 +78,8 @@ export type LayerHeader = {
77
78
  readonly parameters: LayerParameters;
78
79
  /** `modified`: recorded from a checkout with uncommitted changes; `unknown`: origin not captured; absent: clean. Advisory; never changes a status. */
79
80
  readonly freshness?: 'unknown' | 'modified';
81
+ /** The execution the producer named for it (one local or manual invocation), or absent. Grouping and display only; never part of the slot. */
82
+ readonly run?: LayerRun;
80
83
  /** Where this layer's recorded timelines and captured files are read from, or null when it has none. */
81
84
  readonly recording: RecordingAddress | null;
82
85
  /** From a spec run that has not been collected into a layer yet. */
@@ -129,7 +129,7 @@ export declare function distinctEvidence(layers: readonly LayerEvidence[]): Laye
129
129
  /** Drops each mutable layer whose execution is already archived as an immutable one, and keeps one summary per id. */
130
130
  export declare function distinctLayers(layers: readonly LayerSummary[]): LayerSummary[];
131
131
  /**
132
- * A string equal for the mutable and the archived address of one execution: its producer, sha, time and parts.
132
+ * A string equal for the mutable and the archived address of one execution: its producer, sha, time, parts and, when the producer named one, its execution id.
133
133
  * Object key order is ignored at every depth, including check evidence and extensions; array order is preserved.
134
134
  * This is an advisory execution identity, not the content digest that addresses an immutable layer in storage.
135
135
  */
@@ -97,6 +97,7 @@ export function layerEvidence(summary, backend) {
97
97
  const layerSummaryEvidence = summarizeEvidence(summary, ...summary.parts);
98
98
  const layer = {
99
99
  ...(summary.freshness ? { freshness: summary.freshness } : {}),
100
+ ...(summary.run ? { run: summary.run } : {}),
100
101
  id: summary.id,
101
102
  backend,
102
103
  producer: summary.producer,
@@ -270,12 +271,12 @@ export function distinctLayers(layers) {
270
271
  return [...new Map(layers.filter(layer => archived(layer.id) || !kept.has(layerExecutionKey(layer))).map(layer => [layer.id, layer])).values()];
271
272
  }
272
273
  /**
273
- * A string equal for the mutable and the archived address of one execution: its producer, sha, time and parts.
274
+ * A string equal for the mutable and the archived address of one execution: its producer, sha, time, parts and, when the producer named one, its execution id.
274
275
  * Object key order is ignored at every depth, including check evidence and extensions; array order is preserved.
275
276
  * This is an advisory execution identity, not the content digest that addresses an immutable layer in storage.
276
277
  */
277
278
  export function layerExecutionKey(layer) {
278
- return JSON.stringify([layer.producer, layer.sha, layer.recordedAtMs, orderedValue(layer.parts)]);
279
+ return JSON.stringify([layer.producer, layer.sha, layer.recordedAtMs, orderedValue(layer.parts), ...(layer.run ? [layer.run.id] : [])]);
279
280
  }
280
281
  /** Sort JSON objects recursively without changing array order or interpreting extension keys as prototypes. */
281
282
  function orderedValue(value) {
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * @see docs/report-contract.md#the-layer-document
10
10
  */
11
+ import type { LayerRun } from '../../schema/src/layer.js';
11
12
  import type { BundleIndex } from './bundle.js';
12
13
  import type { ReportPart, SpecEntry } from './explorer-types.js';
13
14
  import type { SourceRecord } from './protocol.js';
@@ -33,6 +34,8 @@ export type LayerSummary = LayerExtras & {
33
34
  readonly environment?: string | null;
34
35
  /** Provenance: the CI run that produced the layer, or `null`. Never part of the slot. */
35
36
  readonly runUrl: string | null;
37
+ /** The execution (one invocation of a local or manual run) the layer belongs to, when the producer named one. Grouping and display only; never part of the slot. Absent: the run is told by `runUrl`, else `branch@sha`. */
38
+ readonly run?: LayerRun;
36
39
  /** Free choices that make a result its own slot. The `environment` parameter is read as the environment (see `layerPlace`). */
37
40
  readonly parameters: LayerParameters;
38
41
  /** The specs the layer was recorded against, with fingerprints of their text. */
@@ -103,6 +103,7 @@ export async function publishChecks(store, input, options = {}) {
103
103
  sha: args.sha,
104
104
  branch: args.branch,
105
105
  runUrl: args.runUrl,
106
+ ...(args.run ? { run: args.run } : {}),
106
107
  ...(environment ? { environment } : {}),
107
108
  parameters: Object.fromEntries(Object.entries(parameters).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
108
109
  specs: args.specs,
@@ -13,7 +13,7 @@
13
13
  * @see docs/agents.md
14
14
  */
15
15
  /** The text `atlas report --help` prints: every subcommand, its flags, what each does and the exit codes. Also the source of the command reference in `docs/reference/cli.md`. */
16
- export declare const REPORT_USAGE = "atlas report \u2014 layered evidence for any kind of spec\n\n atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]\n ( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )\n [--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]\n [--spec <path> | --specs <dir>] [--upload-run]\n [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]\n [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...\n [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]\n atlas report stream [--store <dir|s3://\u2026|https://atlas.example.com>] --producer <name> [--environment <name>]\n [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...\n [--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)\n atlas report status [--store <dir|s3://\u2026|id=url>]... [--backends <module>]\n [--specs <dir>] [--format text|json|markdown]\n [--require-environments a,b] [--require-all-specs]\n [--branch <name>]... [--ignore-modified]\n atlas report merge [--store \u2026]... [--backends <module>] --into <dir|s3://\u2026>\n atlas report doctor [--store \u2026]... [--role publisher|reader|both]\n atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]\n\n A store is a directory or s3://bucket/prefix (add ?region=\u2026&endpoint=\u2026 for R2, MinIO and the\n rest). Select a project on an Atlas node with --project <slug> or ATLAS_PROJECT. This scopes specs, reports and recordings together.\n When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set\n AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with `aws sso login`, or use a role.\n\n publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.\n --producer is required, unless the --checks file names one\n --spec <path> every check in the report belongs to this spec (a report per feature file)\n --specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or\n file name the report gives; also publishes the specs themselves, so the\n node can show them without a checkout\n --upload-run with --manifest: also upload the run's screenshots and clips for the viewer\n --attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive\n (screenshots, videos and traces of a single test go in its CTRF or checks file)\n --label k=v a fact about the run, not part of what it checks: build, os, browser\n --link [n=]<url> a link to the run, a dashboard, an issue\n Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own\n folder and each --attach-root, and never from a hidden file or a link out: a report is data from a\n test run, not instructions. One that is missing or not allowed is skipped with a warning.\n From a checkout with uncommitted changes the layer is marked \"modified\".\n stream Send checks as they finish: one JSON object per line on standard input (the fields of one\n check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server\n shows the run while it is still going. A check sent again with the same id (running, then\n passed) is replaced by the later. Ends when the input does; exit 1 if anything could not be sent.\n Point it at an Atlas server with --store https://\u2026 and a token in ATLAS_TOKEN.\n status Combine every layer of every store, and exit 1 unless the report passes.\n --specs <dir> lay the results over these spec files (default: the specs in the store)\n --require-all-specs fail while any spec has no results\n --branch <name> only layers recorded on this branch (repeatable)\n --ignore-modified leave out layers from modified checkouts and unknown origins\n merge Copy the layers of several stores into one.\n doctor Check that a store works: credentials, listing, writing, and what it holds.\n policy Print the least-privilege IAM policy for publishers, readers, or both.\n\n --backends <module> A TypeScript or JavaScript module whose default export is a list of\n { id, label?, store } (or a function returning it): a device lab, anything\n that implements ObjectStore.\n\n --format json (alias --json) publish, stream, merge, doctor and status print one JSON document instead of prose\n (schemas: publish-result, stream-result, merge-result, doctor-result, report-status). A command\n that is wrong is {\"ok\":false,\"error\":{\"message\":\u2026,\"code\":\"usage\"}} on standard error.\n A store may be an Atlas server (https://\u2026): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.\n\n Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.\n";
16
+ export declare const REPORT_USAGE = "atlas report \u2014 layered evidence for any kind of spec\n\n atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]\n ( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )\n [--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]\n [--run-id <id>] [--run-title <t>] [--run-started-at <iso|ms>] [--run-ended-at <iso|ms>]\n [--spec <path> | --specs <dir>] [--upload-run]\n [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]\n [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...\n [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]\n atlas report stream [--store <dir|s3://\u2026|https://atlas.example.com>] --producer <name> [--environment <name>]\n [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...\n [--run-id <id>] [--run-title <t>]\n [--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)\n atlas report status [--store <dir|s3://\u2026|id=url>]... [--backends <module>]\n [--specs <dir>] [--format text|json|markdown]\n [--require-environments a,b] [--require-all-specs]\n [--branch <name>]... [--ignore-modified]\n atlas report merge [--store \u2026]... [--backends <module>] --into <dir|s3://\u2026>\n atlas report doctor [--store \u2026]... [--role publisher|reader|both]\n atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]\n\n A store is a directory or s3://bucket/prefix (add ?region=\u2026&endpoint=\u2026 for R2, MinIO and the\n rest). Select a project on an Atlas node with --project <slug> or ATLAS_PROJECT. This scopes specs, reports and recordings together.\n When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set\n AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with `aws sso login`, or use a role.\n\n publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.\n --producer is required, unless the --checks file names one\n --spec <path> every check in the report belongs to this spec (a report per feature file)\n --specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or\n file name the report gives; also publishes the specs themselves, so the\n node can show them without a checkout\n --run-id <id> name this execution (a local or manual run with no CI run URL; also ATLAS_RUN_ID).\n Layers that share an id are one run, however many shards or batches uploaded them;\n another invocation at the same commit gets another id. Never part of what a check is\n --run-title <t> a human name for the run (also ATLAS_RUN_TITLE); --run-started-at and --run-ended-at\n record when it ran. Without --run-id a run is its CI run URL, else branch@sha\n --upload-run with --manifest: also upload the run's screenshots and clips for the viewer\n --attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive\n (screenshots, videos and traces of a single test go in its CTRF or checks file)\n --label k=v a fact about the run, not part of what it checks: build, os, browser\n --link [n=]<url> a link to the run, a dashboard, an issue\n Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own\n folder and each --attach-root, and never from a hidden file or a link out: a report is data from a\n test run, not instructions. One that is missing or not allowed is skipped with a warning.\n From a checkout with uncommitted changes the layer is marked \"modified\".\n stream Send checks as they finish: one JSON object per line on standard input (the fields of one\n check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server\n shows the run while it is still going. A check sent again with the same id (running, then\n passed) is replaced by the later. Ends when the input does; exit 1 if anything could not be sent.\n Point it at an Atlas server with --store https://\u2026 and a token in ATLAS_TOKEN.\n status Combine every layer of every store, and exit 1 unless the report passes.\n --specs <dir> lay the results over these spec files (default: the specs in the store)\n --require-all-specs fail while any spec has no results\n --branch <name> only layers recorded on this branch (repeatable)\n --ignore-modified leave out layers from modified checkouts and unknown origins\n merge Copy the layers of several stores into one.\n doctor Check that a store works: credentials, listing, writing, and what it holds.\n policy Print the least-privilege IAM policy for publishers, readers, or both.\n\n --backends <module> A TypeScript or JavaScript module whose default export is a list of\n { id, label?, store } (or a function returning it): a device lab, anything\n that implements ObjectStore.\n\n --format json (alias --json) publish, stream, merge, doctor and status print one JSON document instead of prose\n (schemas: publish-result, stream-result, merge-result, doctor-result, report-status). A command\n that is wrong is {\"ok\":false,\"error\":{\"message\":\u2026,\"code\":\"usage\"}} on standard error.\n A store may be an Atlas server (https://\u2026): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.\n\n Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.\n";
17
17
  type Env = Readonly<Record<string, string | undefined>>;
18
18
  type Git = (args: readonly string[]) => string | null;
19
19
  /** What `reportMain` is given besides arguments: where output goes (`out`), and, for tests and embedding, the environment, working directory, git, clock and standard input (`stdin`, where `stream` reads checks from). Defaults are the real ones. */
@@ -38,12 +38,14 @@ export const REPORT_USAGE = `atlas report — layered evidence for any kind of s
38
38
  atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]
39
39
  ( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )
40
40
  [--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]
41
+ [--run-id <id>] [--run-title <t>] [--run-started-at <iso|ms>] [--run-ended-at <iso|ms>]
41
42
  [--spec <path> | --specs <dir>] [--upload-run]
42
43
  [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]
43
44
  [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...
44
45
  [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]
45
46
  atlas report stream [--store <dir|s3://…|https://atlas.example.com>] --producer <name> [--environment <name>]
46
47
  [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...
48
+ [--run-id <id>] [--run-title <t>]
47
49
  [--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)
48
50
  atlas report status [--store <dir|s3://…|id=url>]... [--backends <module>]
49
51
  [--specs <dir>] [--format text|json|markdown]
@@ -64,6 +66,11 @@ export const REPORT_USAGE = `atlas report — layered evidence for any kind of s
64
66
  --specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or
65
67
  file name the report gives; also publishes the specs themselves, so the
66
68
  node can show them without a checkout
69
+ --run-id <id> name this execution (a local or manual run with no CI run URL; also ATLAS_RUN_ID).
70
+ Layers that share an id are one run, however many shards or batches uploaded them;
71
+ another invocation at the same commit gets another id. Never part of what a check is
72
+ --run-title <t> a human name for the run (also ATLAS_RUN_TITLE); --run-started-at and --run-ended-at
73
+ record when it ran. Without --run-id a run is its CI run URL, else branch@sha
67
74
  --upload-run with --manifest: also upload the run's screenshots and clips for the viewer
68
75
  --attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive
69
76
  (screenshots, videos and traces of a single test go in its CTRF or checks file)
@@ -151,15 +158,32 @@ function linkSpec(spec) {
151
158
  const named = /^([^=/:]+)=(.+)$/.exec(spec);
152
159
  return named ? { name: named[1], url: named[2] } : { url: spec };
153
160
  }
154
- function parseTime(text) {
161
+ function parseTime(text, flag = '--recorded-at') {
155
162
  if (text === undefined)
156
163
  return null;
157
164
  const numeric = Number(text);
158
165
  const parsed = Number.isFinite(numeric) ? numeric : Date.parse(text);
159
166
  if (!Number.isFinite(parsed) || parsed < 0)
160
- throw new Error(`--recorded-at takes an ISO date or epoch milliseconds (got "${text}")`);
167
+ throw new Error(`${flag} takes an ISO date or epoch milliseconds (got "${text}")`);
161
168
  return parsed;
162
169
  }
170
+ /**
171
+ * The execution the producer named, from `--run-id`, `--run-title`, `--run-started-at`, `--run-ended-at` and then
172
+ * `ATLAS_RUN_ID` / `ATLAS_RUN_TITLE`, laid over a `run` the checks file gave. `undefined` when nothing names one, so the
173
+ * documented grouping (CI run URL, else branch@sha) applies. A title or time without an id is refused: it would have nothing to belong to.
174
+ */
175
+ function executionOf(rest, env, given) {
176
+ const file = typeof given === 'object' && given !== null && !Array.isArray(given) ? given : {};
177
+ const id = value(rest, 'run-id') ?? env.ATLAS_RUN_ID ?? (typeof file.id === 'string' ? file.id : undefined);
178
+ const title = value(rest, 'run-title') ?? env.ATLAS_RUN_TITLE;
179
+ const startedAtMs = parseTime(value(rest, 'run-started-at'), '--run-started-at');
180
+ const endedAtMs = parseTime(value(rest, 'run-ended-at'), '--run-ended-at');
181
+ if (id === undefined && (title !== undefined || startedAtMs !== null || endedAtMs !== null))
182
+ throw new Error('--run-title, --run-started-at and --run-ended-at go with --run-id <id> (or ATLAS_RUN_ID): a run needs an identity first');
183
+ if (id === undefined)
184
+ return undefined;
185
+ return { ...file, id, ...(title !== undefined ? { title } : {}), ...(startedAtMs !== null ? { startedAtMs } : {}), ...(endedAtMs !== null ? { endedAtMs } : {}) };
186
+ }
163
187
  function ingest(argv, base) {
164
188
  const read = (file) => readFileSync(path.resolve(base, file), 'utf8');
165
189
  const sources = ['ctrf', 'junit', 'manifest', 'checks'].filter(name => value(argv, name) !== undefined);
@@ -258,6 +282,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
258
282
  }
259
283
  const labels = { ...(ingested.labels ?? {}), ...(extra.labels ?? {}), ...Object.fromEntries(values(rest, 'label').map(pair => keyValue(pair, '--label'))) };
260
284
  const links = [...(extra.links ?? []), ...values(rest, 'link').map(linkSpec)];
285
+ const execution = executionOf(rest, env, extra.run);
261
286
  const layer = await publishChecks(target.store, {
262
287
  ...extra,
263
288
  producer,
@@ -272,6 +297,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
272
297
  ...(value(rest, 'title') ?? extra.title ? { title: (value(rest, 'title') ?? extra.title) } : {}),
273
298
  sha, branch,
274
299
  runUrl: value(rest, 'run-url') ?? extra.runUrl ?? origin.runUrl,
300
+ ...(execution ? { run: execution } : {}),
275
301
  ...(freshness ? { freshness } : {}),
276
302
  ...(value(rest, 'environment') ?? extra.environment ? { environment: (value(rest, 'environment') ?? extra.environment) } : {}),
277
303
  parameters: { ...(extra.parameters ?? {}), ...parameters }
@@ -329,6 +355,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
329
355
  const branch = value(rest, 'branch') ?? origin.branch;
330
356
  const freshness = detectFreshness({ sha, ci: origin.ci && !value(rest, 'sha') }, io.git);
331
357
  const environment = value(rest, 'environment');
358
+ const execution = executionOf(rest, env);
332
359
  const warnings = [];
333
360
  const reporter = createReporter({
334
361
  producer,
@@ -336,6 +363,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
336
363
  ...(value(rest, 'title') ? { title: value(rest, 'title') } : {}),
337
364
  sha, branch,
338
365
  runUrl: value(rest, 'run-url') ?? origin.runUrl,
366
+ ...(execution ? { run: execution } : {}),
339
367
  ...(freshness ? { freshness } : {}),
340
368
  ...(environment ? { environment } : {}),
341
369
  parameters: Object.fromEntries(values(rest, 'param').map(pair => keyValue(pair, '--param')))
@@ -13,7 +13,7 @@ import { type PublishChecksInput } from '../../schema/src/layer.js';
13
13
  import type { ObjectStore } from './object-store.js';
14
14
  type Check = PublishChecksInput['checks'][number];
15
15
  /** What every layer of one stream shares: who is reporting, from where. Whatever `publishChecks` takes, except the checks and the time. */
16
- export type ReporterIdentity = Pick<PublishChecksInput, 'producer' | 'kind' | 'title' | 'sha' | 'branch' | 'runUrl' | 'environment' | 'parameters' | 'freshness' | 'specs' | 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>;
16
+ export type ReporterIdentity = Pick<PublishChecksInput, 'producer' | 'kind' | 'title' | 'sha' | 'branch' | 'runUrl' | 'run' | 'environment' | 'parameters' | 'freshness' | 'specs' | 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>;
17
17
  /** How a reporter sends: the `store` (a folder, S3, or `httpStore` for a server), how often (`flushMs`, `batchSize`), how hard it tries (`attempts`, `backoffMs`), what it tells (`onError`, `onPublished`), and the clock and sleep for tests. */
18
18
  export type ReporterOptions = {
19
19
  /** Where layers go: a directory, S3, or `httpStore` for an Atlas server. */
@@ -234,10 +234,19 @@ export type TestHistoryDocument = {
234
234
  readonly test: TestSummary;
235
235
  readonly history: readonly TestResultEntry[];
236
236
  };
237
- /** A CI run (or any group of layers sharing a run URL, else a branch and commit): when, where, and how its tests went. */
237
+ /**
238
+ * A run: the layers that name the same execution (`run.id`), else share a CI run URL, else a branch and commit. When,
239
+ * where, and how its tests went.
240
+ */
238
241
  export type RunSummary = {
239
- /** Stable key: the run URL, or `branch@sha`. */
242
+ /** Stable key: `run:<id>` for an execution a producer named, else the run URL, else `branch@sha`. */
240
243
  readonly key: string;
244
+ /** The execution id a producer named for it, when it did. Layers that share it are one run, however many uploads they took; another invocation at the same commit has another. */
245
+ readonly runId?: string;
246
+ /** The human title the producer gave the execution (the newest layer's). Presentation only: the key, the commit and `freshness` are unaffected. */
247
+ readonly title?: string;
248
+ /** `modified` when any of its layers was recorded from a checkout with uncommitted changes, else `unknown` when any origin was not captured; absent when every layer is from a clean commit. Advisory; never changes a status. */
249
+ readonly freshness?: 'unknown' | 'modified';
241
250
  readonly runUrl: string | null;
242
251
  readonly branch: string | null;
243
252
  readonly sha: string | null;
@@ -290,8 +299,15 @@ export type RunDetail = {
290
299
  readonly run: RunSummary;
291
300
  readonly results: readonly RunResult[];
292
301
  };
293
- /** The run a layer belongs to: its run URL, else `branch@sha`, else `local`. Layers with the same key are one run. */
294
- export declare function runKey(layer: Pick<LayerHeader, 'runUrl' | 'branch' | 'sha'>): string;
302
+ /**
303
+ * The run a layer belongs to: the execution its producer named (`run:<id>`), else its run URL, else `branch@sha`, else
304
+ * `local`. Layers with the same key are one run. Only grouping: it is never part of the slot a result fills.
305
+ */
306
+ export declare function runKey(layer: Pick<LayerHeader, 'runUrl' | 'branch' | 'sha'> & {
307
+ readonly run?: {
308
+ readonly id: string;
309
+ };
310
+ }): string;
295
311
  /**
296
312
  * Every recorded result of every test (the newest `HISTORY_PER_TEST` per test, leaving out `running` progress) and the
297
313
  * runs they came from. A run's results are read from that history, so memory follows the number of tests and not the age
@@ -8,8 +8,13 @@ export const HISTORY_PER_TEST = 100;
8
8
  const FAILURE_RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 };
9
9
  /** How tests are ordered. */
10
10
  export const TEST_SORTS = ['spec', 'title', 'recent', 'slowest', 'failures', 'flaky', 'runs'];
11
- /** The run a layer belongs to: its run URL, else `branch@sha`, else `local`. Layers with the same key are one run. */
11
+ /**
12
+ * The run a layer belongs to: the execution its producer named (`run:<id>`), else its run URL, else `branch@sha`, else
13
+ * `local`. Layers with the same key are one run. Only grouping: it is never part of the slot a result fills.
14
+ */
12
15
  export function runKey(layer) {
16
+ if (layer.run?.id)
17
+ return `run:${layer.run.id}`;
13
18
  if (layer.runUrl)
14
19
  return layer.runUrl;
15
20
  if (layer.branch || layer.sha)
@@ -98,17 +103,29 @@ export class TestHistory {
98
103
  const key = runKey(layer);
99
104
  let run = this.#runs.get(key);
100
105
  if (!run) {
101
- run = { key, runUrl: layer.runUrl, branch: layer.branch, sha: layer.sha, start: layer.recordedAtMs, end: layer.recordedAtMs, first: layer.id, producers: new Set(), environments: new Set(), layers: 0 };
106
+ run = { key, runUrl: layer.runUrl, branch: layer.branch, sha: layer.sha, start: layer.recordedAtMs, end: layer.recordedAtMs, firstAt: layer.recordedAtMs, first: layer.id, producers: new Set(), environments: new Set(), layers: 0, runId: layer.run?.id, title: undefined, titledAt: 0, titledBy: '', freshness: undefined };
102
107
  this.#runs.set(key, run);
103
108
  }
104
109
  // A run is told by its first layer (by time, then id), whatever order its layers arrive in.
105
- if (layer.recordedAtMs < run.start || (layer.recordedAtMs === run.start && layer.id < run.first)) {
106
- run.start = layer.recordedAtMs;
110
+ if (layer.recordedAtMs < run.firstAt || (layer.recordedAtMs === run.firstAt && layer.id < run.first)) {
111
+ run.firstAt = layer.recordedAtMs;
107
112
  run.first = layer.id;
108
113
  run.branch = layer.branch;
109
114
  run.sha = layer.sha;
115
+ run.runUrl = layer.runUrl;
110
116
  }
111
- run.end = Math.max(run.end, layer.recordedAtMs);
117
+ run.start = Math.min(run.start, layer.recordedAtMs, layer.run?.startedAtMs ?? Infinity);
118
+ run.end = Math.max(run.end, layer.recordedAtMs, layer.run?.endedAtMs ?? -Infinity);
119
+ // The newest title wins, so a person can rename a run by sending its id again with another title.
120
+ const title = layer.run?.title;
121
+ if (title && (layer.recordedAtMs > run.titledAt || (layer.recordedAtMs === run.titledAt && layer.id > run.titledBy))) {
122
+ run.title = title;
123
+ run.titledAt = layer.recordedAtMs;
124
+ run.titledBy = layer.id;
125
+ }
126
+ // Local changes are never lost to a clean sibling: the run is as modified as its most modified layer.
127
+ if (layer.freshness === 'modified' || (layer.freshness === 'unknown' && run.freshness !== 'modified'))
128
+ run.freshness = layer.freshness;
112
129
  run.producers.add(layer.producer);
113
130
  if (layer.environment)
114
131
  run.environments.add(layer.environment);
@@ -227,7 +244,8 @@ function runSummary(run, cells) {
227
244
  failed.sort((a, b) => compareText(a.i.slot, b.i.slot) || compareText(a.r.o.place, b.r.o.place) || compareText(a.r.k, b.r.k));
228
245
  const failing = failed.slice(0, 20).map(cell => ({ key: cell.i.slot, title: cell.i.title, environment: cell.r.o.environment, detail: (cell.r.t.split('\n')[0] ?? '').slice(0, 500) }));
229
246
  return {
230
- key: run.key, runUrl: run.runUrl, branch: run.branch, sha: run.sha,
247
+ key: run.key, ...(run.runId ? { runId: run.runId } : {}), ...(run.title ? { title: run.title } : {}), ...(run.freshness ? { freshness: run.freshness } : {}),
248
+ runUrl: run.runUrl, branch: run.branch, sha: run.sha,
231
249
  startedAtMs: run.start, endedAtMs: run.end, spanMs: run.end - run.start,
232
250
  producers: [...run.producers].sort(), environments: [...run.environments].sort(),
233
251
  layers: run.layers, tests: tests.size, counts, totalDurationMs: measured ? total : null, failing,
@@ -631,6 +649,10 @@ export function historyMarkdown(document) {
631
649
  ...document.history.map(e => `| ${iso(e.at)} | ${e.environment ?? '—'} | ${e.status} | ${formatMs(e.durationMs)} | ${cell(e.branch ?? '—')} | ${e.sha?.slice(0, 10) ?? '—'} | ${cell(e.detail.split('\n')[0] ?? '')} |`),
632
650
  ].join('\n')}\n`;
633
651
  }
652
+ /** What to say beside a commit that was not recorded from a clean checkout: nothing, ` (modified)` or ` (origin unknown)`. */
653
+ function freshnessNote(freshness) {
654
+ return freshness === 'modified' ? ' (modified: local changes)' : freshness === 'unknown' ? ' (origin unknown)' : '';
655
+ }
634
656
  /** The run list as Markdown. */
635
657
  export function runsMarkdown(list) {
636
658
  return `${[
@@ -638,7 +660,7 @@ export function runsMarkdown(list) {
638
660
  '',
639
661
  '| Ended | Branch | Commit | Environments | Tests | Passed | Failed | Not run | Span | Run |',
640
662
  '|---|---|---|---|---:|---:|---:|---:|---:|---|',
641
- ...list.runs.map(r => `| ${iso(r.endedAtMs)} | ${cell(r.branch ?? '—')} | ${r.sha?.slice(0, 10) ?? '—'} | ${r.environments.join(', ') || '—'} | ${r.tests} | ${r.counts.passed} | ${r.counts.failed} | ${r.counts['not-run']} | ${formatMs(r.spanMs)} | ${cell(r.key)} |`),
663
+ ...list.runs.map(r => `| ${iso(r.endedAtMs)} | ${cell(r.branch ?? '—')} | ${r.sha?.slice(0, 10) ?? '—'}${freshnessNote(r.freshness)} | ${r.environments.join(', ') || '—'} | ${r.tests} | ${r.counts.passed} | ${r.counts.failed} | ${r.counts['not-run']} | ${formatMs(r.spanMs)} | ${cell(r.title ? `${r.title} (${r.key})` : r.key)} |`),
642
664
  ].join('\n')}\n`;
643
665
  }
644
666
  /** One run as Markdown. */
@@ -647,7 +669,8 @@ export function runMarkdown(detail) {
647
669
  return `${[
648
670
  `# Run ${run.key}`,
649
671
  '',
650
- `- Branch ${run.branch ?? '—'}, commit ${run.sha ?? '—'}`,
672
+ ...(run.title ? [`- Title: ${run.title}`] : []),
673
+ `- Branch ${run.branch ?? '—'}, commit ${run.sha ?? '—'}${freshnessNote(run.freshness)}`,
651
674
  `- ${iso(run.startedAtMs)} to ${iso(run.endedAtMs)} (${formatMs(run.spanMs)}); ${run.layers} layers from ${run.producers.join(', ')}`,
652
675
  `- Environments: ${run.environments.join(', ') || '—'}`,
653
676
  `- ${run.tests} tests: ${counted(run.counts)}${run.totalDurationMs === null ? '' : `; ${formatMs(run.totalDurationMs)} of test time`}`,
@@ -107,6 +107,12 @@ export declare const LayerHeaderSchema: z.ZodObject<{
107
107
  unknown: "unknown";
108
108
  modified: "modified";
109
109
  }>>;
110
+ run: z.ZodOptional<z.ZodObject<{
111
+ id: z.ZodString;
112
+ title: z.ZodOptional<z.ZodString>;
113
+ startedAtMs: z.ZodOptional<z.ZodNumber>;
114
+ endedAtMs: z.ZodOptional<z.ZodNumber>;
115
+ }, z.core.$loose>>;
110
116
  recording: z.ZodNullable<z.ZodUnknown>;
111
117
  live: z.ZodBoolean;
112
118
  counts: z.ZodRecord<z.ZodEnum<{
@@ -248,6 +254,12 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
248
254
  unknown: "unknown";
249
255
  modified: "modified";
250
256
  }>>;
257
+ run: z.ZodOptional<z.ZodObject<{
258
+ id: z.ZodString;
259
+ title: z.ZodOptional<z.ZodString>;
260
+ startedAtMs: z.ZodOptional<z.ZodNumber>;
261
+ endedAtMs: z.ZodOptional<z.ZodNumber>;
262
+ }, z.core.$loose>>;
251
263
  recording: z.ZodNullable<z.ZodUnknown>;
252
264
  live: z.ZodBoolean;
253
265
  counts: z.ZodRecord<z.ZodEnum<{
@@ -326,6 +338,12 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
326
338
  unknown: "unknown";
327
339
  modified: "modified";
328
340
  }>>;
341
+ run: z.ZodOptional<z.ZodObject<{
342
+ id: z.ZodString;
343
+ title: z.ZodOptional<z.ZodString>;
344
+ startedAtMs: z.ZodOptional<z.ZodNumber>;
345
+ endedAtMs: z.ZodOptional<z.ZodNumber>;
346
+ }, z.core.$loose>>;
329
347
  recording: z.ZodNullable<z.ZodUnknown>;
330
348
  live: z.ZodBoolean;
331
349
  counts: z.ZodRecord<z.ZodEnum<{
@@ -425,6 +443,12 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
425
443
  unknown: "unknown";
426
444
  modified: "modified";
427
445
  }>>;
446
+ run: z.ZodOptional<z.ZodObject<{
447
+ id: z.ZodString;
448
+ title: z.ZodOptional<z.ZodString>;
449
+ startedAtMs: z.ZodOptional<z.ZodNumber>;
450
+ endedAtMs: z.ZodOptional<z.ZodNumber>;
451
+ }, z.core.$loose>>;
428
452
  recording: z.ZodNullable<z.ZodUnknown>;
429
453
  live: z.ZodBoolean;
430
454
  counts: z.ZodRecord<z.ZodEnum<{
@@ -485,6 +509,12 @@ export declare const EvidencePageSchema: z.ZodObject<{
485
509
  unknown: "unknown";
486
510
  modified: "modified";
487
511
  }>>;
512
+ run: z.ZodOptional<z.ZodObject<{
513
+ id: z.ZodString;
514
+ title: z.ZodOptional<z.ZodString>;
515
+ startedAtMs: z.ZodOptional<z.ZodNumber>;
516
+ endedAtMs: z.ZodOptional<z.ZodNumber>;
517
+ }, z.core.$loose>>;
488
518
  recording: z.ZodNullable<z.ZodUnknown>;
489
519
  live: z.ZodBoolean;
490
520
  counts: z.ZodRecord<z.ZodEnum<{
@@ -1354,9 +1384,15 @@ export declare const TestHistorySchema: z.ZodObject<{
1354
1384
  }, z.core.$loose>>;
1355
1385
  }, z.core.$loose>>;
1356
1386
  }, z.core.$loose>;
1357
- /** A run: the layers that share a run URL (else a branch and commit), with what they recorded. */
1387
+ /** A run: the layers that name the same execution (else share a run URL, else a branch and commit), with what they recorded. */
1358
1388
  export declare const ReportRunSchema: z.ZodObject<{
1359
1389
  key: z.ZodString;
1390
+ runId: z.ZodOptional<z.ZodString>;
1391
+ title: z.ZodOptional<z.ZodString>;
1392
+ freshness: z.ZodOptional<z.ZodEnum<{
1393
+ unknown: "unknown";
1394
+ modified: "modified";
1395
+ }>>;
1360
1396
  runUrl: z.ZodNullable<z.ZodString>;
1361
1397
  branch: z.ZodNullable<z.ZodString>;
1362
1398
  sha: z.ZodNullable<z.ZodString>;
@@ -1388,6 +1424,12 @@ export declare const RunListSchema: z.ZodObject<{
1388
1424
  total: z.ZodNumber;
1389
1425
  runs: z.ZodArray<z.ZodObject<{
1390
1426
  key: z.ZodString;
1427
+ runId: z.ZodOptional<z.ZodString>;
1428
+ title: z.ZodOptional<z.ZodString>;
1429
+ freshness: z.ZodOptional<z.ZodEnum<{
1430
+ unknown: "unknown";
1431
+ modified: "modified";
1432
+ }>>;
1391
1433
  runUrl: z.ZodNullable<z.ZodString>;
1392
1434
  branch: z.ZodNullable<z.ZodString>;
1393
1435
  sha: z.ZodNullable<z.ZodString>;
@@ -1419,6 +1461,12 @@ export declare const RunDetailSchema: z.ZodObject<{
1419
1461
  version: z.ZodString;
1420
1462
  run: z.ZodObject<{
1421
1463
  key: z.ZodString;
1464
+ runId: z.ZodOptional<z.ZodString>;
1465
+ title: z.ZodOptional<z.ZodString>;
1466
+ freshness: z.ZodOptional<z.ZodEnum<{
1467
+ unknown: "unknown";
1468
+ modified: "modified";
1469
+ }>>;
1422
1470
  runUrl: z.ZodNullable<z.ZodString>;
1423
1471
  branch: z.ZodNullable<z.ZodString>;
1424
1472
  sha: z.ZodNullable<z.ZodString>;
@@ -14,7 +14,7 @@
14
14
  * @see docs/server.md
15
15
  */
16
16
  import { z } from 'zod/v4';
17
- import { CheckSchema } from './layer.js';
17
+ import { CheckSchema, LayerRunSchema } from './layer.js';
18
18
  /** One of the statuses a check has in a report: `passed`, `failed`, `running` (not finished) or `not-run`. Closed. */
19
19
  export const EvidenceStatusSchema = z.enum(['passed', 'failed', 'running', 'not-run']);
20
20
  /**
@@ -86,6 +86,7 @@ export const LayerHeaderSchema = z.looseObject({
86
86
  runUrl: z.string().nullable(),
87
87
  parameters: z.record(z.string(), z.string()).describe('Other choices that make a result its own slot'),
88
88
  freshness: z.enum(['unknown', 'modified']).optional().describe('`modified`: recorded from a checkout with uncommitted changes. `unknown`: origin not captured. Absent: clean'),
89
+ run: LayerRunSchema.optional().describe('The execution the producer named for it (one local or manual invocation). Grouping and display only; never part of the slot'),
89
90
  recording: z.unknown().nullable().describe('Where recorded timelines are read from, or null'),
90
91
  live: z.boolean(),
91
92
  counts: z.record(EvidenceStatusSchema, z.number().int().nonnegative()),
@@ -332,9 +333,12 @@ export const TestHistorySchema = z.looseObject({
332
333
  test: TestSummarySchema,
333
334
  history: z.array(TestResultEntrySchema)
334
335
  });
335
- /** A run: the layers that share a run URL (else a branch and commit), with what they recorded. */
336
+ /** A run: the layers that name the same execution (else share a run URL, else a branch and commit), with what they recorded. */
336
337
  export const ReportRunSchema = z.looseObject({
337
- key: z.string().describe('The run URL, or `branch@sha`. Pass it to `/api/reports/runs/detail?key=` or `/api/reports/tests?run=`'),
338
+ key: z.string().describe('`run:<id>` for an execution a producer named, else the run URL, else `branch@sha`. Pass it to `/api/reports/runs/detail?key=` or `/api/reports/tests?run=`'),
339
+ runId: z.string().optional().describe('The execution id the producer named, when it did. Layers that share it are one run however many uploads they took'),
340
+ title: z.string().optional().describe('The human title of the execution (the newest layer\'s). Presentation only: the key, commit and `freshness` are unaffected'),
341
+ freshness: z.enum(['unknown', 'modified']).optional().describe('`modified` when any layer of the run was recorded from a checkout with uncommitted changes; `unknown` when an origin was not captured; absent when all are clean. Shown beside the commit, never replaced by the title'),
338
342
  runUrl: z.string().nullable(), branch: z.string().nullable(), sha: z.string().nullable(),
339
343
  startedAtMs: z.number(), endedAtMs: z.number(), spanMs: z.number(),
340
344
  producers: z.array(z.string()), environments: z.array(z.string()),
@@ -60,6 +60,20 @@ export type CheckStatus = z.infer<typeof CheckStatusSchema>;
60
60
  * changes only with a new `schemaVersion`.
61
61
  */
62
62
  export declare const FRESHNESS: readonly ["unknown", "modified"];
63
+ /**
64
+ * The execution a layer belongs to, named by the producer: one invocation of a local or manual run, which several
65
+ * layers (shards, batches, producers) may share. Presentation and history grouping only: it never takes part in the
66
+ * slot a result fills, in which result wins, or in a status. Absent, a run is told by its CI run URL, else `branch@sha`.
67
+ * Within one project, layers that carry the same `id` are one run; a later invocation gets another `id`.
68
+ */
69
+ export declare const LayerRunSchema: z.ZodObject<{
70
+ id: z.ZodString;
71
+ title: z.ZodOptional<z.ZodString>;
72
+ startedAtMs: z.ZodOptional<z.ZodNumber>;
73
+ endedAtMs: z.ZodOptional<z.ZodNumber>;
74
+ }, z.core.$loose>;
75
+ /** The execution of a layer, parsed from `LayerRunSchema`. */
76
+ export type LayerRun = z.output<typeof LayerRunSchema>;
63
77
  /**
64
78
  * A number a check measured, such as a latency or a size, with its unit and an optional allowance. A measurement over
65
79
  * its `limit` is reported and never changes the check's status.
@@ -1211,6 +1225,12 @@ export declare const LayerDocumentSchema: z.ZodObject<{
1211
1225
  unknown: "unknown";
1212
1226
  modified: "modified";
1213
1227
  }>>;
1228
+ run: z.ZodOptional<z.ZodObject<{
1229
+ id: z.ZodString;
1230
+ title: z.ZodOptional<z.ZodString>;
1231
+ startedAtMs: z.ZodOptional<z.ZodNumber>;
1232
+ endedAtMs: z.ZodOptional<z.ZodNumber>;
1233
+ }, z.core.$loose>>;
1214
1234
  specs: z.ZodDefault<z.ZodArray<z.ZodObject<{
1215
1235
  path: z.ZodString;
1216
1236
  domain: z.ZodString;
@@ -2109,6 +2129,12 @@ export declare const PublishChecksSchema: z.ZodObject<{
2109
2129
  unknown: "unknown";
2110
2130
  modified: "modified";
2111
2131
  }>>;
2132
+ run: z.ZodOptional<z.ZodObject<{
2133
+ id: z.ZodString;
2134
+ title: z.ZodOptional<z.ZodString>;
2135
+ startedAtMs: z.ZodOptional<z.ZodNumber>;
2136
+ endedAtMs: z.ZodOptional<z.ZodNumber>;
2137
+ }, z.core.$loose>>;
2112
2138
  specs: z.ZodDefault<z.ZodArray<z.ZodObject<{
2113
2139
  path: z.ZodString;
2114
2140
  domain: z.ZodString;