@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/README.md +206 -24
- package/dist/commands/case-batch-contract.js +2 -2
- package/dist/commands/case-batch.js +7 -17
- package/dist/commands/crawl-contract.js +3 -0
- package/dist/commands/db-profile-contract.js +419 -0
- package/dist/commands/db-profile-database.js +289 -0
- package/dist/commands/db-profile-distinct.js +97 -0
- package/dist/commands/db-profile-owners.js +94 -0
- package/dist/commands/db-profile-privacy.js +450 -0
- package/dist/commands/db-profile-show.js +218 -0
- package/dist/commands/db-profile-sql-literals.js +140 -0
- package/dist/commands/db-profile-sql.js +362 -0
- package/dist/commands/db-profile-upload.js +88 -0
- package/dist/commands/db-profile.js +1020 -0
- package/dist/commands/fleet-policy-contract.js +28 -0
- package/dist/commands/fleet-policy.js +305 -0
- package/dist/commands/precompute-delivery-worker.js +17 -3
- package/dist/commands/precompute-delivery.js +16 -6
- package/dist/commands/verify-hosted.js +20 -0
- package/dist/commands/verify-precompute.js +140 -95
- package/dist/commands/verify.js +523 -52
- package/dist/index.js +172 -20
- package/dist/schema.js +1 -0
- package/dist/utils/verify-base.js +31 -0
- package/package.json +4 -1
- package/schemas/verify.v1.json +217 -0
- package/dist/commands/combination-search-hook-contract.js +0 -1
- package/dist/commands/combination-search-hook.js +0 -139
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('
|
|
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', '
|
|
212
|
-
.option('--interval <seconds>', '
|
|
213
|
-
.option('--
|
|
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
|
|
217
|
-
|
|
218
|
-
origin's default branch
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
//
|
|
291
|
-
//
|
|
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
|
@@ -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.
|
|
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 {};
|