@crvouga/atlas 0.5.0 → 0.6.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 (140) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +30 -11
  3. package/dist/core/src/cli.d.ts +1 -1
  4. package/dist/core/src/cli.js +8 -3
  5. package/dist/core/src/config.d.ts +7 -0
  6. package/dist/core/src/config.js +48 -14
  7. package/dist/core/src/run/execute.d.ts +6 -0
  8. package/dist/core/src/run/execute.js +4 -2
  9. package/dist/reports/src/doctor.d.ts +1 -1
  10. package/dist/reports/src/doctor.js +3 -1
  11. package/dist/reports/src/http-store.d.ts +4 -0
  12. package/dist/reports/src/http-store.js +6 -3
  13. package/dist/reports/src/init.js +6 -1
  14. package/dist/reports/src/live-reporter.d.ts +34 -0
  15. package/dist/reports/src/live-reporter.js +122 -0
  16. package/dist/reports/src/report-cli.d.ts +1 -1
  17. package/dist/reports/src/report-cli.js +3 -2
  18. package/dist/reports/src/store-url.js +1 -1
  19. package/dist/runners/src/codecept.d.ts +14 -0
  20. package/dist/runners/src/codecept.js +16 -0
  21. package/dist/runners/src/jest.d.ts +50 -0
  22. package/dist/runners/src/jest.js +52 -0
  23. package/dist/runners/src/mocha.d.ts +40 -0
  24. package/dist/runners/src/mocha.js +39 -0
  25. package/dist/runners/src/node-test.d.ts +28 -0
  26. package/dist/runners/src/node-test.js +44 -0
  27. package/dist/runners/src/publish.d.ts +30 -0
  28. package/dist/runners/src/publish.js +68 -0
  29. package/dist/schema/src/api.d.ts +1 -0
  30. package/dist/schema/src/api.js +1 -1
  31. package/dist/schema/src/index.d.ts +2 -0
  32. package/dist/schema/src/index.js +2 -0
  33. package/dist/schema/src/live-run.d.ts +123 -0
  34. package/dist/schema/src/live-run.js +30 -0
  35. package/dist/schema/src/manifest.d.ts +2 -2
  36. package/dist/schema/src/project.d.ts +20 -0
  37. package/dist/schema/src/project.js +10 -0
  38. package/dist/schema/src/report-sources.d.ts +1 -0
  39. package/dist/schema/src/report-sources.js +2 -1
  40. package/dist/schema/src/runs-index.d.ts +1 -1
  41. package/dist/schema/src/server-config.d.ts +84 -0
  42. package/dist/schema/src/server-config.js +12 -0
  43. package/dist/server/src/app.d.ts +10 -0
  44. package/dist/server/src/app.js +148 -10
  45. package/dist/server/src/config.d.ts +1 -1
  46. package/dist/server/src/config.js +20 -3
  47. package/dist/server/src/dev.d.ts +3 -3
  48. package/dist/server/src/dev.js +22 -13
  49. package/dist/server/src/events.d.ts +3 -3
  50. package/dist/server/src/events.js +1 -1
  51. package/dist/server/src/index.d.ts +2 -0
  52. package/dist/server/src/index.js +2 -0
  53. package/dist/server/src/live-runs.d.ts +52 -0
  54. package/dist/server/src/live-runs.js +229 -0
  55. package/dist/server/src/main.d.ts +1 -1
  56. package/dist/server/src/main.js +7 -5
  57. package/dist/server/src/openapi.d.ts +2 -5
  58. package/dist/server/src/openapi.js +71 -7
  59. package/dist/server/src/projects.d.ts +3 -0
  60. package/dist/server/src/projects.js +154 -0
  61. package/dist/server/src/settings-page.d.ts +9 -0
  62. package/dist/server/src/settings-page.js +93 -0
  63. package/dist/server/src/settings.d.ts +23 -0
  64. package/dist/server/src/settings.js +72 -0
  65. package/dist/server/src/site.d.ts +8 -6
  66. package/dist/server/src/site.js +45 -10
  67. package/dist/server/src/store-api.js +43 -22
  68. package/docs/deploying.md +19 -19
  69. package/docs/dogfooding.md +2 -2
  70. package/docs/extending.md +3 -3
  71. package/docs/integrating.md +9 -9
  72. package/docs/reference/cli.md +9 -7
  73. package/docs/reference/core.md +6 -0
  74. package/docs/reference/http-api.md +498 -2
  75. package/docs/reference/reports.md +55 -1
  76. package/docs/reference/runners.md +169 -0
  77. package/docs/reference/schema.md +111 -0
  78. package/docs/reference/server.md +154 -14
  79. package/docs/report-backends.md +8 -8
  80. package/docs/report-contract.md +7 -7
  81. package/docs/reporting-issues.md +3 -3
  82. package/docs/runners.md +69 -0
  83. package/docs/server.md +61 -25
  84. package/docs/statecharts.md +22 -20
  85. package/llms-full.txt +713 -105
  86. package/llms.txt +3 -1
  87. package/package.json +31 -9
  88. package/packages/runners/README.md +6 -0
  89. package/packages/runners/compat/codecept.cjs +2 -0
  90. package/packages/runners/compat/jest.cjs +2 -0
  91. package/packages/runners/compat/mocha.cjs +2 -0
  92. package/packages/schema/schemas/atlas-change.schema.json +4 -0
  93. package/packages/schema/schemas/hello-event.schema.json +4 -0
  94. package/packages/schema/schemas/live-run-start.schema.json +42 -0
  95. package/packages/schema/schemas/live-run-update.schema.json +93 -0
  96. package/packages/schema/schemas/live-run.schema.json +225 -0
  97. package/packages/schema/schemas/project-slug.schema.json +8 -0
  98. package/packages/schema/schemas/project.schema.json +22 -0
  99. package/packages/schema/schemas/projects.schema.json +33 -0
  100. package/packages/schema/schemas/server-config.schema.json +14 -0
  101. package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
  102. package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
  103. package/packages/server/openapi.json +5101 -779
  104. package/packages/visualizer/README.md +13 -13
  105. package/packages/visualizer/bin/atlas-client.mjs +176 -0
  106. package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
  107. package/packages/visualizer/src/app/AppShell.tsx +26 -17
  108. package/packages/visualizer/src/app/report-sources.tsx +2 -1
  109. package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
  110. package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
  111. package/packages/visualizer/src/components/Header.module.css +101 -227
  112. package/packages/visualizer/src/components/Header.tsx +79 -41
  113. package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
  114. package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
  115. package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
  116. package/packages/visualizer/src/data/queries.ts +14 -7
  117. package/packages/visualizer/src/evidence/api.ts +2 -1
  118. package/packages/visualizer/src/evidence/check-detail.tsx +19 -4
  119. package/packages/visualizer/src/evidence/document-page.tsx +6 -2
  120. package/packages/visualizer/src/evidence/evidence-context.tsx +17 -12
  121. package/packages/visualizer/src/evidence/evidence.module.css +11 -1
  122. package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
  123. package/packages/visualizer/src/evidence/model-page.tsx +6 -2
  124. package/packages/visualizer/src/evidence/project-home.tsx +45 -0
  125. package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
  126. package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
  127. package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
  128. package/packages/visualizer/src/live/live-query.ts +21 -0
  129. package/packages/visualizer/src/live/live-runs.tsx +69 -0
  130. package/packages/visualizer/src/live/live-stream.ts +30 -9
  131. package/packages/visualizer/src/live/report-progress.ts +62 -0
  132. package/packages/visualizer/src/live/test-progress.tsx +34 -0
  133. package/packages/visualizer/src/runtime.ts +17 -1
  134. package/packages/visualizer/src/state/ui-store.ts +3 -2
  135. package/packages/visualizer/src/views/ChartPage.tsx +2 -0
  136. package/packages/visualizer/src/views/ChartTests.tsx +43 -0
  137. package/packages/visualizer/src/views/SidePanel.tsx +2 -0
  138. package/packages/visualizer/vite/atlas-files.test.ts +4 -4
  139. package/packages/visualizer/vite/atlas-files.ts +1 -5
  140. package/packages/visualizer/vite/atlas-server-proxy.ts +2 -1
