@haystackeditor/cli 0.17.1 → 0.18.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.
package/dist/index.js CHANGED
@@ -89,6 +89,9 @@ const headlessLoginCommand = lazy(() => import('./commands/tokens.js'), 'headles
89
89
  const headlessLogoutCommand = lazy(() => import('./commands/tokens.js'), 'headlessLogoutCommand');
90
90
  const listTokensCommand = lazy(() => import('./commands/tokens.js'), 'listTokensCommand');
91
91
  const revokeTokenCommand = lazy(() => import('./commands/tokens.js'), 'revokeTokenCommand');
92
+ const databaseProfileCommand = lazy(() => import('./commands/db-profile.js'), 'databaseProfileCommand');
93
+ const databaseProfileShowCommand = lazy(() => import('./commands/db-profile-show.js'), 'databaseProfileShowCommand');
94
+ const databaseProfileUploadCommand = lazy(() => import('./commands/db-profile-upload.js'), 'databaseProfileUploadCommand');
92
95
  // `mcp` is imported lazily inside its command action (below), NOT at the top level.
93
96
  // It is the only module that pulls in `@modelcontextprotocol/sdk`; keeping it out of
94
97
  // the startup import graph means a missing/broken SDK can never crash core commands
@@ -201,28 +204,44 @@ program
201
204
  .action(statusCommand);
202
205
  const verify = program
203
206
  .command('verify')
204
- .description('Verify the current change with the hosted Haystack fleet')
207
+ .description('Crawl the running app with your current change: where it shows up and what broke')
205
208
  .option('--repo <owner/repo>', 'GitHub repository (default: origin remote)')
206
- .option('--base <ref>', 'Base revision (default: merge base of HEAD and origin/default branch)')
207
- .option('--head <ref>', 'Head revision (default: current HEAD commit)')
208
- .option('--intent-file <path>', 'Optional intent JSON with problem, goal, and intended_outcomes')
209
- .option('--idempotency-key <key>', 'Stable retry key (default: derived from repository, refs, and intent)')
210
209
  .option('--account <login>', 'Use a specific saved Haystack account')
211
- .option('--no-wait', 'Return after the hosted run is queued')
212
- .option('--interval <seconds>', 'Hosted polling interval (default 5)')
213
- .option('--timeout <minutes>', 'Maximum hosted wait (default 35)')
214
- .option('--json', 'Machine-readable run status and results')
210
+ .option('--no-wait', 'Print the crawl\'s current state and return without waiting')
211
+ .option('--interval <seconds>', 'Polling interval while waiting (default 5)')
212
+ .option('--json', 'The crawl as one JSON document on stdout (see `haystack schema verify`)')
215
213
  .addHelpText('after', `
216
- Run inside a git checkout after pushing your current commit. The repository
217
- comes from origin, head from HEAD, and base from git merge-base HEAD with
218
- origin's default branch (origin/HEAD, falling back to origin/main).
219
- Explicit --repo, --base, and --head values override these defaults.
220
- Intent is optional. Hosted status resumes an existing run.
214
+ Run inside a git checkout; nothing needs to be committed or pushed. It captures
215
+ the checkout (committed and uncommitted changes) exactly as the stop hook does,
216
+ with the base at the merge base of HEAD and origin's default branch, and finds
217
+ your crawl of that exact capture: the same code and the same change title (the
218
+ title tells the crawl's judge what the change is). When there is none, or the
219
+ one it finds was cancelled, stopped before finishing or is being cancelled, it
220
+ submits the capture (the service starts the crawl, or runs the stopped one
221
+ again), says so, and follows that crawl.
222
+
223
+ A crawl builds the app with and without your change, starts from the changed
224
+ code, explores outward with both side by side, and double-checks and judges
225
+ every difference. The command waits until the crawl finishes, printing each
226
+ step; Ctrl-C stops waiting, never the crawl. It then prints where your change
227
+ shows up in the app (each changed spot, how far the crawl got with it, and the
228
+ steps that reached it), what the crawl found (bugs first, with the steps to see
229
+ each), and the changed code it never ran.
230
+
231
+ Exit codes:
232
+ 0 the crawl finished (the bugs it found are in the output), or it is
233
+ still running and --no-wait was given
234
+ 1 the command failed, or no crawl could be started for this change
235
+ 2 the crawl ended without finishing: it stopped early or was cancelled
236
+ (a newer stop in the repository replaces an older crawl), or the
237
+ machines it used could not be proven shut down
238
+
239
+ The hosted fleet run of pushed commits is \`haystack verify hosted start\`.
221
240
 
222
241
  Examples:
223
242
  haystack verify
243
+ haystack verify --no-wait
224
244
  haystack verify --json
225
- haystack verify --repo owner/repo --base main --head feature --no-wait
226
245
  haystack verify history owner/repo --limit 20
227
246
  haystack verify hosted status cv_<48 lowercase hex characters> --wait
228
247
  `)
@@ -287,10 +306,8 @@ hostedVerify
287
306
  .command('start')
288
307
  .description('Start an authenticated production verification and wait for its terminal result')
289
308
  .argument('<repository>', 'Exact GitHub owner/repository name')
290
- // `verify` already owns --base/--head for repository verification.
291
- // Re-declaring these as Commander requiredOption values makes the parent
292
- // consume them and the child then reject a valid invocation. Runtime
293
- // validation below accepts the inherited values and still fails closed.
309
+ // Not Commander requiredOption values: runtime validation below accepts
310
+ // values inherited through verifyCommandOptions and still fails closed.
294
311
  .option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
295
312
  .option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
296
313
  .option('--intent-file <path>', 'Optional JSON with exactly problem, goal, and intended_outcomes')
@@ -810,6 +827,55 @@ Example:
810
827
  includeSymbolsFile: options.includeSymbols,
811
828
  includeSymbols: undefined,
812
829
  }), options.json));
