@haystackeditor/cli 0.17.1 → 0.19.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,50 @@ 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('--minutes <n>', 'How long the crawl may take, 1-30 (default: .haystack.json crawl.minutes, else 3)')
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 it can within its time: 3 minutes unless the repository's
226
+ .haystack.json sets crawl.minutes or --minutes says otherwise (1 to 30).
227
+ Differences it had no time to check are listed as not finished in time. The
228
+ command waits for the crawl, printing each step and each bug the moment the
229
+ crawl finds it; it finishes when the crawl answers, as soon as its time is up,
230
+ without waiting for the crawl's machines to shut down. Ctrl-C stops waiting,
231
+ never the crawl. It then prints where your change shows up in the app (each
232
+ changed spot, how far the crawl got with it, and the steps that reached it),
233
+ what the crawl found (bugs first, with the steps to see each), and the changed
234
+ code it never ran.
235
+
236
+ Exit codes:
237
+ 0 the crawl answered or finished (the bugs it found are in the output), or
238
+ it is still running and --no-wait was given
239
+ 1 the command failed, or no crawl could be started for this change
240
+ 2 the crawl ended without finishing: it stopped early or was cancelled
241
+ (a newer stop in the repository replaces an older crawl), or the
242
+ machines it used could not be proven shut down
243
+
244
+ The hosted fleet run of pushed commits is \`haystack verify hosted start\`.
221
245
 
222
246
  Examples:
223
247
  haystack verify
248
+ haystack verify --no-wait
249
+ haystack verify --minutes 10
224
250
  haystack verify --json
225
- haystack verify --repo owner/repo --base main --head feature --no-wait
226
251
  haystack verify history owner/repo --limit 20
227
252
  haystack verify hosted status cv_<48 lowercase hex characters> --wait
228
253
  `)
@@ -287,10 +312,8 @@ hostedVerify
287
312
  .command('start')
288
313
  .description('Start an authenticated production verification and wait for its terminal result')
289
314
  .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.
315
+ // Not Commander requiredOption values: runtime validation below accepts
316
+ // values inherited through verifyCommandOptions and still fails closed.
294
317
  .option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
295
318
  .option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
296
319
  .option('--intent-file <path>', 'Optional JSON with exactly problem, goal, and intended_outcomes')
@@ -810,6 +833,55 @@ Example:
810
833
  includeSymbolsFile: options.includeSymbols,
811
834
  includeSymbols: undefined,
812
835
  }), options.json));