@@ -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). 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 [--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";
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. */
@@ -54,7 +54,8 @@ export const REPORT_USAGE = `atlas report — layered evidence for any kind of s
54
54
  atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]
55
55
 
56
56
  A store is a directory or s3://bucket/prefix (add ?region=…&endpoint=… for R2, MinIO and the
57
- rest). When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set
57
+ rest). Select a project on an Atlas node with --project <slug> or ATLAS_PROJECT. This scopes specs, reports and recordings together.
58
+ When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set
58
59
  AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with \`aws sso login\`, or use a role.
59
60
 
60
61
  publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.
@@ -211,7 +212,7 @@ function printChecks(out, label, checks) {
211
212
  export async function reportMain(argv, io = { out: text => process.stdout.write(text) }) {
212
213
  const [command, ...rest] = argv;
213
214
  const base = io.cwd ?? process.cwd();
214
- const env = io.env ?? process.env;
215
+ const env = { ...(io.env ?? process.env), ...(value(rest, 'project') !== undefined ? { ATLAS_PROJECT: value(rest, 'project') } : {}) };
215
216
  const clock = io.clock ?? Date.now;
216
217
  if (command === 'publish') {
217
218
  const target = writeStoreOf(value(rest, 'store'), base, env);
@@ -77,7 +77,7 @@ export function openStore(spec, options) {
77
77
  const token = serverToken(env, options.role ?? { id: '', soleServer: true });
78
78
  if (token)
79
79
  refuseClearToken(location.url, env);
80
- const server = httpStore({ url: location.url, ...(token ? { token } : {}), ...(options.fetch ? { fetch: options.fetch } : {}) });
80
+ const server = httpStore({ url: location.url, ...(env.ATLAS_PROJECT ? { project: env.ATLAS_PROJECT } : {}), ...(token ? { token } : {}), ...(options.fetch ? { fetch: options.fetch } : {}) });
81
81
  return { store: server, label: server.label, location, server };
82
82
  }
83
83
  const s3 = s3Store({
@@ -0,0 +1,14 @@
1
+ /** @module CodeceptJS reporter
2
+ * CodeceptJS's Mocha reporter interface carries scenario outcomes and steps to the singleton Atlas server.
3
+ */
4
+ import AtlasMochaReporter from './mocha.js';
5
+ /**
6
+ * Use `codeceptjs run --reporter @crvouga/atlas/codecept` with ATLAS_SERVER_URL and ATLAS_PUBLISH_TOKEN.
7
+ * Upload failures fail the run; skipped scenarios remain not-run.
8
+ * @example
9
+ * // codecept.conf.js
10
+ * export const config = { tests: './tests/*_test.js', reporter: '@crvouga/atlas/codecept' };
11
+ */
12
+ export default class AtlasCodeceptReporter extends AtlasMochaReporter {
13
+ constructor(...args: [ConstructorParameters<typeof AtlasMochaReporter>[0], ConstructorParameters<typeof AtlasMochaReporter>[1]?]);
14
+ }
@@ -0,0 +1,16 @@
1
+ /** @module CodeceptJS reporter
2
+ * CodeceptJS's Mocha reporter interface carries scenario outcomes and steps to the singleton Atlas server.
3
+ */
4
+ import AtlasMochaReporter from './mocha.js';
5
+ /**
6
+ * Use `codeceptjs run --reporter @crvouga/atlas/codecept` with ATLAS_SERVER_URL and ATLAS_PUBLISH_TOKEN.
7
+ * Upload failures fail the run; skipped scenarios remain not-run.
8
+ * @example
9
+ * // codecept.conf.js
10
+ * export const config = { tests: './tests/*_test.js', reporter: '@crvouga/atlas/codecept' };
11
+ */
12
+ export default class AtlasCodeceptReporter extends AtlasMochaReporter {
13
+ constructor(...args) {
14
+ super(args[0], args[1], 'codeceptjs');
15
+ }
16
+ }
@@ -0,0 +1,50 @@
1
+ import { type ReporterOptions } from './publish.js';
2
+ type Result = {
3
+ testFilePath: string;
4
+ testExecError?: {
5
+ message?: string;
6
+ stack?: string;
7
+ };
8
+ failureMessage?: string;
9
+ testResults: {
10
+ title: string;
11
+ fullName: string;
12
+ status: string;
13
+ duration?: number | null;
14
+ failureMessages: string[];
15
+ invocations?: number;
16
+ }[];
17
+ };
18
+ /**
19
+ * Jest's native reporter; retain `default` in the reporter list for console output. Upload errors fail Jest.
20
+ * @example
21
+ * export default { reporters: ['default', ['@crvouga/atlas/jest', { url: 'https://atlas.example.com' }]] };
22
+ */
23
+ export default class AtlasJestReporter {
24
+ private readonly options;
25
+ private live;
26
+ private error;
27
+ constructor(_config: unknown, options?: ReporterOptions);
28
+ /** Reset publishing errors for each watch-mode invocation. */
29
+ onRunStart(result?: {
30
+ numTotalTests?: number;
31
+ }): void;
32
+ /** Publish the test as running before its body executes in a worker. */
33
+ onTestCaseStart(test: {
34
+ path: string;
35
+ }, info: {
36
+ fullName: string;
37
+ title: string;
38
+ }): void;
39
+ /** Stream each test outcome immediately, without waiting for other files to finish. */
40
+ onTestCaseResult(test: {
41
+ path: string;
42
+ }, result: Result['testResults'][number]): void;
43
+ /** Await uploading every final result, including test-file failures before any test ran. */
44
+ onRunComplete(_contexts: unknown, result: {
45
+ testResults: Result[];
46
+ }): Promise<void>;
47
+ /** Jest uses this to turn a publishing error into a failed invocation. */
48
+ getLastError(): Error | undefined;
49
+ }
50
+ export {};
@@ -0,0 +1,52 @@
1
+ import { testFile, runnerLive, finishRunner } from './publish.js';
2
+ /**
3
+ * Jest's native reporter; retain `default` in the reporter list for console output. Upload errors fail Jest.
4
+ * @example
5
+ * export default { reporters: ['default', ['@crvouga/atlas/jest', { url: 'https://atlas.example.com' }]] };
6
+ */
7
+ export default class AtlasJestReporter {
8
+ options;
9
+ live;
10
+ error;
11
+ constructor(_config, options = {}) {
12
+ this.options = options;
13
+ }
14
+ /** Reset publishing errors for each watch-mode invocation. */
15
+ onRunStart(result) { this.error = undefined; this.live = runnerLive('jest', this.options, result?.numTotalTests || undefined); }
16
+ /** Publish the test as running before its body executes in a worker. */
17
+ onTestCaseStart(test, info) {
18
+ this.live ??= runnerLive('jest', this.options);
19
+ this.live.check({ id: `${testFile(test.path, this.options.rootDir)} > ${info.fullName}`, title: info.title, status: 'running' });
20
+ }
21
+ /** Stream each test outcome immediately, without waiting for other files to finish. */
22
+ onTestCaseResult(test, result) {
23
+ this.live ??= runnerLive('jest', this.options);
24
+ this.live.check({ id: `${testFile(test.path, this.options.rootDir)} > ${result.fullName}`, title: result.title,
25
+ status: result.status === 'passed' ? 'passed' : result.status === 'failed' ? 'failed' : 'not-run', detail: result.failureMessages.join('\n') });
26
+ }
27
+ /** Await uploading every final result, including test-file failures before any test ran. */
28
+ async onRunComplete(_contexts, result) {
29
+ const checks = [];
30
+ for (const suite of result.testResults) {
31
+ const file = testFile(suite.testFilePath, this.options.rootDir);
32
+ for (const test of suite.testResults)
33
+ checks.push({ id: `${file} > ${test.fullName}`, title: test.title,
34
+ status: test.status === 'passed' ? 'passed' : test.status === 'failed' ? 'failed' : 'not-run',
35
+ detail: test.failureMessages.join('\n'), measurements: [],
36
+ ...(typeof test.duration === 'number' && Number.isFinite(test.duration) && test.duration >= 0 ? { durationMs: test.duration } : {}),
37
+ ...((test.invocations ?? 1) > 1 ? { labels: { retries: String(test.invocations - 1), ...(test.status === 'passed' ? { flaky: 'true' } : {}) } } : {}) });
38
+ if (suite.testExecError)
39
+ checks.push({ id: `${file} > [suite error]`, title: `${file}: suite error`, status: 'failed', detail: suite.testExecError.stack ?? suite.testExecError.message ?? suite.failureMessage ?? 'Suite did not execute', measurements: [] });
40
+ }
41
+ this.live ??= runnerLive('jest', this.options);
42
+ checks.forEach(check => this.live.check(check));
43
+ try {
44
+ await finishRunner(this.live, 'jest', checks, this.options);
45
+ }
46
+ catch (error) {
47
+ this.error = error instanceof Error ? error : new Error(String(error));
48
+ }
49
+ }
50
+ /** Jest uses this to turn a publishing error into a failed invocation. */
51
+ getLastError() { return this.error; }
52
+ }
@@ -0,0 +1,40 @@
1
+ import { type ReporterOptions } from './publish.js';
2
+ type Test = {
3
+ title: string;
4
+ file?: string;
5
+ duration?: number;
6
+ fullTitle(): string;
7
+ err?: {
8
+ stack?: string;
9
+ message?: string;
10
+ };
11
+ steps?: {
12
+ name?: string;
13
+ status?: string;
14
+ }[];
15
+ currentRetry?(): number;
16
+ };
17
+ type Runner = {
18
+ total?: number;
19
+ on(event: string, listener: (test: Test, error?: Error) => void): unknown;
20
+ };
21
+ /**
22
+ * Mocha reporter; use `--reporter @crvouga/atlas/mocha` or CodeceptJS `--reporter @crvouga/atlas/codecept`.
23
+ * Reporter options go in `reporterOptions`; environment variables are supported by every runner.
24
+ * A failed upload adds a runner failure through Mocha's completion callback.
25
+ * @example
26
+ * // mocha configuration
27
+ * export default { reporter: '@crvouga/atlas/mocha', reporterOptions: { url: 'https://atlas.example.com' } };
28
+ */
29
+ export default class AtlasMochaReporter {
30
+ private readonly checks;
31
+ private readonly options;
32
+ private readonly runnerName;
33
+ private readonly live;
34
+ constructor(runner: Runner, options?: {
35
+ reporterOptions?: ReporterOptions;
36
+ }, runnerName?: string);
37
+ /** Mocha waits for this callback before exiting. Publishing errors fail the invocation. */
38
+ done(failures: number, callback: (failures: number) => void): void;
39
+ }
40
+ export {};
@@ -0,0 +1,39 @@
1
+ import { testFile, runnerLive, finishRunner } from './publish.js';
2
+ /**
3
+ * Mocha reporter; use `--reporter @crvouga/atlas/mocha` or CodeceptJS `--reporter @crvouga/atlas/codecept`.
4
+ * Reporter options go in `reporterOptions`; environment variables are supported by every runner.
5
+ * A failed upload adds a runner failure through Mocha's completion callback.
6
+ * @example
7
+ * // mocha configuration
8
+ * export default { reporter: '@crvouga/atlas/mocha', reporterOptions: { url: 'https://atlas.example.com' } };
9
+ */
10
+ export default class AtlasMochaReporter {
11
+ checks = new Map();
12
+ options;
13
+ runnerName;
14
+ live;
15
+ constructor(runner, options = {}, runnerName = 'mocha') {
16
+ this.options = options.reporterOptions ?? {};
17
+ this.runnerName = runnerName;
18
+ this.live = runnerLive(runnerName, this.options, runner.total);
19
+ const add = (test, status, error) => {
20
+ const file = testFile(test.file, this.options.rootDir);
21
+ const retry = test.currentRetry?.() ?? 0;
22
+ this.checks.set(test, { id: [file, test.fullTitle()].filter(Boolean).join(' > '), title: test.title, status,
23
+ detail: error?.stack ?? error?.message ?? (status === 'failed' ? test.err?.stack ?? test.err?.message ?? 'failed' : ''), measurements: [],
24
+ ...(typeof test.duration === 'number' && Number.isFinite(test.duration) && test.duration >= 0 ? { durationMs: test.duration } : {}),
25
+ ...(retry ? { labels: { retries: String(retry), ...(status === 'passed' ? { flaky: 'true' } : {}) } } : {}),
26
+ ...(test.steps?.length ? { steps: test.steps.map(step => ({ name: step.name ?? 'step', ...(step.status ? { status: step.status } : {}) })) } : {}) });
27
+ const check = this.checks.get(test);
28
+ this.live.check({ ...check, steps: check.steps?.map(step => ({ name: step.name, status: step.status })) });
29
+ };
30
+ runner.on('test', test => add(test, 'running'));
31
+ runner.on('pass', test => add(test, 'passed'));
32
+ runner.on('fail', (test, error) => add(test, 'failed', error));
33
+ runner.on('pending', test => add(test, 'not-run'));
34
+ }
35
+ /** Mocha waits for this callback before exiting. Publishing errors fail the invocation. */
36
+ done(failures, callback) {
37
+ void finishRunner(this.live, this.runnerName, [...this.checks.values()], this.options).then(() => callback(failures), error => { console.error(error instanceof Error ? error.message : String(error)); callback(failures + 1); });
38
+ }
39
+ }
@@ -0,0 +1,28 @@
1
+ type Event = {
2
+ type: string;
3
+ data: {
4
+ name?: string;
5
+ file?: string;
6
+ nesting?: number;
7
+ type?: string;
8
+ skip?: boolean | string;
9
+ todo?: boolean | string;
10
+ details?: {
11
+ type?: string;
12
+ duration_ms?: number;
13
+ error?: {
14
+ stack?: string;
15
+ message?: string;
16
+ failureType?: string;
17
+ };
18
+ };
19
+ };
20
+ };
21
+ /**
22
+ * Async generator reporter. Use alongside Node's spec reporter to keep normal output.
23
+ * Upload errors reject the stream; skips and TODOs never count as passed. Successful suite containers are excluded; hook failures are retained.
24
+ * @example
25
+ * // node --test --test-reporter=spec --test-reporter=@crvouga/atlas/node-test
26
+ */
27
+ export default function atlasNodeReporter(source: AsyncIterable<Event>): AsyncGenerator<string>;
28
+ export {};
@@ -0,0 +1,44 @@
1
+ import { testFile, runnerLive, finishRunner } from './publish.js';
2
+ /**
3
+ * Async generator reporter. Use alongside Node's spec reporter to keep normal output.
4
+ * Upload errors reject the stream; skips and TODOs never count as passed. Successful suite containers are excluded; hook failures are retained.
5
+ * @example
6
+ * // node --test --test-reporter=spec --test-reporter=@crvouga/atlas/node-test
7
+ */
8
+ export default async function* atlasNodeReporter(source) {
9
+ const checks = [];
10
+ const live = runnerLive('node-test');
11
+ const kinds = new Map();
12
+ try {
13
+ const parents = [];
14
+ for await (const { type, data } of source) {
15
+ const nesting = data.nesting ?? 0;
16
+ const identity = `${data.file ?? ''}|${nesting}|${data.name ?? ''}`;
17
+ if (type === 'test:enqueue' && data.type)
18
+ kinds.set(identity, data.type);
19
+ if (type === 'test:start') {
20
+ parents[nesting] = data.name ?? '';
21
+ parents.length = nesting + 1;
22
+ if (kinds.get(identity) === 'test')
23
+ live.check({ id: [testFile(data.file), ...parents.slice(0, nesting), data.name].filter(Boolean).join(' > '), title: data.name ?? 'test', status: 'running' });
24
+ }
25
+ if (type !== 'test:pass' && type !== 'test:fail')
26
+ continue;
27
+ if (data.details?.type === 'suite' && (type === 'test:pass' || data.details.error?.failureType === 'subtestsFailed'))
28
+ continue;
29
+ const file = testFile(data.file);
30
+ const status = data.skip || data.todo ? 'not-run' : type === 'test:pass' ? 'passed' : 'failed';
31
+ checks.push({ id: [file, ...parents.slice(0, nesting), data.name].filter(Boolean).join(' > '), title: data.name ?? 'test', status,
32
+ detail: data.details?.error?.stack ?? data.details?.error?.message ?? '', measurements: [],
33
+ ...(typeof data.details?.duration_ms === 'number' && Number.isFinite(data.details.duration_ms) && data.details.duration_ms >= 0 ? { durationMs: data.details.duration_ms } : {}) });
34
+ live.check(checks.at(-1));
35
+ }
36
+ checks.forEach(check => live.check(check));
37
+ await finishRunner(live, 'node-test', checks);
38
+ yield `Atlas: uploaded ${checks.length} checks\n`;
39
+ }
40
+ catch (error) {
41
+ await live.finish('interrupted', 'The test event stream ended unexpectedly.').catch(() => { });
42
+ throw error;
43
+ }
44
+ }
@@ -0,0 +1,30 @@
1
+ import type { EvidenceCheck } from '../../reports/src/explorer-types.js';
2
+ /** Reporter options; explicit values override ATLAS_SERVER_URL, ATLAS_PUBLISH_TOKEN, ATLAS_ENVIRONMENT, ATLAS_BRANCH and ATLAS_SHA. */
3
+ export type ReporterOptions = {
4
+ /** Project namespace; defaults to ATLAS_PROJECT. */
5
+ readonly project?: string;
6
+ readonly url?: string;
7
+ readonly token?: string;
8
+ readonly producer?: string;
9
+ readonly environment?: string;
10
+ readonly branch?: string;
11
+ readonly sha?: string;
12
+ readonly timeoutMs?: number;
13
+ readonly rootDir?: string;
14
+ /** Explicit final evidence time; normally generated when publication finishes. */
15
+ readonly recordedAtMs?: number;
16
+ };
17
+ /** A portable check identity: paths relative to the project, using forward slashes on every OS. */
18
+ export declare function testFile(file: string | undefined, root?: string): string;
19
+ /**
20
+ * Upload one completed runner invocation. Unknown outcomes stay not-run, repeated identities are numbered.
21
+ * Throws on missing configuration, timeouts or refused uploads so CI cannot silently lose its evidence.
22
+ * No storage credentials or backend URL are needed by the runner.
23
+ * @example
24
+ * await uploadChecks('mocha', checks, { url: 'https://atlas.example.com', token: process.env.ATLAS_PUBLISH_TOKEN });
25
+ */
26
+ export declare function uploadChecks(runner: string, checks: readonly EvidenceCheck[], options?: ReporterOptions): Promise<void>;
27
+ /** Start the native runner's live feed with the same endpoint and identity as its final report. */
28
+ export declare function runnerLive(runner: string, options?: ReporterOptions, plannedChecks?: number): import('../../reports/src/live-reporter.js').LiveReporter;
29
+ /** Flush live progress, then publish the full final evidence even if the lease was interrupted. Reject either failure. */
30
+ export declare function finishRunner(live: import('../../reports/src/live-reporter.js').LiveReporter, runner: string, checks: readonly EvidenceCheck[], options?: ReporterOptions): Promise<void>;
@@ -0,0 +1,68 @@
1
+ /** @module Publishing native runner results
2
+ * Native reporters send checks to the project's Atlas server. Only the server knows its storage backend.
3
+ */
4
+ import path from 'node:path';
5
+ import { createLiveReporter } from '../../reports/src/live-reporter.js';
6
+ import { uniqueChecks } from '../../reports/src/ingest.js';
7
+ /** A portable check identity: paths relative to the project, using forward slashes on every OS. */
8
+ export function testFile(file, root = process.cwd()) {
9
+ return file ? path.relative(root, path.resolve(root, file)).replaceAll('\\', '/') : '';
10
+ }
11
+ /**
12
+ * Upload one completed runner invocation. Unknown outcomes stay not-run, repeated identities are numbered.
13
+ * Throws on missing configuration, timeouts or refused uploads so CI cannot silently lose its evidence.
14
+ * No storage credentials or backend URL are needed by the runner.
15
+ * @example
16
+ * await uploadChecks('mocha', checks, { url: 'https://atlas.example.com', token: process.env.ATLAS_PUBLISH_TOKEN });
17
+ */
18
+ export async function uploadChecks(runner, checks, options = {}) {
19
+ const url = options.url ?? process.env.ATLAS_SERVER_URL;
20
+ if (!url)
21
+ throw new Error('Atlas reporter: set ATLAS_SERVER_URL to the project Atlas server');
22
+ const target = new URL(url);
23
+ if (!['http:', 'https:'].includes(target.protocol) || target.username || target.password || target.search || target.hash)
24
+ throw new Error('Atlas reporter: use an HTTP(S) server URL without credentials, query or fragment');
25
+ target.pathname = `${target.pathname.replace(/\/$/, '')}/api/checks`;
26
+ const token = options.token ?? process.env.ATLAS_PUBLISH_TOKEN ?? process.env.ATLAS_TOKEN;
27
+ const response = await fetch(target, {
28
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),
29
+ headers: { 'content-type': 'application/json', ...((options.project ?? process.env.ATLAS_PROJECT) ? { 'x-atlas-project': options.project ?? process.env.ATLAS_PROJECT } : {}), ...(token ? { authorization: `Bearer ${token}` } : {}) },
30
+ body: JSON.stringify({ producer: options.producer ?? runner, recordedAtMs: options.recordedAtMs ?? Date.now(),
31
+ environment: options.environment ?? process.env.ATLAS_ENVIRONMENT,
32
+ branch: options.branch ?? process.env.ATLAS_BRANCH, sha: options.sha ?? process.env.ATLAS_SHA,
33
+ checks: uniqueChecks(checks) })
34
+ });
35
+ if (!response.ok) {
36
+ await response.body?.cancel();
37
+ throw new Error(`Atlas reporter: upload refused (HTTP ${response.status})`);
38
+ }
39
+ await response.body?.cancel();
40
+ }
41
+ /** Start the native runner's live feed with the same endpoint and identity as its final report. */
42
+ export function runnerLive(runner, options = {}, plannedChecks) {
43
+ const url = options.url ?? process.env.ATLAS_SERVER_URL;
44
+ if (!url)
45
+ throw new Error('Atlas reporter: set ATLAS_SERVER_URL to the project Atlas server');
46
+ return createLiveReporter({ producer: options.producer ?? runner, plannedChecks, title: `${runner} tests`, environment: options.environment ?? process.env.ATLAS_ENVIRONMENT,
47
+ branch: options.branch ?? process.env.ATLAS_BRANCH, sha: options.sha ?? process.env.ATLAS_SHA }, { url, project: options.project ?? process.env.ATLAS_PROJECT, token: options.token ?? process.env.ATLAS_PUBLISH_TOKEN ?? process.env.ATLAS_TOKEN, timeoutMs: options.timeoutMs });
48
+ }
49
+ /** Flush live progress, then publish the full final evidence even if the lease was interrupted. Reject either failure. */
50
+ export async function finishRunner(live, runner, checks, options = {}) {
51
+ let error;
52
+ const finalChecks = uniqueChecks(checks);
53
+ finalChecks.forEach(check => live.check(check));
54
+ try {
55
+ await live.finish();
56
+ }
57
+ catch (caught) {
58
+ error = caught;
59
+ }
60
+ try {
61
+ await uploadChecks(runner, finalChecks, { ...options, recordedAtMs: Math.max(Date.now(), (live.finishedAtMs ?? 0) + 1) });
62
+ }
63
+ catch (caught) {
64
+ error ??= caught;
65
+ }
66
+ if (error)
67
+ throw error;
68
+ }
@@ -774,6 +774,7 @@ export declare const HelloEventSchema: z.ZodObject<{
774
774
  version: z.ZodString;
775
775
  ready: z.ZodBoolean;
776
776
  time: z.ZodNumber;
777
+ heartbeatMs: z.ZodOptional<z.ZodNumber>;
777
778
  }, z.core.$loose>;
778
779
  /** The data of the `report` event: layers arrived. Enough to say who sent what, without asking. */
779
780
  export declare const ReportEventSchema: z.ZodObject<{
@@ -186,7 +186,7 @@ export const ChecksResultSchema = z.looseObject({
186
186
  checks: z.number().int().nonnegative()
187
187
  });
188
188
  /** The data of the `hello` event: sent first on `/api/events`. */
189
- export const HelloEventSchema = z.looseObject({ version: z.string(), ready: z.boolean(), time: z.number() });
189
+ export const HelloEventSchema = z.looseObject({ version: z.string(), ready: z.boolean(), time: z.number(), heartbeatMs: z.number().positive().optional() });
190
190
  /** The data of the `report` event: layers arrived. Enough to say who sent what, without asking. */
191
191
  export const ReportEventSchema = z.looseObject({
192
192
  version: z.string().describe('The report version after these layers'),
@@ -17,3 +17,5 @@ export * from './layer.js';
17
17
  export * from './server-config.js';
18
18
  export * from './api.js';
19
19
  export * from './cli-results.js';
20
+ export * from './live-run.js';
21
+ export * from './project.js';
@@ -17,3 +17,5 @@ export * from './layer.js';
17
17
  export * from './server-config.js';
18
18
  export * from './api.js';
19
19
  export * from './cli-results.js';
20
+ export * from './live-run.js';
21
+ export * from './project.js';
@@ -0,0 +1,123 @@
1
+ /** @module Live run protocol
2
+ * A live run is a producer lease, separate from immutable report layers. Server receipt times determine liveness.
3
+ * Expiration means interrupted, never passed; completed check results retain the stable layer contract.
4
+ */
5
+ import { z } from 'zod/v4';
6
+ /** Small progress messages: no attachment bytes, bounded step history and text. */
7
+ export declare const LiveCheckSchema: z.ZodObject<{
8
+ id: z.ZodString;
9
+ title: z.ZodString;
10
+ status: z.ZodEnum<{
11
+ passed: "passed";
12
+ failed: "failed";
13
+ running: "running";
14
+ "not-run": "not-run";
15
+ }>;
16
+ detail: z.ZodDefault<z.ZodString>;
17
+ specPath: z.ZodOptional<z.ZodString>;
18
+ durationMs: z.ZodOptional<z.ZodNumber>;
19
+ steps: z.ZodOptional<z.ZodArray<z.ZodObject<{
20
+ name: z.ZodString;
21
+ status: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strip>>>;
23
+ }, z.core.$strip>;
24
+ /** The producer starts one UUID-named run. Identity stays fixed for its lifetime. */
25
+ export declare const LiveRunStartSchema: z.ZodObject<{
26
+ id: z.ZodString;
27
+ producer: z.ZodString;
28
+ title: z.ZodOptional<z.ZodString>;
29
+ environment: z.ZodOptional<z.ZodString>;
30
+ branch: z.ZodOptional<z.ZodString>;
31
+ sha: z.ZodOptional<z.ZodString>;
32
+ plannedChecks: z.ZodOptional<z.ZodNumber>;
33
+ }, z.core.$strip>;
34
+ /** Ordered deltas. A retried sequence must have identical content; terminal runs never accept more progress. */
35
+ export declare const LiveRunUpdateSchema: z.ZodObject<{
36
+ sequence: z.ZodNumber;
37
+ checks: z.ZodDefault<z.ZodArray<z.ZodObject<{
38
+ id: z.ZodString;
39
+ title: z.ZodString;
40
+ status: z.ZodEnum<{
41
+ passed: "passed";
42
+ failed: "failed";
43
+ running: "running";
44
+ "not-run": "not-run";
45
+ }>;
46
+ detail: z.ZodDefault<z.ZodString>;
47
+ specPath: z.ZodOptional<z.ZodString>;
48
+ durationMs: z.ZodOptional<z.ZodNumber>;
49
+ steps: z.ZodOptional<z.ZodArray<z.ZodObject<{
50
+ name: z.ZodString;
51
+ status: z.ZodOptional<z.ZodString>;
52
+ }, z.core.$strip>>>;
53
+ }, z.core.$strip>>>;
54
+ finish: z.ZodOptional<z.ZodEnum<{
55
+ completed: "completed";
56
+ interrupted: "interrupted";
57
+ }>>;
58
+ reason: z.ZodOptional<z.ZodString>;
59
+ }, z.core.$strip>;
60
+ /** The bounded live feed response; completed and interrupted runs remain visible as recent history. */
61
+ export declare const LiveRunSchema: z.ZodObject<{
62
+ id: z.ZodString;
63
+ producer: z.ZodString;
64
+ title: z.ZodOptional<z.ZodString>;
65
+ environment: z.ZodOptional<z.ZodString>;
66
+ branch: z.ZodOptional<z.ZodString>;
67
+ sha: z.ZodOptional<z.ZodString>;
68
+ plannedChecks: z.ZodOptional<z.ZodNumber>;
69
+ status: z.ZodEnum<{
70
+ running: "running";
71
+ completed: "completed";
72
+ interrupted: "interrupted";
73
+ }>;
74
+ startedAtMs: z.ZodNumber;
75
+ lastHeartbeatAtMs: z.ZodNumber;
76
+ lastProgressAtMs: z.ZodNumber;
77
+ finishedAtMs: z.ZodOptional<z.ZodNumber>;
78
+ reason: z.ZodOptional<z.ZodString>;
79
+ sequence: z.ZodNumber;
80
+ completed: z.ZodNumber;
81
+ passed: z.ZodNumber;
82
+ failed: z.ZodNumber;
83
+ notRun: z.ZodNumber;
84
+ running: z.ZodNumber;
85
+ current: z.ZodArray<z.ZodObject<{
86
+ id: z.ZodString;
87
+ title: z.ZodString;
88
+ status: z.ZodEnum<{
89
+ passed: "passed";
90
+ failed: "failed";
91
+ running: "running";
92
+ "not-run": "not-run";
93
+ }>;
94
+ detail: z.ZodDefault<z.ZodString>;
95
+ specPath: z.ZodOptional<z.ZodString>;
96
+ durationMs: z.ZodOptional<z.ZodNumber>;
97
+ steps: z.ZodOptional<z.ZodArray<z.ZodObject<{
98
+ name: z.ZodString;
99
+ status: z.ZodOptional<z.ZodString>;
100
+ }, z.core.$strip>>>;
101
+ }, z.core.$strip>>;
102
+ recent: z.ZodArray<z.ZodObject<{
103
+ id: z.ZodString;
104
+ title: z.ZodString;
105
+ status: z.ZodEnum<{
106
+ passed: "passed";
107
+ failed: "failed";
108
+ running: "running";
109
+ "not-run": "not-run";
110
+ }>;
111
+ detail: z.ZodDefault<z.ZodString>;
112
+ specPath: z.ZodOptional<z.ZodString>;
113
+ durationMs: z.ZodOptional<z.ZodNumber>;
114
+ steps: z.ZodOptional<z.ZodArray<z.ZodObject<{
115
+ name: z.ZodString;
116
+ status: z.ZodOptional<z.ZodString>;
117
+ }, z.core.$strip>>>;
118
+ }, z.core.$strip>>;
119
+ }, z.core.$strip>;
120
+ /** The shape a producer uses for a progress check. */
121
+ export type LiveCheck = z.input<typeof LiveCheckSchema>;
122
+ /** A server's live run snapshot. */
123
+ export type LiveRun = z.output<typeof LiveRunSchema>;
@@ -0,0 +1,30 @@
1
+ /** @module Live run protocol
2
+ * A live run is a producer lease, separate from immutable report layers. Server receipt times determine liveness.
3
+ * Expiration means interrupted, never passed; completed check results retain the stable layer contract.
4
+ */
5
+ import { z } from 'zod/v4';
6
+ /** Small progress messages: no attachment bytes, bounded step history and text. */
7
+ export const LiveCheckSchema = z.object({
8
+ id: z.string().min(1).max(16384), title: z.string().min(1).max(2048),
9
+ status: z.enum(['running', 'passed', 'failed', 'not-run']), detail: z.string().max(8000).default(''),
10
+ specPath: z.string().max(2048).optional(), durationMs: z.number().finite().nonnegative().optional(),
11
+ steps: z.array(z.object({ name: z.string().max(2048), status: z.string().max(100).optional() })).max(20).optional()
12
+ });
13
+ /** The producer starts one UUID-named run. Identity stays fixed for its lifetime. */
14
+ export const LiveRunStartSchema = z.object({
15
+ id: z.string().uuid(), producer: z.string().min(1).max(200), title: z.string().max(2048).optional(),
16
+ environment: z.string().max(200).optional(), branch: z.string().max(500).optional(), sha: z.string().max(200).optional(),
17
+ plannedChecks: z.number().int().nonnegative().max(1_000_000).optional()
18
+ });
19
+ /** Ordered deltas. A retried sequence must have identical content; terminal runs never accept more progress. */
20
+ export const LiveRunUpdateSchema = z.object({
21
+ sequence: z.number().int().positive(), checks: z.array(LiveCheckSchema).max(1000).refine(checks => new Set(checks.map(check => check.id)).size === checks.length, 'Check ids must be unique in a batch').default([]),
22
+ finish: z.enum(['completed', 'interrupted']).optional(), reason: z.string().max(2000).optional()
23
+ });
24
+ /** The bounded live feed response; completed and interrupted runs remain visible as recent history. */
25
+ export const LiveRunSchema = LiveRunStartSchema.extend({
26
+ status: z.enum(['running', 'completed', 'interrupted']), startedAtMs: z.number(), lastHeartbeatAtMs: z.number(), lastProgressAtMs: z.number(),
27
+ finishedAtMs: z.number().optional(), reason: z.string().optional(), sequence: z.number(),
28
+ completed: z.number(), passed: z.number(), failed: z.number(), notRun: z.number(), running: z.number(),
29
+ current: z.array(LiveCheckSchema), recent: z.array(LiveCheckSchema)
30
+ });