830
+ // ─── db ──────────────────────────────────────────────────────────────────────
831
+ const dbProgram = program
832
+ .command('db')
833
+ .description('Profile your own database so Haystack can build a full-size stand-in without personal data');
834
+ const dbProfile = dbProgram
835
+ .command('profile')
836
+ .description('Write a read-only, PII-free profile of a Postgres database; run it inside your own environment')
837
+ .option('--url-env <name>', 'Name of the environment variable holding the postgres:// connection string (never the string itself)')
838
+ .option('--owner-table <schema.table>', 'Table whose rows are the owners (users or organizations); values must be shared by enough of them')
839
+ .option('--schema <name>', 'Schema to profile; repeat for several (default: every non-system schema)', (value, previous = []) => [...previous, value])
840
+ .option('--category-max-distinct <n>', 'Most distinct values a column may have and still count as a category (default 50)')
841
+ .option('--category-min-owners <n>', 'Fewest owners that must share a value, shape or JSON key before it is kept (default 50, minimum 5)')
842
+ .option('--concurrency <n>', 'Database connections used at once (default 2, maximum 8)')
843
+ .option('--statement-timeout <seconds>', 'Longest any one query may run before the profile fails (default 120)')
844
+ .option('--scan-max-mb <mb>', 'Tables larger than this are read through a TABLESAMPLE of about this size (default 256)')
845
+ .option('--out <file>', 'Where to write the profile JSON')
846
+ .addHelpText('after', `
847
+ The profiler only reads. Every query runs in a READ ONLY transaction with a
848
+ statement timeout, and the database computes all counts and patterns: no row
849
+ ever reaches the profiler. A column's values are written only when it has at
850
+ most --category-max-distinct values, is not personal, and each value is shared
851
+ by at least --category-min-owners owners. Review the file before sending it.
852
+
853
+ Examples:
854
+ export DATABASE_URL='postgres://readonly@db.internal:5432/app'
855
+ haystack db profile --url-env DATABASE_URL --owner-table public.users --out profile.json
856
+ haystack db profile show profile.json
857
+ haystack db profile upload profile.json --repo acme/app
858
+ `)
859
+ .action((options) => runPublicCommand(() => databaseProfileCommand(options)));
860
+ dbProfile
861
+ .command('show <file>')
862
+ .description('Print, in plain English, exactly what a profile file would send')
863
+ .action((file) => runPublicCommand(() => databaseProfileShowCommand(file)));
864
+ dbProfile
865
+ .command('upload <file>')
866
+ .description('Send a reviewed profile to Haystack for one repository (and, optionally, one run-map database dependency)')
867
+ .option('--repo <owner/repo>', 'GitHub repository the database belongs to')
868
+ .option('--dependency <id>', 'Run-map database dependency id the profile describes. Omit it when the run map has one Postgres database: the profile is then used for it, whatever its id')
869
+ .option('--account <login>', 'Use a specific saved Haystack account')
870
+ .addHelpText('after', `
871
+ Without --dependency the profile belongs to the repository, and onboarding
872
+ builds the stand-in database from it when the run map declares exactly one
873
+ Postgres dependency. When it declares none or several, haystack verify hosted
874
+ start prints why no stand-in was used and names the declared ids; upload again with
875
+ --dependency <id> to pick one. A profile uploaded with --dependency is used for
876
+ that dependency; when both apply, the newer upload is used.
877
+ `)
878
+ .action((file, options) => runPublicCommand(() => databaseProfileUploadCommand(file, options)));
813
879
  program