836
+ // ─── db ──────────────────────────────────────────────────────────────────────
837
+ const dbProgram = program
838
+ .command('db')
839
+ .description('Profile your own database so Haystack can build a full-size stand-in without personal data');
840
+ const dbProfile = dbProgram
841
+ .command('profile')
842
+ .description('Write a read-only, PII-free profile of a Postgres database; run it inside your own environment')
843
+ .option('--url-env <name>', 'Name of the environment variable holding the postgres:// connection string (never the string itself)')
844
+ .option('--owner-table <schema.table>', 'Table whose rows are the owners (users or organizations); values must be shared by enough of them')
845
+ .option('--schema <name>', 'Schema to profile; repeat for several (default: every non-system schema)', (value, previous = []) => [...previous, value])
846
+ .option('--category-max-distinct <n>', 'Most distinct values a column may have and still count as a category (default 50)')
847
+ .option('--category-min-owners <n>', 'Fewest owners that must share a value, shape or JSON key before it is kept (default 50, minimum 5)')
848
+ .option('--concurrency <n>', 'Database connections used at once (default 2, maximum 8)')
849
+ .option('--statement-timeout <seconds>', 'Longest any one query may run before the profile fails (default 120)')
850
+ .option('--scan-max-mb <mb>', 'Tables larger than this are read through a TABLESAMPLE of about this size (default 256)')
851
+ .option('--out <file>', 'Where to write the profile JSON')
852
+ .addHelpText('after', `
853
+ The profiler only reads. Every query runs in a READ ONLY transaction with a
854
+ statement timeout, and the database computes all counts and patterns: no row
855
+ ever reaches the profiler. A column's values are written only when it has at
856
+ most --category-max-distinct values, is not personal, and each value is shared
857
+ by at least --category-min-owners owners. Review the file before sending it.
858
+
859
+ Examples:
860
+ export DATABASE_URL='postgres://readonly@db.internal:5432/app'
861
+ haystack db profile --url-env DATABASE_URL --owner-table public.users --out profile.json
862
+ haystack db profile show profile.json
863
+ haystack db profile upload profile.json --repo acme/app
864
+ `)
865
+ .action((options) => runPublicCommand(() => databaseProfileCommand(options)));
866
+ dbProfile
867
+ .command('show <file>')
868
+ .description('Print, in plain English, exactly what a profile file would send')
869
+ .action((file) => runPublicCommand(() => databaseProfileShowCommand(file)));
870
+ dbProfile
871
+ .command('upload <file>')
872
+ .description('Send a reviewed profile to Haystack for one repository (and, optionally, one run-map database dependency)')
873
+ .option('--repo <owner/repo>', 'GitHub repository the database belongs to')
874
+ .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')
875
+ .option('--account <login>', 'Use a specific saved Haystack account')
876
+ .addHelpText('after', `
877
+ Without --dependency the profile belongs to the repository, and onboarding
878
+ builds the stand-in database from it when the run map declares exactly one
879
+ Postgres dependency. When it declares none or several, haystack verify hosted
880
+ start prints why no stand-in was used and names the declared ids; upload again with
881
+ --dependency <id> to pick one. A profile uploaded with --dependency is used for
882
+ that dependency; when both apply, the newer upload is used.
883
+ `)
884
+ .action((file, options) => runPublicCommand(() => databaseProfileUploadCommand(file, options)));
813
885
  program
814
886
  .command('dismiss <pr>')
815
887
  .description('Dismiss analysis findings for a PR')
