graftfork 0.2.0 → 0.3.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 +17 -1
- package/graft.mjs +117 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,8 +17,24 @@ graft why # why the copy stayed, and what to do next
|
|
|
17
17
|
graft pull # fetch the latest move as graft/<release>; your branch is untouched
|
|
18
18
|
graft upstream "fix the parser" # Graft builds that one change alone on your release and runs your tests;
|
|
19
19
|
# this fetches graft/upstream/<change>. Nothing is pushed: you open the PR
|
|
20
|
+
graft upstream-status <build> # an upstream build's status, tests and links
|
|
20
21
|
graft upstream-pr <build> <url> # the pull request you opened; Graft tells you when it's merged
|
|
22
|
+
graft upgrade <dependency> # upgrade one dependency (npm or Python), delivered only if the tests pass
|
|
23
|
+
graft upgrades # your upgrades and what their tests said
|
|
24
|
+
graft check <base> <head> --wait # check a change against the project's own tests: exit 0 pass, 1 fail, 2 couldn't run
|
|
25
|
+
graft checks # your recent change checks
|
|
26
|
+
graft verify-record <file> # check a signed run record or agent change record; no token needed
|
|
27
|
+
graft agent-record <org> # an organization's signed agent change record, as JSON
|
|
28
|
+
graft regen connect <repo> ... # connect a spec (a duramen record); Graft rebuilds its implementation blind
|
|
29
|
+
graft regen run <spec> # rebuild it now
|
|
30
|
+
graft regen status [id] # your specs, a spec's rebuilds, or one rebuild and how to take it
|
|
31
|
+
graft regen report <spec> # the spec's grade and why, before anything is spent
|
|
21
32
|
```
|
|
22
33
|
|
|
23
|
-
Also `graft whoami`, `graft status` and `graft usage`; `--json` prints the API's answers as they are
|
|
34
|
+
Also `graft whoami`, `graft status` and `graft usage`; `--json` prints the API's answers as they are, and
|
|
35
|
+
`graft --help` lists every command with its options.
|
|
36
|
+
|
|
37
|
+
Which version has what: 0.1.0 had copies, connect, sync, status, why, pull, usage, whoami and verify-record; 0.1.1
|
|
38
|
+
added agent-record; 0.2.0 added check(s), upgrade(s) and upstream, upstream-status and upstream-pr; 0.3.0 adds
|
|
39
|
+
regen connect, run, status and report. API docs:
|
|
24
40
|
https://graftfork.com/docs/api. Support by email: support@graftfork.com. Apache-2.0.
|
package/graft.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/client/cli.ts
|
|
4
4
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
|
|
8
8
|
// src/pull.ts
|
|
@@ -30,7 +30,7 @@ async function pull(o) {
|
|
|
30
30
|
let id = o.copy && /^[A-Za-z0-9_-]{8,64}$/.test(o.copy) && !o.copy.includes("/") ? o.copy : null;
|
|
31
31
|
if (!id) {
|
|
32
32
|
const repo = o.copy ?? repoOfRemote(git(o.cwd, ["remote", "get-url", "origin"]).stdout ?? "");
|
|
33
|
-
if (!repo) return { ok: false, message: "Couldn't tell which copy this is:
|
|
33
|
+
if (!repo) return { ok: false, message: "Couldn't tell which copy this is: give its id or owner/name (graft pull <copy>, or --copy <copy>), or run this inside its clone." };
|
|
34
34
|
const { copies } = await get("/api/v1/copies");
|
|
35
35
|
const c = copies.find((x) => String(x.repo).toLowerCase() === repo.toLowerCase());
|
|
36
36
|
if (!c) return { ok: false, message: `${repo} isn't one of your copies on Graft.` };
|
|
@@ -132,6 +132,7 @@ var USAGE = `graft: the command line for graftfork.com (a thin client; syncs run
|
|
|
132
132
|
graft connect <owner/name> connect a repository (Graft's GitHub app must be installed on it)
|
|
133
133
|
[--branch <b> --upstream <owner/name or URL> --base <release tag>] [--tags <pattern>]
|
|
134
134
|
graft sync [copy] start a sync now
|
|
135
|
+
[--force] again, though the last sync of this release stopped at the agent's per-sync budget
|
|
135
136
|
graft status [copy] a copy and its latest runs
|
|
136
137
|
graft why [copy] why the copy stayed, and what to do next
|
|
137
138
|
graft pull [copy] fetch the latest move as graft/<release> in your clone
|
|
@@ -144,16 +145,18 @@ var USAGE = `graft: the command line for graftfork.com (a thin client; syncs run
|
|
|
144
145
|
graft upstream-pr <build> <url> the pull request you opened upstream; Graft checks it until it's merged
|
|
145
146
|
graft usage this month's syncs and agent work
|
|
146
147
|
graft verify-record <file> check a run's signed record (from /app or GET /api/v1/runs/{id}/record),
|
|
147
|
-
or an organization's agent change record
|
|
148
|
+
a rebuild's, or an organization's agent change record
|
|
149
|
+
graft verify-record <id> fetch a rebuild's (or a sync's) signed record by its id, with the token, and check it
|
|
148
150
|
graft agent-record <org> an organization's signed agent change record, as JSON
|
|
149
151
|
[--from <YYYY-MM-DD> --to <YYYY-MM-DD>] (save it: graft agent-record <org> > record.json)
|
|
150
152
|
graft check <base> <head> check a change against the project's own tests (no push, no agent)
|
|
151
153
|
[--pr <n>] [--repo <owner/name> | --clone-url <url>] [--wait]
|
|
152
154
|
graft checks [id] your recent change checks, or one check's result
|
|
153
|
-
graft regen connect <owner/name or URL> --branch <b> --record <path> --impl <dir> --lang ts|py [--builds 1-3] [--on-push]
|
|
155
|
+
graft regen connect <owner/name or URL> --branch <b> --record <path> --impl <dir> --lang ts|py|rs|go [--builds 1-3] [--on-push]
|
|
154
156
|
connect a spec (a duramen record): Graft rebuilds its implementation blind
|
|
155
|
-
graft regen run <spec> rebuild it now
|
|
157
|
+
graft regen run <spec> rebuild it now [--force: even if nothing changed] [--max-usd <n>: its budget]
|
|
156
158
|
graft regen status [id] your specs, a spec's rebuilds, or one rebuild and how to take it
|
|
159
|
+
graft regen report <spec> the spec's grade (strong, usable, thin) and why [--refresh [--yes]]
|
|
157
160
|
|
|
158
161
|
[copy] is a copy id or owner/name; left out, it's this clone's origin remote.
|
|
159
162
|
Token: GRAFT_TOKEN or --token (make one at https://graftfork.com/app#settings). --json prints raw answers.`;
|
|
@@ -164,7 +167,7 @@ async function run(argv, io) {
|
|
|
164
167
|
args: argv,
|
|
165
168
|
allowPositionals: true,
|
|
166
169
|
strict: false,
|
|
167
|
-
options: { token: { type: "string" }, api: { type: "string" }, json: { type: "boolean" }, patch: { type: "string" }, "no-wait": { type: "boolean" }, branch: { type: "string" }, upstream: { type: "string" }, base: { type: "string" }, tags: { type: "string" }, from: { type: "string" }, to: { type: "string" }, repo: { type: "string" }, "clone-url": { type: "string" }, pr: { type: "string" }, wait: { type: "boolean" }, copy: { type: "string" }, record: { type: "string" }, impl: { type: "string" }, lang: { type: "string" }, builds: { type: "string" }, "on-push": { type: "boolean" }, help: { type: "boolean", short: "h" } }
|
|
170
|
+
options: { token: { type: "string" }, api: { type: "string" }, json: { type: "boolean" }, patch: { type: "string" }, "no-wait": { type: "boolean" }, branch: { type: "string" }, upstream: { type: "string" }, base: { type: "string" }, tags: { type: "string" }, from: { type: "string" }, to: { type: "string" }, repo: { type: "string" }, "clone-url": { type: "string" }, pr: { type: "string" }, wait: { type: "boolean" }, copy: { type: "string" }, record: { type: "string" }, impl: { type: "string" }, lang: { type: "string" }, builds: { type: "string" }, "on-push": { type: "boolean" }, refresh: { type: "boolean" }, force: { type: "boolean" }, "max-usd": { type: "string" }, yes: { type: "boolean" }, help: { type: "boolean", short: "h" } }
|
|
168
171
|
});
|
|
169
172
|
const [cmd, arg, arg2] = positionals;
|
|
170
173
|
if (!cmd || a.help || cmd === "help") {
|
|
@@ -172,8 +175,8 @@ async function run(argv, io) {
|
|
|
172
175
|
return cmd || a.help ? 0 : 1;
|
|
173
176
|
}
|
|
174
177
|
const api = String(a.api ?? io.env.GRAFT_API ?? "https://graftfork.com").replace(/\/+$/, "");
|
|
175
|
-
if (cmd === "verify-record") return verify(arg, api, io);
|
|
176
178
|
const token = a.token ?? io.env.GRAFT_TOKEN;
|
|
179
|
+
if (cmd === "verify-record") return verify(arg, api, io, token);
|
|
177
180
|
if (!token) {
|
|
178
181
|
io.out("graft needs a personal API token: make one at https://graftfork.com/app#settings, then set GRAFT_TOKEN or pass --token.");
|
|
179
182
|
return 1;
|
|
@@ -211,7 +214,7 @@ async function run(argv, io) {
|
|
|
211
214
|
show(b, `${b.message ?? "Connected."}
|
|
212
215
|
Copy id: ${b.copyId}`);
|
|
213
216
|
} else if (cmd === "sync") {
|
|
214
|
-
const b = await call("POST", `/api/v1/copies/${await copyId(arg)}/sync
|
|
217
|
+
const b = await call("POST", `/api/v1/copies/${await copyId(arg)}/sync`, a.force ? { force: true } : void 0);
|
|
215
218
|
show(b, `${b.message}
|
|
216
219
|
Run: ${b.run}`);
|
|
217
220
|
} else if (cmd === "status") {
|
|
@@ -224,7 +227,7 @@ Run: ${b.run}`);
|
|
|
224
227
|
const stuck = (b.stuck ?? []).map((s) => ` ${s.change}: ${s.why}`);
|
|
225
228
|
show(b, [b.headline, ...stuck.length ? ["Stuck:", ...stuck] : [], ...b.action ? [`What to do: ${b.action}`] : [], ...b.command ? [` ${b.command}`] : [], `Next: ${b.next?.why ?? ""}`].filter(Boolean).join("\n"));
|
|
226
229
|
} else if (cmd === "pull") {
|
|
227
|
-
const r = await pull({ cwd: io.cwd, copy: arg, token, api, fetchImpl: io.fetch });
|
|
230
|
+
const r = await pull({ cwd: io.cwd, copy: arg ?? a.copy, token, api, fetchImpl: io.fetch });
|
|
228
231
|
io.out(r.message);
|
|
229
232
|
return r.ok ? 0 : 1;
|
|
230
233
|
} else if (cmd === "agent-record") {
|
|
@@ -296,7 +299,9 @@ Upgrade: ${b.upgrade} (graft upgrades ${b.upgrade})`);
|
|
|
296
299
|
return await regen(arg, arg2, a, call, show, io);
|
|
297
300
|
} else if (cmd === "usage") {
|
|
298
301
|
const b = await call("GET", "/api/v1/usage");
|
|
299
|
-
|
|
302
|
+
const orgs = Array.isArray(b.coveredBy) ? b.coveredBy : [];
|
|
303
|
+
const covered = orgs.length ? ` Copies in ${orgs.map((o) => `${o.name} (${o.plan})`).join(", ")} work as Premium: up to ${b.syncs?.coveredSyncs ?? "?"} syncs this month.` : "";
|
|
304
|
+
show(b, `${b.plan}${orgs.length ? " (your own)" : ""}: ${b.syncs?.used ?? 0} of ${b.syncs?.limit ?? "?"} syncs this month; Graft's agent $${Number(b.graftAgentUsd ?? 0).toFixed(2)}.${covered} Resets ${b.resetsOn}.`);
|
|
300
305
|
} else {
|
|
301
306
|
io.out(USAGE);
|
|
302
307
|
return 1;
|
|
@@ -356,10 +361,10 @@ function checkText(c) {
|
|
|
356
361
|
].join("\n");
|
|
357
362
|
}
|
|
358
363
|
async function regen(sub, id, a, call, show, io) {
|
|
359
|
-
const line = (r) => `${r.id} ${r.status}${r.phase ? ` (${r.phase})` : ""}${r.suite?.total ? ` suite ${r.suite.passed}/${r.suite.total}` : ""}${r.choicesDiffer ? ` ${r.choicesDiffer} choice(s) differ` : ""}${r.why ? `: ${r.why}` : ""}`;
|
|
364
|
+
const line = (r) => `${r.id} ${r.status === "stayed" ? "not kept" : r.status}${r.phase ? ` (${r.phase})` : ""}${r.suite?.total ? ` suite ${r.suite.passed}/${r.suite.total}` : ""}${r.choicesDiffer ? ` ${r.choicesDiffer} choice(s) differ` : ""}${r.why ? `: ${r.why}` : ""}`;
|
|
360
365
|
if (sub === "connect") {
|
|
361
366
|
if (!id || !a.record || !a.impl || !a.lang || !a.branch) {
|
|
362
|
-
io.out("graft regen connect <owner/name or https URL> --branch <b> --record <path> --impl <dir> --lang ts|py [--builds 1-3] [--on-push]");
|
|
367
|
+
io.out("graft regen connect <owner/name or https URL> --branch <b> --record <path> --impl <dir> --lang ts|py|rs|go [--builds 1-3] [--on-push]");
|
|
363
368
|
return 1;
|
|
364
369
|
}
|
|
365
370
|
const b = await call("POST", "/api/v1/regen", { .../^https:\/\//.test(id) ? { clone_url: id } : { repo: id }, branch: a.branch, record: a.record, impl: a.impl, lang: a.lang, ...a.builds && { builds: Number(a.builds) }, ...a["on-push"] && { on_push: true } });
|
|
@@ -367,10 +372,18 @@ async function regen(sub, id, a, call, show, io) {
|
|
|
367
372
|
Spec id: ${b.id}`);
|
|
368
373
|
} else if (sub === "run") {
|
|
369
374
|
if (!id) {
|
|
370
|
-
io.out("graft regen run <spec id>");
|
|
375
|
+
io.out("graft regen run <spec id> [--force] [--max-usd <n>]");
|
|
371
376
|
return 1;
|
|
372
377
|
}
|
|
373
|
-
const
|
|
378
|
+
const max = a["max-usd"] === void 0 ? void 0 : Number(a["max-usd"]);
|
|
379
|
+
if (max !== void 0 && !(Number.isFinite(max) && max >= 0)) {
|
|
380
|
+
io.out("--max-usd is a number of dollars, 0 or more.");
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
const body = { ...a.force && { force: true }, ...max !== void 0 && { max_usd: max } };
|
|
384
|
+
const b = await call("POST", `/api/v1/regen/${encodeURIComponent(id)}/run`, Object.keys(body).length ? body : void 0).catch((e) => {
|
|
385
|
+
throw e instanceof ApiError ? new ApiError(e.message.replace(/\bmax_usd\b/g, "--max-usd")) : e;
|
|
386
|
+
});
|
|
374
387
|
show(b, `${b.message}
|
|
375
388
|
Rebuild: ${b.run}`);
|
|
376
389
|
} else if (sub === "status") {
|
|
@@ -390,26 +403,91 @@ Rebuild: ${b.run}`);
|
|
|
390
403
|
const differ = run2.choicesDiff?.previous;
|
|
391
404
|
const lines = [line(run2), ...run2.builds.map((b) => ` build ${b.n}: ${b.verdict?.why ?? b.note ?? b.status}`)];
|
|
392
405
|
if (differ) for (const [k, label] of [["changed", "chosen differently"], ["added", "new"], ["removed", "gone"]]) for (const c of differ[k]) lines.push(` ${label}: ${c.title}`);
|
|
406
|
+
if (run2.doctor?.gaps?.length) {
|
|
407
|
+
const say = (g) => g.kind === "failing" ? `${g.ref}: cases ${g.cases.join(", ")} fail${g.why ? ` (${g.why})` : ""}` : g.kind === "builds differ" ? `${g.ref ?? g.title}: the builds chose differently` : g.kind === "changed since" ? `${g.ref ?? g.title}: chosen differently from the last passing rebuild` : g.kind === "build crashed" ? `${g.build ? `build ${g.build}` : "a build"} crashed: ${g.why}` : g.kind === "hidden request" ? `build ${g.build} answered ${g.op} ${g.input} differently from the oracle` : `the oracle can't answer ${g.op} ${g.input}: ${g.error}`;
|
|
408
|
+
lines.push(run2.doctor.summary, ...run2.doctor.gaps.slice(0, 8).map((g) => ` - ${say(g)}`), ...run2.doctor.gaps.length > 8 ? [` and ${run2.doctor.gaps.length - 8} more (--json)`] : []);
|
|
409
|
+
}
|
|
393
410
|
if (run2.take) lines.push("Take it, in your clone:", ...run2.take.commands.map((c) => ` ${c}`));
|
|
394
411
|
show({ run: run2 }, lines.join("\n"));
|
|
412
|
+
} else if (sub === "report") {
|
|
413
|
+
if (!id) {
|
|
414
|
+
io.out("graft regen report <spec id> [--refresh]");
|
|
415
|
+
return 1;
|
|
416
|
+
}
|
|
417
|
+
if (a.refresh) {
|
|
418
|
+
const before = await call("GET", `/api/v1/regen/${encodeURIComponent(id)}/report`);
|
|
419
|
+
const price = Number(before.nextReport?.priceUsd ?? 0);
|
|
420
|
+
if (before.nextReport?.available === false) {
|
|
421
|
+
io.out(before.nextReport.why);
|
|
422
|
+
return 1;
|
|
423
|
+
}
|
|
424
|
+
if (price > 0 && !a.yes) {
|
|
425
|
+
io.out(`A new report costs $${price.toFixed(2)} from your agent work (today's free ones are used). Run again with --yes to pay for it.`);
|
|
426
|
+
return 1;
|
|
427
|
+
}
|
|
428
|
+
const q = await call("POST", `/api/v1/regen/${encodeURIComponent(id)}/report`, { max_usd: price });
|
|
429
|
+
show(q, `${q.message}
|
|
430
|
+
Report run: ${q.run}`);
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
const b = await call("GET", `/api/v1/regen/${encodeURIComponent(id)}/report`);
|
|
434
|
+
const r = b.report;
|
|
435
|
+
if (!r) {
|
|
436
|
+
show(b, b.pending ? `A report is on its way (${b.pending}).` : b.message ?? "No report yet: graft regen report <spec id> --refresh");
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
const n = r.numbers ?? {};
|
|
440
|
+
const m = r.mutation ?? {};
|
|
441
|
+
show(b, [
|
|
442
|
+
`${String(r.grade).toUpperCase()}${r.commit ? ` (at ${String(r.commit).slice(0, 12)})` : ""}`,
|
|
443
|
+
...r.reasons.map((x) => ` - ${x}`),
|
|
444
|
+
`${n.requirements ?? "?"} requirements, ${n.examples ?? "?"} examples, ${n.decisions ?? "?"} decisions, ${n.open ?? "?"} open; ${n.oracle ? "an oracle" : "no oracle"}`,
|
|
445
|
+
m.ran ? `mistakes planted in the oracle: ${m.killed} caught, ${m.unpinned} unnoticed, ${m.silent} change nothing (of ${m.total})` : `no mutation score: ${m.why}`,
|
|
446
|
+
`fit: ${r.fit?.why ?? ""}`,
|
|
447
|
+
...b.pending ? [`A newer report is on its way (${b.pending}).`] : []
|
|
448
|
+
].join("\n"));
|
|
395
449
|
} else {
|
|
396
|
-
io.out("graft regen connect|run|status (graft help)");
|
|
450
|
+
io.out("graft regen connect|run|status|report (graft help)");
|
|
397
451
|
return 1;
|
|
398
452
|
}
|
|
399
453
|
return 0;
|
|
400
454
|
}
|
|
401
455
|
var unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
402
|
-
async function verify(file, api, io) {
|
|
456
|
+
async function verify(file, api, io, token) {
|
|
403
457
|
if (!file) {
|
|
404
|
-
io.out("graft verify-record <file>");
|
|
458
|
+
io.out("graft verify-record <file or rebuild id>");
|
|
405
459
|
return 1;
|
|
406
460
|
}
|
|
407
461
|
let e;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
462
|
+
if (!existsSync(file) && /^[A-Za-z0-9_-]{8,64}$/.test(file)) {
|
|
463
|
+
if (!token) {
|
|
464
|
+
io.out(`${file} isn't a file. To fetch a record by its id, graft needs a personal API token: make one at https://graftfork.com/app#settings, then set GRAFT_TOKEN or pass --token.`);
|
|
465
|
+
return 1;
|
|
466
|
+
}
|
|
467
|
+
let got = null;
|
|
468
|
+
for (const path of [`/api/v1/regen/runs/${file}/record`, `/api/v1/runs/${file}/record`]) {
|
|
469
|
+
const r2 = await io.fetch(`${api}${path}`, { headers: { authorization: `Bearer ${token}` } });
|
|
470
|
+
if (r2.ok) {
|
|
471
|
+
got = await r2.json().catch(() => null);
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
if (r2.status !== 404) {
|
|
475
|
+
io.out(`graftfork.com answered ${r2.status} for ${file}'s record.`);
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!got) {
|
|
480
|
+
io.out(`No signed record for ${file}: not one of your rebuilds or syncs (or a public spec's rebuild), or it hasn't finished.`);
|
|
481
|
+
return 1;
|
|
482
|
+
}
|
|
483
|
+
e = got;
|
|
484
|
+
} else {
|
|
485
|
+
try {
|
|
486
|
+
e = JSON.parse(readFileSync(file, "utf8"));
|
|
487
|
+
} catch {
|
|
488
|
+
io.out(`${file} isn't a Graft record (not JSON).`);
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
413
491
|
}
|
|
414
492
|
if (e?.graft_record !== 1 || e.alg !== "Ed25519" || typeof e.payload !== "string" || typeof e.signature !== "string") {
|
|
415
493
|
io.out(`${file} isn't a Graft record.`);
|
|
@@ -436,7 +514,23 @@ async function verify(file, api, io) {
|
|
|
436
514
|
io.out(`Valid: signed by graftfork.com (key ${k.id}). Agent change record for ${r.org?.name ?? r.org?.id}, ${r.range?.from} to ${r.range?.to}: ${(r.changes ?? []).length} change(s) in ${r.counts?.runs ?? 0} run(s), ${(r.agent_actions ?? []).length} agent action(s).`);
|
|
437
515
|
return 0;
|
|
438
516
|
}
|
|
439
|
-
|
|
517
|
+
const i = r.initiator;
|
|
518
|
+
const by = !i?.kind ? "" : i.kind === "agent" ? ` Started by ${i.agent ? `the organization's agent ${i.agent} (org ${i.org ?? "unknown"}, ` : "an agent ("}token ${i.token ?? "unknown"}${i.client?.name ? `, ${i.client.name}${i.client.version ? ` ${i.client.version}` : ""}` : ""}).` : i.kind === "ops" ? " Started by Graft's operator." : i.kind === "schedule" ? " Started by Graft on its own (a release, a push or a new model)." : " Started by a person.";
|
|
519
|
+
if (r.kind === "regen") {
|
|
520
|
+
const outcome = r.outcome === "passed" ? "kept" : r.outcome === "stayed" ? "not kept" : r.outcome;
|
|
521
|
+
if (r.rejudge?.of) {
|
|
522
|
+
const verdict = r.outcome === "passed" ? "passed" : r.outcome === "stayed" ? "didn't pass" : r.outcome;
|
|
523
|
+
const agree = r.rejudge.agrees === true ? "agrees with" : r.rejudge.agrees === false ? "differs from" : "can't be compared with";
|
|
524
|
+
io.out(`Valid: signed by graftfork.com (key ${k.id}). Re-judge ${r.run} of rebuild ${r.rejudge.of}: ${verdict}${r.suite ? `, suite ${r.suite.passed}/${r.suite.total}` : ""}, holdout seed ${r.rejudge.seed}${r.rejudge.same_seed ? " (the rebuild's own)" : ""}; it ${agree} the first judge.${r.judge ? ` Judged as ${r.judge.driver_user === "separate" ? "a separate user" : "the oracle's user"}, network ${r.judge.network}.` : ""}${by}`);
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
const what = r.draft?.branch ? `Draft rebuild ${r.run} of branch ${r.draft.branch}` : `Rebuild ${r.run}`;
|
|
528
|
+
const b = r.behaviour;
|
|
529
|
+
const beh = !b ? "" : b.sampled ? ` Against rebuild ${b.against?.run ?? "?"}: ${b.requests} requests, ${(b.paths ?? []).length} part(s) of the answer changed, ${b.unexplained} change(s) the record didn't ask for.` : ` Not compared with the kept build${b.why ? `: ${b.why}` : ""}.`;
|
|
530
|
+
io.out(`Valid: signed by graftfork.com (key ${k.id}). ${what}, ${outcome}${r.copy?.repo ? `: ${r.copy.repo} ${r.copy.record ?? ""} to ${r.copy.impl ?? ""}` : ""}${r.spec?.name ? ` (${r.spec.name}${r.spec.version ? ` ${r.spec.version}` : ""})` : ""}${r.suite ? `, suite ${r.suite.passed}/${r.suite.total}` : ""}, ${(r.builds ?? []).length} build(s).${beh}${by}`);
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
io.out(`Valid: signed by graftfork.com (key ${k.id}). Run ${r.run}, ${r.outcome}${r.upstream?.to ? ` onto ${r.upstream.to}` : ""}, ${(r.changes ?? []).length} change(s).${by}`);
|
|
440
534
|
return 0;
|
|
441
535
|
}
|
|
442
536
|
if (process.argv[1] && /graft(\.mjs)?$|client[/\\]cli\.ts$/.test(process.argv[1])) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graftfork",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The command line for graftfork.com: connect a copy, sync it, see why it stayed, and take each move. A thin client; syncs run on graftfork.com.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://graftfork.com/docs/api",
|