814
880
  .command('dismiss <pr>')
815
881
  .description('Dismiss analysis findings for a PR')
@@ -1205,7 +1271,8 @@ hooks
1205
1271
  .addHelpText('after', `
1206
1272
  Configures your coding CLI to run \`haystack triage --hook\` on session start.
1207
1273
  This shows pending PR analysis results when you open a new terminal session.
1208
- Claude Code also runs \`haystack verify precompute --hook\` when a session stops.
1274
+ Claude Code also runs \`haystack verify precompute --hook\` when a session stops,
1275
+ which starts a crawl of the change in the background (see \`haystack verify\`).
1209
1276
 
1210
1277
  Claude Code: Native SessionStart and Stop hooks (.claude/settings.local.json;
1211
1278
  --shared writes the committed .claude/settings.json)
@@ -1382,6 +1449,91 @@ webhooks
1382
1449
  process.exit(1);
1383
1450
  }
1384
1451
  });
1452
+ // ─── admin ───────────────────────────────────────────────────────────────────
1453
+ // Operator commands. The only credential is the operator's Cloudflare Access
1454
+ // token (`cloudflared access token`); exit 3 means an epoch conflict (409).
1455
+ async function runAdminCommand(action) {
1456
+ try {
1457
+ process.exitCode = await action();
1458
+ }
1459
+ catch (err) {
1460
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
1461
+ process.exitCode = 1;
1462
+ }
1463
+ }
1464
+ const admin = program.command('admin').description('Haystack operator commands (Cloudflare Access)');
1465
+ const fleetPolicy = admin
1466
+ .command('fleet-policy')
1467
+ .description('Fleet policy authority in D1: approved policies per tenant and repository')
1468
+ .addHelpText('after', `
1469
+ <repository> is owner/name or github.com:<id>. The worker URL comes from
1470
+ --worker-url, else $HAYSTACK_ADMIN_WORKER_URL, else the production agent worker.
1471
+ set and revoke need --expected-epoch (read it with show); a stale epoch exits 3.
1472
+ `);
1473
+ const workerUrlOption = '--worker-url <url>';
1474
+ const workerUrlHelp = 'Agent worker origin (default: $HAYSTACK_ADMIN_WORKER_URL or production)';
1475
+ fleetPolicy
1476
+ .command('show <repository>')
1477
+ .description('Show the active policy, epoch and binding of each matching scope')
1478
+ .option('--tenant <id>', 'Tenant id, when several tenants have the repository')
1479
+ .option('--json', 'Print the raw response')
1480
+ .option(workerUrlOption, workerUrlHelp)
1481
+ .action(async (repository, options) => {
1482
+ const { fleetPolicyShowCommand } = await import('./commands/fleet-policy.js');
1483
+ await runAdminCommand(() => fleetPolicyShowCommand(repository, options));
1484
+ });
1485
+ fleetPolicy
1486
+ .command('diff <repository>')
1487
+ .description('Dry-run a set and print what would change, field by field')
1488
+ .requiredOption('--file <policy.json>', 'One VerifyPolicyV1 object')
1489
+ .option('--tenant <id>', 'Tenant id (required for a repository without an installation)')
1490
+ .option(workerUrlOption, workerUrlHelp)
1491
+ .action(async (repository, options) => {
1492
+ const { fleetPolicyDiffCommand } = await import('./commands/fleet-policy.js');
1493
+ await runAdminCommand(() => fleetPolicyDiffCommand(repository, options));
1494
+ });
1495
+ fleetPolicy
1496
+ .command('set <repository>')
1497
+ .description('Write a new policy version at the expected epoch (live GitHub check first)')
1498
+ .requiredOption('--file <policy.json>', 'One VerifyPolicyV1 object')
1499
+ .requiredOption('--expected-epoch <n>', 'The epoch show printed; never filled in for you')
1500
+ .requiredOption('--reason <text>', 'Why, 1 to 500 characters; recorded in the change log')
1501
+ .option('--tenant <id>', 'Tenant id (required for a repository without an installation)')
1502
+ .option(workerUrlOption, workerUrlHelp)
1503
+ .action(async (repository, options) => {
1504
+ const { fleetPolicySetCommand } = await import('./commands/fleet-policy.js');
1505
+ await runAdminCommand(() => fleetPolicySetCommand(repository, options));
1506
+ });
1507
+ fleetPolicy
1508
+ .command('revoke <repository>')
1509
+ .description('Revoke the scope at the expected epoch and stop its running attempts (never contacts GitHub)')
1510
+ .requiredOption('--expected-epoch <n>', 'The epoch show printed; never filled in for you')
1511
+ .requiredOption('--reason <text>', 'Why, 1 to 500 characters; recorded in the change log')
1512
+ .option('--tenant <id>', 'Tenant id, when several tenants have the repository')
1513
+ .option(workerUrlOption, workerUrlHelp)
1514
+ .action(async (repository, options) => {
1515
+ const { fleetPolicyRevokeCommand } = await import('./commands/fleet-policy.js');
1516
+ await runAdminCommand(() => fleetPolicyRevokeCommand(repository, options));
1517
+ });
1518
+ fleetPolicy
1519
+ .command('history <repository>')
1520
+ .description('Print the change log and the versions, newest first')
1521
+ .option('--tenant <id>', 'Tenant id, when several tenants have the repository')
1522
+ .option('--limit <n>', 'At most this many events and versions, 1 to 500 (default 100)')
1523
+ .option(workerUrlOption, workerUrlHelp)
1524
+ .action(async (repository, options) => {
1525
+ const { fleetPolicyHistoryCommand } = await import('./commands/fleet-policy.js');
1526
+ await runAdminCommand(() => fleetPolicyHistoryCommand(repository, options));
1527
+ });
1528
+ fleetPolicy
1529
+ .command('import-kv')
1530
+ .description('One time: import the KV policies and tenant overrides into D1')
1531
+ .option('--dry-run', 'Report what would be imported without writing')
1532
+ .option(workerUrlOption, workerUrlHelp)
1533
+ .action(async (options) => {
1534
+ const { fleetPolicyImportKvCommand } = await import('./commands/fleet-policy.js');
1535
+ await runAdminCommand(() => fleetPolicyImportKvCommand(options));
1536
+ });
1385
1537
  // ─── rules ───────────────────────────────────────────────────────────────────
1386
1538
  const rules = program.command('rules').description('Manage .haystack/pr-rules.yml');
1387
1539
  rules
package/dist/schema.js CHANGED
@@ -20,6 +20,7 @@ export const SCHEMA_VERSIONS = {
20
20
  action: '1.0.0',
21
21
  'cloud-verifier': '1.0.0',
22
22
  'case-batch': '1.0.1',
23
+ verify: '1.0.0',
23
24
  error: '1.0.0',
24
25
  };
25
26
  /** Wrap a payload with the `schema_version` envelope. */
@@ -0,0 +1,31 @@
1
+ import { resolveDiffBaseRef } from './git.js';
2
+ const COMMIT_SHA = /^[0-9a-f]{40}$/;
3
+ export function resolveOriginDefaultBranch(git) {
4
+ // Prints origin/<branch> when the clone records origin/HEAD, nothing when it
5
+ // does not; for-each-ref exits 0 either way.
6
+ const remoteHead = git.run(['for-each-ref', '--format=%(symref:short)', 'refs/remotes/origin/HEAD']);
7
+ if (remoteHead.startsWith('origin/') && remoteHead.length > 'origin/'.length) {
8
+ const branch = remoteHead.slice('origin/'.length);
9
+ git.run(['check-ref-format', `refs/heads/${branch}`]);
10
+ return branch;
11
+ }
12
+ const remoteRefs = git.run(['ls-remote', '--symref', 'origin', 'HEAD'], { remote: true });
13
+ const prefix = 'ref: refs/heads/';
14
+ const heads = remoteRefs.split('\n').map(line => line.split('\t'))
15
+ .filter(fields => fields.length === 2 && fields[1] === 'HEAD' && fields[0].startsWith(prefix))
16
+ .map(fields => fields[0].slice(prefix.length));
17
+ if (heads.length !== 1 || heads[0].length === 0) {
18
+ throw new Error('Could not resolve the origin default branch.');
19
+ }
20
+ git.run(['check-ref-format', `refs/heads/${heads[0]}`]);
21
+ return heads[0];
22
+ }
23
+ /** The base commit of `head` under the rule above. */
24
+ export function resolveVerifyBaseSha(head, git) {
25
+ const defaultBranch = resolveOriginDefaultBranch(git);
26
+ const baseRef = resolveDiffBaseRef(defaultBranch, git.fetchOptions());
27
+ const baseSha = git.run(['merge-base', head, baseRef]);
28
+ if (!COMMIT_SHA.test(baseSha))
29
+ throw new Error('Could not derive the base commit.');
30
+ return baseSha;
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,6 +47,8 @@
47
47
  "chalk": "6.0.0",
48
48
  "commander": "15.0.0",
49
49
  "fast-glob": "3.3.3",
50
+ "pg": "8.23.0",
51
+ "pg-connection-string": "2.14.0",
50
52
  "ws": "8.21.3",
51
53
  "yaml": "2.9.1",
52
54
  "zod": "4.6.5"
@@ -54,6 +56,7 @@
54
56
  "devDependencies": {
55
57
  "@eslint/js": "8.57.1",
56
58
  "@types/node": "22.19.8",
59
+ "@types/pg": "8.23.1",
57
60
  "@types/ws": "8.18.1",
58
61
  "@typescript-eslint/eslint-plugin": "7.18.0",
59
62
  "@typescript-eslint/parser": "7.18.0",
@@ -0,0 +1,217 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://docs.haystackeditor.com/cli/schemas/verify.v1.json",
4
+ "title": "Haystack verify: the crawl of the current change (CRAWL-V1)",
5
+ "description": "Machine-readable output of `haystack verify --json`: the caller's crawl of the exact captured checkout (its code and its change title), as GET /api/agent/cloud-verifier/crawls?repository=&workCommit= or, for a crawl the command just submitted, GET /api/agent/cloud-verifier/crawls/:runId?repository= returns it, or null when no crawl of the capture could be started. The crawl shapes mirror infra/lambda/haystack-design-verifier-shared/crawl_contracts.ts (with amendment 1); that file wins where they differ.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema_version", "crawl"],
9
+ "properties": {
10
+ "schema_version": { "const": "1.0.0" },
11
+ "crawl": {
12
+ "oneOf": [
13
+ { "type": "null" },
14
+ { "$ref": "#/$defs/crawlView" }
15
+ ]
16
+ }
17
+ },
18
+ "$defs": {
19
+ "runId": { "type": "string", "pattern": "^cv_[0-9a-f]{48}$" },
20
+ "commit": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
21
+ "count": { "type": "integer", "minimum": 0 },
22
+ "timestamp": { "type": ["string", "null"], "format": "date-time" },
23
+ "status": { "enum": ["queued", "preparing", "crawling", "draining", "completed", "incomplete", "cancelled"] },
24
+ "stage": { "enum": ["reconcile", "source", "worlds", "blast", "knobs", "prepare", "crawl", "publish", "cleanup"] },
25
+ "spotStage": { "enum": ["resolved", "loaded", "exposed", "activated", "pressed", "observed", "confirmed", "judged"] },
26
+ "reuseValue": { "enum": ["hit", "miss", null] },
27
+ "reuse": {
28
+ "type": "object",
29
+ "required": ["baseWorld", "headWorld", "prepare", "routes"],
30
+ "properties": {
31
+ "baseWorld": { "$ref": "#/$defs/reuseValue" },
32
+ "headWorld": { "$ref": "#/$defs/reuseValue" },
33
+ "prepare": { "$ref": "#/$defs/reuseValue" },
34
+ "routes": { "$ref": "#/$defs/reuseValue" }
35
+ }
36
+ },
37
+ "error": {
38
+ "type": "object",
39
+ "required": ["code", "stage", "message"],
40
+ "properties": {
41
+ "code": {
42
+ "enum": ["source-mismatch", "blast-unavailable", "world-build-failed", "prepare-failed", "crawl-failed",
43
+ "no-capacity", "stalled", "lease-lost", "cancelled"]
44
+ },
45
+ "stage": { "$ref": "#/$defs/stage" },
46
+ "message": { "type": "string" }
47
+ }
48
+ },
49
+ "strings": { "type": "array", "items": { "type": "string" } },
50
+ "finding": {
51
+ "type": "object",
52
+ "required": ["id", "verdict", "why", "start", "steps", "observed", "codeThatRan", "confirmedAtSeconds", "images"],
53
+ "properties": {
54
+ "id": { "type": "string" },
55
+ "verdict": { "enum": ["bug", "not-a-bug", "cant-tell", "being-judged"] },
56
+ "why": { "type": ["string", "null"] },
57
+ "start": { "type": "string", "description": "Where the replay starts." },
58
+ "steps": { "type": "array", "maxItems": 64, "items": { "type": "string" } },
59
+ "observed": { "type": "object" },
60
+ "codeThatRan": { "type": "object" },
61
+ "confirmedAtSeconds": { "type": "number", "minimum": 0 },
62
+ "images": {
63
+ "type": "object",
64
+ "required": ["old", "new"],
65
+ "properties": {
66
+ "old": { "type": ["string", "null"] },
67
+ "new": { "type": ["string", "null"] }
68
+ }
69
+ }
70
+ }
71
+ },
72
+ "spot": {
73
+ "description": "One changed spot (one hunk of a changed file) and how far the crawl got with it: the change's product blast radius.",
74
+ "type": "object",
75
+ "required": ["id", "file", "line", "furthest", "reachedBy", "shows", "findingIds"],
76
+ "properties": {
77
+ "id": { "type": "string", "description": "<file>#<n>: the file's n-th hunk, 1-based." },
78
+ "file": { "type": "string" },
79
+ "line": { "type": "integer", "minimum": 1 },
80
+ "furthest": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/spotStage" }] },
81
+ "reachedBy": {
82
+ "oneOf": [
83
+ { "type": "null" },
84
+ {
85
+ "type": "object",
86
+ "required": ["stage", "start", "steps", "page"],
87
+ "properties": {
88
+ "stage": { "$ref": "#/$defs/spotStage" },
89
+ "start": { "type": "string" },
90
+ "steps": { "$ref": "#/$defs/strings" },
91
+ "page": { "type": ["string", "null"] }
92
+ }
93
+ }
94
+ ]
95
+ },
96
+ "shows": { "type": "array", "maxItems": 5, "items": { "type": "string" } },
97
+ "findingIds": { "$ref": "#/$defs/strings" }
98
+ }
99
+ },
100
+ "manifest": {
101
+ "description": "Present once the crawl is terminal and published.",
102
+ "type": "object",
103
+ "required": ["schemaVersion", "runId", "attemptId", "repository", "baseCommit", "workCommit", "treeSha",
104
+ "changeTitle", "status", "worlds", "reuse", "timings", "bugs", "findings", "reach", "notFinishedInTime", "error"],
105
+ "properties": {
106
+ "schemaVersion": { "const": 1 },
107
+ "runId": { "$ref": "#/$defs/runId" },
108
+ "attemptId": { "type": "string" },
109
+ "repository": { "type": "string" },
110
+ "baseCommit": { "$ref": "#/$defs/commit" },
111
+ "workCommit": { "$ref": "#/$defs/commit" },
112
+ "treeSha": { "$ref": "#/$defs/commit" },
113
+ "changeTitle": { "type": "string", "maxLength": 200 },
114
+ "status": { "enum": ["completed", "incomplete", "cancelled"] },
115
+ "worlds": {
116
+ "oneOf": [
117
+ { "type": "null" },
118
+ {
119
+ "type": "object",
120
+ "required": ["base", "head"],
121
+ "properties": { "base": { "type": "string" }, "head": { "type": "string" } }
122
+ }
123
+ ]
124
+ },
125
+ "reuse": { "$ref": "#/$defs/reuse" },
126
+ "timings": {
127
+ "description": "Milliseconds per stage, as measured.",
128
+ "type": "object",
129
+ "additionalProperties": { "type": "number", "minimum": 0 }
130
+ },
131
+ "bugs": { "$ref": "#/$defs/count" },
132
+ "findings": { "type": "array", "maxItems": 50, "items": { "$ref": "#/$defs/finding" } },
133
+ "reach": {
134
+ "type": "object",
135
+ "required": ["spots", "unreachedChangedFiles", "blast"],
136
+ "properties": {
137
+ "spots": { "type": "array", "items": { "$ref": "#/$defs/spot" } },
138
+ "unreachedChangedFiles": { "$ref": "#/$defs/strings" },
139
+ "blast": {
140
+ "oneOf": [
141
+ { "type": "null" },
142
+ {
143
+ "type": "object",
144
+ "required": ["functions", "ran", "unreached", "nearestUnreached"],
145
+ "properties": {
146
+ "functions": { "$ref": "#/$defs/count" },
147
+ "ran": { "$ref": "#/$defs/count" },
148
+ "unreached": { "$ref": "#/$defs/count" },
149
+ "nearestUnreached": { "$ref": "#/$defs/strings" }
150
+ }
151
+ }
152
+ ]
153
+ }
154
+ }
155
+ },
156
+ "notFinishedInTime": { "$ref": "#/$defs/count" },
157
+ "error": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/error" }] }
158
+ }
159
+ },
160
+ "crawlView": {
161
+ "type": "object",
162
+ "required": ["runId", "repository", "baseCommit", "workCommit", "treeSha", "changeTitle", "observedAt",
163
+ "createdAt", "updatedAt", "supersededBy", "cancelRequested", "manifest", "status", "stage", "clocks",
164
+ "reuse", "worlds", "totals"],
165
+ "properties": {
166
+ "runId": { "$ref": "#/$defs/runId" },
167
+ "repository": { "type": "string", "pattern": "^[A-Za-z0-9_.-]{1,100}/[A-Za-z0-9_.-]{1,100}$" },
168
+ "baseCommit": { "$ref": "#/$defs/commit" },
169
+ "workCommit": { "$ref": "#/$defs/commit" },
170
+ "treeSha": { "$ref": "#/$defs/commit" },
171
+ "changeTitle": { "type": "string", "maxLength": 200 },
172
+ "observedAt": { "$ref": "#/$defs/count" },
173
+ "createdAt": { "type": "string", "format": "date-time" },
174
+ "updatedAt": { "type": "string", "format": "date-time" },
175
+ "supersededBy": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/runId" }] },
176
+ "cancelRequested": { "type": "boolean" },
177
+ "status": { "$ref": "#/$defs/status" },
178
+ "stage": { "$ref": "#/$defs/stage" },
179
+ "clocks": {
180
+ "type": "object",
181
+ "required": ["startedAt", "stageStartedAt", "crawlStartedAt", "finishedAt", "crawlBudgetMs"],
182
+ "properties": {
183
+ "startedAt": { "$ref": "#/$defs/timestamp" },
184
+ "stageStartedAt": { "$ref": "#/$defs/timestamp" },
185
+ "crawlStartedAt": { "$ref": "#/$defs/timestamp" },
186
+ "finishedAt": { "$ref": "#/$defs/timestamp" },
187
+ "crawlBudgetMs": { "$ref": "#/$defs/count" }
188
+ }
189
+ },
190
+ "reuse": { "$ref": "#/$defs/reuse" },
191
+ "worlds": {
192
+ "type": "object",
193
+ "required": ["base", "head"],
194
+ "properties": {
195
+ "base": { "type": ["string", "null"] },
196
+ "head": { "type": ["string", "null"] }
197
+ }
198
+ },
199
+ "totals": {
200
+ "type": "object",
201
+ "required": ["clicks", "differing", "confirmed", "bugs", "vmsLaunched", "cleanupProven", "cleanupUnproven"],
202
+ "properties": {
203
+ "clicks": { "$ref": "#/$defs/count" },
204
+ "differing": { "$ref": "#/$defs/count" },
205
+ "confirmed": { "$ref": "#/$defs/count" },
206
+ "bugs": { "$ref": "#/$defs/count" },
207
+ "vmsLaunched": { "$ref": "#/$defs/count" },
208
+ "cleanupProven": { "$ref": "#/$defs/count" },
209
+ "cleanupUnproven": { "$ref": "#/$defs/count" }
210
+ }
211
+ },
212
+ "error": { "$ref": "#/$defs/error" },
213
+ "manifest": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/manifest" }] }
214
+ }
215
+ }
216
+ }
217
+ }
@@ -1 +0,0 @@
1
- export {};