@@ -1205,7 +1277,8 @@ hooks
1205
1277
  .addHelpText('after', `
1206
1278
  Configures your coding CLI to run \`haystack triage --hook\` on session start.
1207
1279
  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.
1280
+ Claude Code also runs \`haystack verify precompute --hook\` when a session stops,
1281
+ which starts a crawl of the change in the background (see \`haystack verify\`).
1209
1282
 
1210
1283
  Claude Code: Native SessionStart and Stop hooks (.claude/settings.local.json;
1211
1284
  --shared writes the committed .claude/settings.json)
@@ -1382,6 +1455,91 @@ webhooks
1382
1455
  process.exit(1);
1383
1456
  }
1384
1457
  });
1458
+ // ─── admin ───────────────────────────────────────────────────────────────────
1459
+ // Operator commands. The only credential is the operator's Cloudflare Access
1460
+ // token (`cloudflared access token`); exit 3 means an epoch conflict (409).
1461
+ async function runAdminCommand(action) {
1462
+ try {
1463
+ process.exitCode = await action();
1464
+ }
1465
+ catch (err) {
1466
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
1467
+ process.exitCode = 1;
1468
+ }
1469
+ }
1470
+ const admin = program.command('admin').description('Haystack operator commands (Cloudflare Access)');
1471
+ const fleetPolicy = admin
1472
+ .command('fleet-policy')
1473
+ .description('Fleet policy authority in D1: approved policies per tenant and repository')
1474
+ .addHelpText('after', `
1475
+ <repository> is owner/name or github.com:<id>. The worker URL comes from
1476
+ --worker-url, else $HAYSTACK_ADMIN_WORKER_URL, else the production agent worker.
1477
+ set and revoke need --expected-epoch (read it with show); a stale epoch exits 3.
1478
+ `);
1479
+ const workerUrlOption = '--worker-url <url>';
1480
+ const workerUrlHelp = 'Agent worker origin (default: $HAYSTACK_ADMIN_WORKER_URL or production)';
1481
+ fleetPolicy
1482
+ .command('show <repository>')
1483
+ .description('Show the active policy, epoch and binding of each matching scope')
1484
+ .option('--tenant <id>', 'Tenant id, when several tenants have the repository')
1485
+ .option('--json', 'Print the raw response')
1486
+ .option(workerUrlOption, workerUrlHelp)
1487
+ .action(async (repository, options) => {
1488
+ const { fleetPolicyShowCommand } = await import('./commands/fleet-policy.js');
1489
+ await runAdminCommand(() => fleetPolicyShowCommand(repository, options));
1490
+ });
1491
+ fleetPolicy
1492
+ .command('diff <repository>')
1493
+ .description('Dry-run a set and print what would change, field by field')
1494
+ .requiredOption('--file <policy.json>', 'One VerifyPolicyV1 object')
1495
+ .option('--tenant <id>', 'Tenant id (required for a repository without an installation)')
1496
+ .option(workerUrlOption, workerUrlHelp)
1497
+ .action(async (repository, options) => {
1498
+ const { fleetPolicyDiffCommand } = await import('./commands/fleet-policy.js');
1499
+ await runAdminCommand(() => fleetPolicyDiffCommand(repository, options));
1500
+ });
1501
+ fleetPolicy
1502
+ .command('set <repository>')
1503
+ .description('Write a new policy version at the expected epoch (live GitHub check first)')
1504
+ .requiredOption('--file <policy.json>', 'One VerifyPolicyV1 object')
1505
+ .requiredOption('--expected-epoch <n>', 'The epoch show printed; never filled in for you')
1506
+ .requiredOption('--reason <text>', 'Why, 1 to 500 characters; recorded in the change log')
1507
+ .option('--tenant <id>', 'Tenant id (required for a repository without an installation)')
1508
+ .option(workerUrlOption, workerUrlHelp)
1509
+ .action(async (repository, options) => {
1510
+ const { fleetPolicySetCommand } = await import('./commands/fleet-policy.js');
1511
+ await runAdminCommand(() => fleetPolicySetCommand(repository, options));
1512
+ });
1513
+ fleetPolicy
1514
+ .command('revoke <repository>')
1515
+ .description('Revoke the scope at the expected epoch and stop its running attempts (never contacts GitHub)')
1516
+ .requiredOption('--expected-epoch <n>', 'The epoch show printed; never filled in for you')
1517
+ .requiredOption('--reason <text>', 'Why, 1 to 500 characters; recorded in the change log')
1518
+ .option('--tenant <id>', 'Tenant id, when several tenants have the repository')
1519
+ .option(workerUrlOption, workerUrlHelp)
1520
+ .action(async (repository, options) => {
1521
+ const { fleetPolicyRevokeCommand } = await import('./commands/fleet-policy.js');
1522
+ await runAdminCommand(() => fleetPolicyRevokeCommand(repository, options));
1523
+ });
1524
+ fleetPolicy
1525
+ .command('history <repository>')
1526
+ .description('Print the change log and the versions, newest first')
1527
+ .option('--tenant <id>', 'Tenant id, when several tenants have the repository')
1528
+ .option('--limit <n>', 'At most this many events and versions, 1 to 500 (default 100)')
1529
+ .option(workerUrlOption, workerUrlHelp)
1530
+ .action(async (repository, options) => {
1531
+ const { fleetPolicyHistoryCommand } = await import('./commands/fleet-policy.js');
1532
+ await runAdminCommand(() => fleetPolicyHistoryCommand(repository, options));
1533
+ });
1534
+ fleetPolicy
1535
+ .command('import-kv')
1536
+ .description('One time: import the KV policies and tenant overrides into D1')
1537
+ .option('--dry-run', 'Report what would be imported without writing')
1538
+ .option(workerUrlOption, workerUrlHelp)
1539
+ .action(async (options) => {
1540
+ const { fleetPolicyImportKvCommand } = await import('./commands/fleet-policy.js');
1541
+ await runAdminCommand(() => fleetPolicyImportKvCommand(options));
1542
+ });
1385
1543
  // ─── rules ───────────────────────────────────────────────────────────────────
1386
1544
  const rules = program.command('rules').description('Manage .haystack/pr-rules.yml');
1387
1545
  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. */
package/dist/types.js CHANGED
@@ -294,6 +294,9 @@ const PreferencesSchema = z.object({
294
294
  export const HaystackConfigSchema = z.object({
295
295
  /** Config version (must be "1") */
296
296
  version: z.literal('1'),
297
+ /** Hosted crawls of this repository (`haystack verify`, the stop hook): how long one may take, whole minutes from 1 to 30.
298
+ * Absent: the service's default (3 minutes). `haystack verify --minutes` overrides it for one run. */
299
+ crawl: z.object({ minutes: z.number().int().min(1).max(30).optional() }).strict().optional(),
297
300
  /** Project name (for display) */
298
301
  name: z.string().optional(),
299
302
  /** Repository-level Haystack behavior */
@@ -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.19.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,237 @@
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 amendments 1, 8 and 9); 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
+ "results": {
101
+ "description": "The published results' shape, shared by the sealed manifest and an answer; each sets its own status.",
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
+ "worlds": {
115
+ "oneOf": [
116
+ { "type": "null" },
117
+ {
118
+ "type": "object",
119
+ "required": ["base", "head"],
120
+ "properties": { "base": { "type": "string" }, "head": { "type": "string" } }
121
+ }
122
+ ]
123
+ },
124
+ "reuse": { "$ref": "#/$defs/reuse" },
125
+ "timings": {
126
+ "description": "Milliseconds per stage, as measured.",
127
+ "type": "object",
128
+ "additionalProperties": { "type": "number", "minimum": 0 }
129
+ },
130
+ "bugs": { "$ref": "#/$defs/count" },
131
+ "findings": { "type": "array", "maxItems": 50, "items": { "$ref": "#/$defs/finding" } },
132
+ "reach": {
133
+ "type": "object",
134
+ "required": ["spots", "unreachedChangedFiles", "blast"],
135
+ "properties": {
136
+ "spots": { "type": "array", "items": { "$ref": "#/$defs/spot" } },
137
+ "unreachedChangedFiles": { "$ref": "#/$defs/strings" },
138
+ "blast": {
139
+ "oneOf": [
140
+ { "type": "null" },
141
+ {
142
+ "type": "object",
143
+ "required": ["functions", "ran", "unreached", "nearestUnreached"],
144
+ "properties": {
145
+ "functions": { "$ref": "#/$defs/count" },
146
+ "ran": { "$ref": "#/$defs/count" },
147
+ "unreached": { "$ref": "#/$defs/count" },
148
+ "nearestUnreached": { "$ref": "#/$defs/strings" }
149
+ }
150
+ }
151
+ ]
152
+ }
153
+ }
154
+ },
155
+ "notFinishedInTime": { "$ref": "#/$defs/count" },
156
+ "error": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/error" }] }
157
+ }
158
+ },
159
+ "manifest": {
160
+ "description": "Present once the crawl is terminal and published.",
161
+ "allOf": [{ "$ref": "#/$defs/results" }],
162
+ "properties": {
163
+ "status": { "enum": ["completed", "incomplete", "cancelled"] }
164
+ }
165
+ },
166
+ "answer": {
167
+ "description": "CRAWL-V1 amendment 9: the current attempt's latest answer while no sealed manifest exists: what the crawl found so far (exploring), or its answer once its time is up (answered), with the same findings and reach the sealed manifest will carry. Its findings carry no images.",
168
+ "allOf": [{ "$ref": "#/$defs/results" }],
169
+ "required": ["n", "atSeconds"],
170
+ "properties": {
171
+ "status": { "enum": ["exploring", "answered"] },
172
+ "n": { "type": "integer", "minimum": 1, "description": "The attempt's answers are numbered from 1." },
173
+ "atSeconds": { "type": "integer", "minimum": 0, "description": "How far into the crawl stage it was written." }
174
+ }
175
+ },
176
+ "crawlView": {
177
+ "type": "object",
178
+ "required": ["runId", "repository", "baseCommit", "workCommit", "treeSha", "changeTitle", "budgetMs", "observedAt",
179
+ "createdAt", "updatedAt", "supersededBy", "cancelRequested", "manifest", "answer", "status", "stage", "clocks",
180
+ "reuse", "worlds", "totals"],
181
+ "properties": {
182
+ "runId": { "$ref": "#/$defs/runId" },
183
+ "repository": { "type": "string", "pattern": "^[A-Za-z0-9_.-]{1,100}/[A-Za-z0-9_.-]{1,100}$" },
184
+ "baseCommit": { "$ref": "#/$defs/commit" },
185
+ "workCommit": { "$ref": "#/$defs/commit" },
186
+ "treeSha": { "$ref": "#/$defs/commit" },
187
+ "changeTitle": { "type": "string", "maxLength": 200 },
188
+ "budgetMs": { "description": "The time the crawl was asked for (ms), or null for the service's default.",
189
+ "oneOf": [{ "type": "null" }, { "type": "integer", "minimum": 60000, "maximum": 1800000 }] },
190
+ "observedAt": { "$ref": "#/$defs/count" },
191
+ "createdAt": { "type": "string", "format": "date-time" },
192
+ "updatedAt": { "type": "string", "format": "date-time" },
193
+ "supersededBy": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/runId" }] },
194
+ "cancelRequested": { "type": "boolean" },
195
+ "status": { "$ref": "#/$defs/status" },
196
+ "stage": { "$ref": "#/$defs/stage" },
197
+ "clocks": {
198
+ "type": "object",
199
+ "required": ["startedAt", "stageStartedAt", "crawlStartedAt", "finishedAt", "crawlBudgetMs"],
200
+ "properties": {
201
+ "startedAt": { "$ref": "#/$defs/timestamp" },
202
+ "stageStartedAt": { "$ref": "#/$defs/timestamp" },
203
+ "crawlStartedAt": { "$ref": "#/$defs/timestamp" },
204
+ "finishedAt": { "$ref": "#/$defs/timestamp" },
205
+ "crawlBudgetMs": { "$ref": "#/$defs/count" }
206
+ }
207
+ },
208
+ "reuse": { "$ref": "#/$defs/reuse" },
209
+ "worlds": {
210
+ "type": "object",
211
+ "required": ["base", "head"],
212
+ "properties": {
213
+ "base": { "type": ["string", "null"] },
214
+ "head": { "type": ["string", "null"] }
215
+ }
216
+ },
217
+ "totals": {
218
+ "type": "object",
219
+ "required": ["clicks", "differing", "confirmed", "bugs", "vmsLaunched", "cleanupProven", "cleanupUnproven"],
220
+ "properties": {
221
+ "clicks": { "$ref": "#/$defs/count" },
222
+ "differing": { "$ref": "#/$defs/count" },
223
+ "confirmed": { "$ref": "#/$defs/count" },
224
+ "bugs": { "$ref": "#/$defs/count" },
225
+ "vmsLaunched": { "$ref": "#/$defs/count" },
226
+ "cleanupProven": { "$ref": "#/$defs/count" },
227
+ "cleanupUnproven": { "$ref": "#/$defs/count" }
228
+ }
229
+ },
230
+ "error": { "$ref": "#/$defs/error" },
231
+ "manifest": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/manifest" }] },
232
+ "answer": { "description": "Null once the sealed manifest exists, and before the crawl's first answer.",
233
+ "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/answer" }] }
234
+ }
235
+ }
236
+ }
237
+ }
@@ -1 +0,0 @@
1
- export {};