patchwork-os 1.2.0-beta.2.canary.753 → 1.2.0-beta.2.canary.756
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 +174 -0
- 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/privacy/describeDestinations.d.ts +76 -0
- package/dist/privacy/describeDestinations.js +132 -0
- package/dist/privacy/describeDestinations.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,8 @@ 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` +
|
|
312
|
+
` privacy destinations Where prompts may go, and which leave this machine\n` +
|
|
307
313
|
` privacy receipts [--since-days N] What the LIVE information boundary actually decided\n` +
|
|
308
314
|
` privacy shadow [--since-days N] What a candidate privacy policy would have stopped\n` +
|
|
309
315
|
` suggest [--since-days N] Recipe + unused-tool suggestions\n` +
|
|
@@ -4160,6 +4166,147 @@ if (process.argv[2] === "shadow-scan") {
|
|
|
4160
4166
|
//
|
|
4161
4167
|
// Reports DENOMINATORS. It is not the reader, and building the reader is
|
|
4162
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
|
+
}
|
|
4163
4310
|
if (process.argv[2] === "evidence") {
|
|
4164
4311
|
const args = process.argv.slice(3);
|
|
4165
4312
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -4730,6 +4877,33 @@ if (process.argv[2] === "privacy") {
|
|
|
4730
4877
|
}
|
|
4731
4878
|
process.exit(0);
|
|
4732
4879
|
}
|
|
4880
|
+
// `privacy destinations` — where prompts may go, and which of those
|
|
4881
|
+
// leave the machine.
|
|
4882
|
+
//
|
|
4883
|
+
// Adds no policy primitive on purpose. Clearing a remote destination for
|
|
4884
|
+
// `personal` is already one line in config.json; what was missing is that
|
|
4885
|
+
// the choice was invisible and undisclosed. A recipe-scoped allow-list
|
|
4886
|
+
// would put recipe identity into the decision point, which the receipt
|
|
4887
|
+
// wiring deliberately keeps out of it, and would smuggle in "purpose"
|
|
4888
|
+
// ahead of the per-field labels ADR-0021 reserves it for.
|
|
4889
|
+
if (args[0] === "destinations") {
|
|
4890
|
+
const { parseRegistry } = await import("./privacy/destinationRegistry.js");
|
|
4891
|
+
const { describeDestinations, formatDestinationsReport } = await import("./privacy/describeDestinations.js");
|
|
4892
|
+
const { loadConfig } = await import("./patchworkConfig.js");
|
|
4893
|
+
const cfg = loadConfig();
|
|
4894
|
+
const registry = parseRegistry(cfg.privacy);
|
|
4895
|
+
const described = describeDestinations(registry.destinations, registry.driversFor, { ...(cfg.privacy?.notes && { notes: cfg.privacy.notes }) });
|
|
4896
|
+
if (args.includes("--json")) {
|
|
4897
|
+
process.stdout.write(`${JSON.stringify(described, null, 2)}\n`);
|
|
4898
|
+
}
|
|
4899
|
+
else {
|
|
4900
|
+
process.stdout.write(`${formatDestinationsReport(described)}\n`);
|
|
4901
|
+
}
|
|
4902
|
+
// Always 0: an inert boundary and a wide-open one are both legitimate
|
|
4903
|
+
// operator states, and exiting non-zero on either would make a correct
|
|
4904
|
+
// answer look like a failure.
|
|
4905
|
+
process.exit(0);
|
|
4906
|
+
}
|
|
4733
4907
|
// `privacy undeclared` — the population most likely to be
|
|
4734
4908
|
// under-classified, fleet-wide. `recipe lint` warns per recipe and
|
|
4735
4909
|
// `privacy suggest` reports undeclared DRIVERS; neither answers "across
|