patchwork-os 1.2.0-beta.2.canary.755 → 1.2.0-beta.2.canary.757
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 +260 -50
- package/dist/index.js.map +1 -1
- package/dist/maintenance/prOutcomeLedger.d.ts +133 -0
- package/dist/maintenance/prOutcomeLedger.js +203 -0
- package/dist/maintenance/prOutcomeLedger.js.map +1 -0
- package/dist/workers/authorityDelta.d.ts +69 -0
- package/dist/workers/authorityDelta.js +252 -0
- package/dist/workers/authorityDelta.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -220,6 +220,10 @@ const KNOWN_SUBCOMMANDS = [
|
|
|
220
220
|
"reject",
|
|
221
221
|
"gate",
|
|
222
222
|
"outcomes",
|
|
223
|
+
// Raw PR-event ledger. Absent from this array a verb IS dispatched but the
|
|
224
|
+
// suggester calls it unknown — the exact trap the `tools` comment below
|
|
225
|
+
// records, and audit-cli-commands.mjs reads this array as ground truth.
|
|
226
|
+
"pr-outcomes",
|
|
223
227
|
"butler",
|
|
224
228
|
"privacy",
|
|
225
229
|
"doctor",
|
|
@@ -304,6 +308,7 @@ if (process.argv[2] === "--help" ||
|
|
|
304
308
|
`Diagnose\n` +
|
|
305
309
|
` halts [--window 1h|24h|overnight|7d] Morning summary of recent recipe halts\n` +
|
|
306
310
|
` judgments [--window ...] [--recipe N] Recent judge-step verdicts across runs\n` +
|
|
311
|
+
` pr-outcomes <collect|show> Record raw PR events so trust can be derived later\n` +
|
|
307
312
|
` privacy destinations Where prompts may go, and which leave this machine\n` +
|
|
308
313
|
` privacy receipts [--since-days N] What the LIVE information boundary actually decided\n` +
|
|
309
314
|
` privacy shadow [--since-days N] What a candidate privacy policy would have stopped\n` +
|
|
@@ -4161,6 +4166,147 @@ if (process.argv[2] === "shadow-scan") {
|
|
|
4161
4166
|
//
|
|
4162
4167
|
// Reports DENOMINATORS. It is not the reader, and building the reader is
|
|
4163
4168
|
// exactly what the spine's own rule defers until there is evidence to read.
|
|
4169
|
+
// --- pr-outcomes (maintenance roadmap, phase 1) ---
|
|
4170
|
+
//
|
|
4171
|
+
// Built before the workers that will read it, deliberately, and it is the one
|
|
4172
|
+
// item on that roadmap where that is correct: outcome history accrues only
|
|
4173
|
+
// with wall-clock time, and a day not recorded is a day that cannot be
|
|
4174
|
+
// recovered. Everything else can be built whenever.
|
|
4175
|
+
if (process.argv[2] === "pr-outcomes") {
|
|
4176
|
+
const args = process.argv.slice(3);
|
|
4177
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4178
|
+
process.stdout.write("Usage: patchwork pr-outcomes <collect|show> [--repo owner/name] [--limit N] [--json]\n\n" +
|
|
4179
|
+
"Record raw pull-request events so trust can be derived LATER from evidence\n" +
|
|
4180
|
+
"rather than asserted now. Each row is what the API said at one moment; run\n" +
|
|
4181
|
+
"`collect` regularly and trajectories appear. Run it once and you get final\n" +
|
|
4182
|
+
"states only, which the summary says rather than implying otherwise.\n\n" +
|
|
4183
|
+
" collect query the repo and append anything that changed\n" +
|
|
4184
|
+
" show summarise what the ledger already holds\n\n" +
|
|
4185
|
+
"No score is derived. Whether a worker opened a PR is OMITTED when no roster\n" +
|
|
4186
|
+
"is configured, never recorded as false: unknown and no are different facts.\n\n" +
|
|
4187
|
+
"Rows name real pull requests and authors, so they are operator data like the\n" +
|
|
4188
|
+
"other ledgers - quote a measurement, never paste the rows.\n");
|
|
4189
|
+
process.exit(0);
|
|
4190
|
+
}
|
|
4191
|
+
(async () => {
|
|
4192
|
+
const { readFileSync, appendFileSync, existsSync } = await import("node:fs");
|
|
4193
|
+
const { join } = await import("node:path");
|
|
4194
|
+
const { patchworkPath } = await import("./patchworkHome.js");
|
|
4195
|
+
const { execFileSync } = await import("node:child_process");
|
|
4196
|
+
const { toObservation, dedupeAgainst, summarise, formatLedgerSummary } = await import("./maintenance/prOutcomeLedger.js");
|
|
4197
|
+
const file = join(patchworkPath(), "pr_outcomes.jsonl");
|
|
4198
|
+
const read = () => {
|
|
4199
|
+
if (!existsSync(file))
|
|
4200
|
+
return [];
|
|
4201
|
+
const out = [];
|
|
4202
|
+
for (const line of readFileSync(file, "utf-8").split("\n")) {
|
|
4203
|
+
if (line.trim() === "")
|
|
4204
|
+
continue;
|
|
4205
|
+
try {
|
|
4206
|
+
out.push(JSON.parse(line));
|
|
4207
|
+
}
|
|
4208
|
+
catch {
|
|
4209
|
+
// A corrupt line is skipped, never repaired: rewriting an
|
|
4210
|
+
// append-only evidence file to make it parse is how evidence
|
|
4211
|
+
// quietly becomes fiction.
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
return out;
|
|
4215
|
+
};
|
|
4216
|
+
const sub = args[0] ?? "show";
|
|
4217
|
+
if (sub === "show") {
|
|
4218
|
+
const rows = read();
|
|
4219
|
+
const s = summarise(rows);
|
|
4220
|
+
process.stdout.write(args.includes("--json")
|
|
4221
|
+
? `${JSON.stringify(s, null, 2)}\n`
|
|
4222
|
+
: `${formatLedgerSummary(s)}\n`);
|
|
4223
|
+
// Always 0. An empty ledger on day one is a true and expected state.
|
|
4224
|
+
process.exit(0);
|
|
4225
|
+
}
|
|
4226
|
+
if (sub === "collect") {
|
|
4227
|
+
const repoIdx = args.indexOf("--repo");
|
|
4228
|
+
const limIdx = args.indexOf("--limit");
|
|
4229
|
+
const limit = limIdx >= 0 ? Number(args[limIdx + 1]) : 100;
|
|
4230
|
+
let repo = repoIdx >= 0 ? args[repoIdx + 1] : undefined;
|
|
4231
|
+
if (repo === undefined) {
|
|
4232
|
+
try {
|
|
4233
|
+
repo = execFileSync("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], {
|
|
4234
|
+
encoding: "utf-8",
|
|
4235
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
4236
|
+
}).trim();
|
|
4237
|
+
}
|
|
4238
|
+
catch {
|
|
4239
|
+
process.stderr.write("[pr-outcomes] could not determine the repository. Pass --repo owner/name.\n");
|
|
4240
|
+
process.exit(2);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
let payload = "";
|
|
4244
|
+
try {
|
|
4245
|
+
payload = execFileSync("gh", [
|
|
4246
|
+
"pr",
|
|
4247
|
+
"list",
|
|
4248
|
+
"--repo",
|
|
4249
|
+
repo,
|
|
4250
|
+
"--state",
|
|
4251
|
+
"all",
|
|
4252
|
+
"--limit",
|
|
4253
|
+
String(Number.isFinite(limit) ? limit : 100),
|
|
4254
|
+
"--json",
|
|
4255
|
+
"number,state,createdAt,mergedAt,closedAt,additions,deletions,changedFiles,author,mergeCommit",
|
|
4256
|
+
], {
|
|
4257
|
+
encoding: "utf-8",
|
|
4258
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4259
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
catch (err) {
|
|
4263
|
+
// Fail-soft and LOUD. A collector that silently reports success on a
|
|
4264
|
+
// failed query would leave a gap indistinguishable from a quiet week.
|
|
4265
|
+
process.stderr.write(`[pr-outcomes] the GitHub query failed, so NOTHING was recorded: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}\n`);
|
|
4266
|
+
process.exit(1);
|
|
4267
|
+
}
|
|
4268
|
+
let parsed;
|
|
4269
|
+
try {
|
|
4270
|
+
parsed = JSON.parse(payload);
|
|
4271
|
+
}
|
|
4272
|
+
catch {
|
|
4273
|
+
process.stderr.write("[pr-outcomes] the GitHub response did not parse; nothing recorded.\n");
|
|
4274
|
+
process.exit(1);
|
|
4275
|
+
}
|
|
4276
|
+
const observedAt = new Date().toISOString();
|
|
4277
|
+
const incoming = [];
|
|
4278
|
+
let unusable = 0;
|
|
4279
|
+
for (const r of Array.isArray(parsed) ? parsed : []) {
|
|
4280
|
+
const o = toObservation(r, {
|
|
4281
|
+
repo: repo,
|
|
4282
|
+
observedAt,
|
|
4283
|
+
});
|
|
4284
|
+
if (o === null)
|
|
4285
|
+
unusable++;
|
|
4286
|
+
else
|
|
4287
|
+
incoming.push(o);
|
|
4288
|
+
}
|
|
4289
|
+
const existing = read();
|
|
4290
|
+
const { toAppend, unchanged, firstSighting } = dedupeAgainst(existing, incoming);
|
|
4291
|
+
for (const o of toAppend)
|
|
4292
|
+
appendFileSync(file, `${JSON.stringify(o)}\n`, { mode: 0o600 });
|
|
4293
|
+
process.stdout.write(`[pr-outcomes] ${repo}: ${incoming.length} pull request(s) queried\n` +
|
|
4294
|
+
` ${toAppend.length} appended (${firstSighting} seen for the first time)\n` +
|
|
4295
|
+
` ${unchanged} unchanged since the last collection\n` +
|
|
4296
|
+
(unusable > 0
|
|
4297
|
+
? ` ${unusable} row(s) lacked the fields needed to key them and were DROPPED\n`
|
|
4298
|
+
: "") +
|
|
4299
|
+
(firstSighting === incoming.length && existing.length === 0
|
|
4300
|
+
? "\n Every row is a first sighting, so this is a snapshot of final\n" +
|
|
4301
|
+
" states rather than a history. Trajectories begin from the next\n" +
|
|
4302
|
+
" collection onward - that part cannot be backfilled.\n"
|
|
4303
|
+
: ""));
|
|
4304
|
+
process.exit(0);
|
|
4305
|
+
}
|
|
4306
|
+
process.stderr.write("Usage: patchwork pr-outcomes <collect|show>\n");
|
|
4307
|
+
process.exit(2);
|
|
4308
|
+
})();
|
|
4309
|
+
}
|
|
4164
4310
|
if (process.argv[2] === "evidence") {
|
|
4165
4311
|
const args = process.argv.slice(3);
|
|
4166
4312
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -4204,10 +4350,17 @@ if (process.argv[2] === "workers") {
|
|
|
4204
4350
|
if ((sub !== "shadow" &&
|
|
4205
4351
|
sub !== "backtest" &&
|
|
4206
4352
|
sub !== "list" &&
|
|
4207
|
-
sub !== "validate"
|
|
4353
|
+
sub !== "validate" &&
|
|
4354
|
+
sub !== "authority-delta") ||
|
|
4208
4355
|
args.includes("--help") ||
|
|
4209
4356
|
args.includes("-h")) {
|
|
4210
|
-
process.stdout.write("Usage: patchwork workers <list|validate|shadow|backtest> [--workers-dir <path>]\n\n" +
|
|
4357
|
+
process.stdout.write("Usage: patchwork workers <list|validate|authority-delta|shadow|backtest> [--workers-dir <path>]\n\n" +
|
|
4358
|
+
" authority-delta What a change does to worker AUTHORITY, comparing manifests\n" +
|
|
4359
|
+
" between two git refs (--base, --head, --dir). Reuses the runtime\n" +
|
|
4360
|
+
" policy primitives, with one inversion: an unreadable `forbids`\n" +
|
|
4361
|
+
" entry fails OPEN at runtime and is reported here as a WIDENING,\n" +
|
|
4362
|
+
' because "I could not read your deny-list" must never resolve to\n' +
|
|
4363
|
+
' "looks fine". Exits 1 on a widening.\n' +
|
|
4211
4364
|
" list What is installed, and — the point — what the bridge IGNORES.\n" +
|
|
4212
4365
|
" A manifest that does not parse is skipped by the loader, so no\n" +
|
|
4213
4366
|
" worker owns its recipe and the autonomy gate never runs for it.\n" +
|
|
@@ -4226,57 +4379,114 @@ if (process.argv[2] === "workers") {
|
|
|
4226
4379
|
" --workers-dir <path> Where *.worker.yaml live (default ~/.patchwork/workers)\n");
|
|
4227
4380
|
process.exit(0);
|
|
4228
4381
|
}
|
|
4229
|
-
(
|
|
4230
|
-
|
|
4231
|
-
const
|
|
4232
|
-
const
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
const
|
|
4237
|
-
|
|
4238
|
-
|
|
4382
|
+
if (sub === "authority-delta") {
|
|
4383
|
+
(async () => {
|
|
4384
|
+
const { execFileSync } = await import("node:child_process");
|
|
4385
|
+
const { parse: parseYaml } = await import("yaml");
|
|
4386
|
+
const { parseWorker } = await import("./workers/worker.js");
|
|
4387
|
+
const { authorityDeltaForSet, formatAuthorityDeltas, widensAuthority } = await import("./workers/authorityDelta.js");
|
|
4388
|
+
const argAfter = (flag) => {
|
|
4389
|
+
const i = args.indexOf(flag);
|
|
4390
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
4391
|
+
};
|
|
4392
|
+
const base = argAfter("--base") ?? "main";
|
|
4393
|
+
const head = argAfter("--head") ?? "HEAD";
|
|
4394
|
+
const dir = argAfter("--dir") ?? "templates/workers";
|
|
4395
|
+
const git = (a) => execFileSync("git", a, { encoding: "utf-8" });
|
|
4396
|
+
const listAt = (ref) => {
|
|
4397
|
+
const m = new Map();
|
|
4398
|
+
let files = [];
|
|
4399
|
+
try {
|
|
4400
|
+
files = git(["ls-tree", "-r", "--name-only", ref, "--", dir])
|
|
4401
|
+
.split("\n")
|
|
4402
|
+
.filter((f) => /\.ya?ml$/i.test(f));
|
|
4403
|
+
}
|
|
4404
|
+
catch {
|
|
4405
|
+
// A ref that cannot be read is a usage error, not "no manifests" —
|
|
4406
|
+
// returning empty would report every worker as newly added, which
|
|
4407
|
+
// this classifier treats as a widening. Failing loudly is the only
|
|
4408
|
+
// honest answer.
|
|
4409
|
+
process.stderr.write(`[authority] cannot read ref "${ref}"\n`);
|
|
4410
|
+
process.exit(2);
|
|
4411
|
+
}
|
|
4412
|
+
for (const f of files) {
|
|
4413
|
+
try {
|
|
4414
|
+
m.set(f, parseWorker(parseYaml(git(["show", `${ref}:${f}`]))));
|
|
4415
|
+
}
|
|
4416
|
+
catch {
|
|
4417
|
+
// An unparseable manifest is SKIPPED by the loader at runtime, so
|
|
4418
|
+
// the worker governs nothing. Omitting it here would masquerade as
|
|
4419
|
+
// a deletion — itself reported as a widening — so say what
|
|
4420
|
+
// happened and point at the verb that diagnoses it.
|
|
4421
|
+
process.stderr.write(`[authority] ${ref}:${f} does not parse — run \`patchwork workers validate\`\n`);
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
return m;
|
|
4425
|
+
};
|
|
4426
|
+
const deltas = authorityDeltaForSet(listAt(base), listAt(head));
|
|
4427
|
+
if (args.includes("--json")) {
|
|
4428
|
+
process.stdout.write(`${JSON.stringify(deltas, null, 2)}\n`);
|
|
4429
|
+
}
|
|
4430
|
+
else {
|
|
4431
|
+
process.stdout.write(`${formatAuthorityDeltas(deltas)}\n`);
|
|
4432
|
+
}
|
|
4433
|
+
// Exit 1 on a widening so CI can gate on it. A widening is not a defect;
|
|
4434
|
+
// it is a change that requires a person to say so.
|
|
4435
|
+
process.exit(widensAuthority(deltas) ? 1 : 0);
|
|
4436
|
+
})();
|
|
4437
|
+
}
|
|
4438
|
+
else
|
|
4439
|
+
(async () => {
|
|
4440
|
+
try {
|
|
4441
|
+
const dirIdx = args.indexOf("--workers-dir");
|
|
4442
|
+
const workersDir = dirIdx !== -1 ? args[dirIdx + 1] : undefined;
|
|
4443
|
+
if (sub === "list" || sub === "validate") {
|
|
4444
|
+
const { scanWorkers, validateWorkers, formatWorkersList, formatWorkersValidate, defaultWorkersDir, defaultRecipesDir, } = await import("./workers/workersCli.js");
|
|
4445
|
+
const dir = workersDir ?? defaultWorkersDir();
|
|
4446
|
+
const json = args.includes("--json");
|
|
4447
|
+
if (sub === "list") {
|
|
4448
|
+
const scan = scanWorkers(dir);
|
|
4449
|
+
process.stdout.write(json
|
|
4450
|
+
? `${JSON.stringify({
|
|
4451
|
+
dir: scan.dir,
|
|
4452
|
+
loaded: scan.loaded.map((l) => ({
|
|
4453
|
+
file: l.file,
|
|
4454
|
+
id: l.worker.id,
|
|
4455
|
+
name: l.worker.name,
|
|
4456
|
+
recipe: l.worker.recipe ?? null,
|
|
4457
|
+
autonomyCeiling: l.worker.autonomyCeiling,
|
|
4458
|
+
owns: l.worker.owns,
|
|
4459
|
+
})),
|
|
4460
|
+
broken: scan.broken,
|
|
4461
|
+
}, null, 2)}\n`
|
|
4462
|
+
: formatWorkersList(scan));
|
|
4463
|
+
// A manifest the bridge ignores is a governance gap, not a note.
|
|
4464
|
+
process.exit(scan.broken.length > 0 ? 1 : 0);
|
|
4465
|
+
}
|
|
4466
|
+
const tIdx = args.indexOf("--templates-dir");
|
|
4467
|
+
const templatesDir = tIdx !== -1 ? args[tIdx + 1] : undefined;
|
|
4468
|
+
const rIdx = args.indexOf("--recipes-dir");
|
|
4469
|
+
const result = validateWorkers({
|
|
4470
|
+
workersDir: dir,
|
|
4471
|
+
recipesDir: rIdx !== -1 ? args[rIdx + 1] : defaultRecipesDir(),
|
|
4472
|
+
...(templatesDir ? { templatesDir } : {}),
|
|
4473
|
+
});
|
|
4239
4474
|
process.stdout.write(json
|
|
4240
|
-
? `${JSON.stringify(
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
file: l.file,
|
|
4244
|
-
id: l.worker.id,
|
|
4245
|
-
name: l.worker.name,
|
|
4246
|
-
recipe: l.worker.recipe ?? null,
|
|
4247
|
-
autonomyCeiling: l.worker.autonomyCeiling,
|
|
4248
|
-
owns: l.worker.owns,
|
|
4249
|
-
})),
|
|
4250
|
-
broken: scan.broken,
|
|
4251
|
-
}, null, 2)}\n`
|
|
4252
|
-
: formatWorkersList(scan));
|
|
4253
|
-
// A manifest the bridge ignores is a governance gap, not a note.
|
|
4254
|
-
process.exit(scan.broken.length > 0 ? 1 : 0);
|
|
4475
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
4476
|
+
: formatWorkersValidate(result));
|
|
4477
|
+
process.exit(result.healthy ? 0 : 1);
|
|
4255
4478
|
}
|
|
4256
|
-
const
|
|
4257
|
-
const
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
recipesDir: rIdx !== -1 ? args[rIdx + 1] : defaultRecipesDir(),
|
|
4262
|
-
...(templatesDir ? { templatesDir } : {}),
|
|
4263
|
-
});
|
|
4264
|
-
process.stdout.write(json
|
|
4265
|
-
? `${JSON.stringify(result, null, 2)}\n`
|
|
4266
|
-
: formatWorkersValidate(result));
|
|
4267
|
-
process.exit(result.healthy ? 0 : 1);
|
|
4479
|
+
const { runWorkerShadowReport, runWorkerBacktest } = await import("./workers/runWorkerShadow.js");
|
|
4480
|
+
const opts = workersDir ? { workersDir } : {};
|
|
4481
|
+
process.stdout.write(sub === "backtest"
|
|
4482
|
+
? runWorkerBacktest(opts)
|
|
4483
|
+
: runWorkerShadowReport(opts));
|
|
4268
4484
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
}
|
|
4275
|
-
catch (err) {
|
|
4276
|
-
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
4277
|
-
process.exit(1);
|
|
4278
|
-
}
|
|
4279
|
-
})();
|
|
4485
|
+
catch (err) {
|
|
4486
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
4487
|
+
process.exit(1);
|
|
4488
|
+
}
|
|
4489
|
+
})();
|
|
4280
4490
|
}
|
|
4281
4491
|
// Handle approvals subcommand — considered-approval KPI. Reads the local
|
|
4282
4492
|
// approval-decision log directly (like `workers shadow`); no bridge round-trip.
|