@junghanacs/entwurf 0.25.0 → 0.25.1
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/CHANGELOG.md +109 -0
- package/CONTRIBUTING.md +1 -1
- package/VERIFY.md +1 -1
- package/package.json +1 -1
- package/run.sh +5 -4
- package/scripts/check-gate-qualification.ts +2347 -7
- package/scripts/lib/qualification-acceptance.ts +146 -0
- package/scripts/lib/qualification-artifact.ts +359 -0
- package/scripts/lib/qualification-collection.ts +384 -0
- package/scripts/lib/qualification-gh.ts +231 -0
- package/scripts/lib/qualification-ledger.ts +669 -0
- package/scripts/lib/qualification-oracle-core.ts +279 -0
- package/scripts/lib/qualification-receipt.ts +561 -0
- package/scripts/lib/qualification-release.ts +271 -0
- package/scripts/mutants/qualification-ledger.json +489 -0
- package/scripts/qualification-oracle.ts +124 -0
- package/scripts/smoke-herdr-raw-install-live.sh +7 -2
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qualification-oracle-core — the exact-SHA composite release oracle, as the TARGET SHA's
|
|
3
|
+
* own code (#124 P1). It runs inside a clean detached clone of that SHA made by the
|
|
4
|
+
* launcher (`scripts/qualification-oracle.ts`); the operator's checkout never judges a SHA
|
|
5
|
+
* with its own bytes.
|
|
6
|
+
*
|
|
7
|
+
* selfCheck the checkout it runs in IS the SHA it was asked about, and is clean —
|
|
8
|
+
* run directly (without the launcher) it still cannot impersonate a SHA.
|
|
9
|
+
* runOracle GitHub evidence (injected GhApi) → admitted collections → composeRelease.
|
|
10
|
+
* The final run must be the one the caller pinned (the run the exact-CI
|
|
11
|
+
* oracle already chose). Admission errors are kept as RED reasons.
|
|
12
|
+
* decisionDigest sha256 over the canonical ledger: machine-independent by construction
|
|
13
|
+
* (every source is a run/file label, never a host path).
|
|
14
|
+
*
|
|
15
|
+
* What it does NOT prove: that the GhApi answers are GitHub's. `ghCli` is the only networked
|
|
16
|
+
* implementation and the provenance boundary; the pure composer never vouches for inputs.
|
|
17
|
+
* No mode of this CLI skips the verdict — exit 0 means COMPOSITE-GREEN and nothing else.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { type MutantSpec, makeOriginChecks, validateManifest, validateManifestSet } from "./mutation-qualify.ts";
|
|
25
|
+
import { ancestorRunEvidence, finalRunEvidence, type GhApi, ghCli, type RunEvidence } from "./qualification-gh.ts";
|
|
26
|
+
import { type ContentCandidate, contentCandidate, type Ledger } from "./qualification-ledger.ts";
|
|
27
|
+
import { canonicalJson, checkReceiptPath, sha256, writeReceiptExclusive } from "./qualification-receipt.ts";
|
|
28
|
+
import { type CiRun, composeRelease, type ReleaseLedger } from "./qualification-release.ts";
|
|
29
|
+
|
|
30
|
+
export const ORACLE_CORE_PATH = "scripts/lib/qualification-oracle-core.ts";
|
|
31
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
32
|
+
const RUN = /^[0-9]+$/;
|
|
33
|
+
const REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
34
|
+
|
|
35
|
+
export interface OracleArgs {
|
|
36
|
+
sha: string;
|
|
37
|
+
repo: string;
|
|
38
|
+
finalRun: string;
|
|
39
|
+
ancestors: { sha: string; runId: string }[];
|
|
40
|
+
/** `--require-group`: exact gate argv a human requires exact-MEASURED on the final SHA. */
|
|
41
|
+
declared: string[][];
|
|
42
|
+
ledgerOut?: string;
|
|
43
|
+
/** Launcher only: the repository to clone (defaults to the launcher's own checkout). */
|
|
44
|
+
origin?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class OracleArgsError extends Error {}
|
|
48
|
+
|
|
49
|
+
/** Strict: unknown/repeated flags, malformed values, duplicate or final-SHA ancestors are refused. */
|
|
50
|
+
export function parseOracleArgs(argv: string[], allowOrigin = false): OracleArgs {
|
|
51
|
+
const seen = new Set<string>();
|
|
52
|
+
const out: Partial<OracleArgs> & { ancestors: { sha: string; runId: string }[]; declared: string[][] } = {
|
|
53
|
+
ancestors: [],
|
|
54
|
+
declared: [],
|
|
55
|
+
};
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
const flag = argv[i];
|
|
58
|
+
const value = argv[i + 1];
|
|
59
|
+
const need = (): string => {
|
|
60
|
+
if (value === undefined || value.startsWith("--")) throw new OracleArgsError(`${flag} needs a value`);
|
|
61
|
+
i++;
|
|
62
|
+
return value;
|
|
63
|
+
};
|
|
64
|
+
if (flag !== "--ancestor-run" && flag !== "--require-group") {
|
|
65
|
+
if (seen.has(flag)) throw new OracleArgsError(`repeated flag ${flag}`);
|
|
66
|
+
seen.add(flag);
|
|
67
|
+
}
|
|
68
|
+
if (flag === "--sha") {
|
|
69
|
+
const v = need();
|
|
70
|
+
if (!SHA.test(v)) throw new OracleArgsError(`--sha must be a full 40-hex SHA: ${v}`);
|
|
71
|
+
out.sha = v;
|
|
72
|
+
} else if (flag === "--repo") {
|
|
73
|
+
const v = need();
|
|
74
|
+
if (!REPO.test(v)) throw new OracleArgsError(`--repo must be owner/name: ${v}`);
|
|
75
|
+
out.repo = v;
|
|
76
|
+
} else if (flag === "--final-run") {
|
|
77
|
+
const v = need();
|
|
78
|
+
if (!RUN.test(v)) throw new OracleArgsError(`--final-run must be a numeric run id: ${v}`);
|
|
79
|
+
out.finalRun = v;
|
|
80
|
+
} else if (flag === "--ancestor-run") {
|
|
81
|
+
const v = need();
|
|
82
|
+
const m = /^([0-9a-f]{40}):([0-9]+)$/.exec(v);
|
|
83
|
+
if (!m) throw new OracleArgsError(`--ancestor-run must be <40-hex sha>:<run id>: ${v}`);
|
|
84
|
+
if (out.ancestors.some((a) => a.sha === m[1])) throw new OracleArgsError(`--ancestor-run repeats SHA ${m[1]}`);
|
|
85
|
+
if (out.ancestors.some((a) => a.runId === m[2])) throw new OracleArgsError(`--ancestor-run repeats run ${m[2]}`);
|
|
86
|
+
out.ancestors.push({ sha: m[1], runId: m[2] });
|
|
87
|
+
} else if (flag === "--require-group") {
|
|
88
|
+
const v = need();
|
|
89
|
+
let argv: unknown;
|
|
90
|
+
try {
|
|
91
|
+
argv = JSON.parse(v);
|
|
92
|
+
} catch {
|
|
93
|
+
throw new OracleArgsError(`--require-group must be a JSON argv array: ${v}`);
|
|
94
|
+
}
|
|
95
|
+
if (!Array.isArray(argv) || argv.length === 0 || !argv.every((a) => typeof a === "string" && a !== "")) {
|
|
96
|
+
throw new OracleArgsError(`--require-group must be a non-empty JSON array of non-empty strings: ${v}`);
|
|
97
|
+
}
|
|
98
|
+
if (out.declared.some((d) => JSON.stringify(d) === JSON.stringify(argv)))
|
|
99
|
+
throw new OracleArgsError(`--require-group repeats ${v}`);
|
|
100
|
+
out.declared.push(argv as string[]);
|
|
101
|
+
} else if (flag === "--ledger-out") {
|
|
102
|
+
out.ledgerOut = need();
|
|
103
|
+
} else if (flag === "--origin" && allowOrigin) {
|
|
104
|
+
out.origin = need();
|
|
105
|
+
} else {
|
|
106
|
+
throw new OracleArgsError(`unknown argument ${flag}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!out.sha || !out.repo || !out.finalRun) throw new OracleArgsError("--sha, --repo and --final-run are required");
|
|
110
|
+
if (out.ancestors.some((a) => a.sha === out.sha))
|
|
111
|
+
throw new OracleArgsError("the final SHA cannot also be an ancestor");
|
|
112
|
+
if (out.ancestors.some((a) => a.runId === out.finalRun))
|
|
113
|
+
throw new OracleArgsError("the final run cannot also be an ancestor run");
|
|
114
|
+
return out as OracleArgs;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type GitRunner = (args: string[]) => { status: number; stdout: string };
|
|
118
|
+
export const gitIn =
|
|
119
|
+
(dir: string): GitRunner =>
|
|
120
|
+
(args) => {
|
|
121
|
+
const r = spawnSync("git", ["-C", dir, ...args], { encoding: "utf8" });
|
|
122
|
+
return { status: r.status ?? 1, stdout: r.stdout ?? "" };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** The checkout IS `sha` and is clean. Injected git so the refusals are testable without a repo. */
|
|
126
|
+
export function selfCheck(git: GitRunner, sha: string): string[] {
|
|
127
|
+
const errors: string[] = [];
|
|
128
|
+
const head = git(["rev-parse", "HEAD"]);
|
|
129
|
+
if (head.status !== 0 || head.stdout.trim() !== sha) {
|
|
130
|
+
errors.push(`self-check: this checkout is at ${head.stdout.trim() || "?"}, not ${sha}`);
|
|
131
|
+
}
|
|
132
|
+
const st = git(["status", "--porcelain"]);
|
|
133
|
+
if (st.status !== 0 || st.stdout !== "") errors.push("self-check: this checkout is not clean");
|
|
134
|
+
return errors;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Every declared argv must be EXACTLY one of this tree's group gates — no prefix, no
|
|
139
|
+
* whitespace folding, no affected-set inference. An unknown argv is a caller error.
|
|
140
|
+
*/
|
|
141
|
+
export function checkDeclared(declared: string[][], groups: { gate: string[] }[]): string[] {
|
|
142
|
+
const known = new Set(groups.map((g) => JSON.stringify(g.gate)));
|
|
143
|
+
return declared
|
|
144
|
+
.filter((d) => !known.has(JSON.stringify(d)))
|
|
145
|
+
.map((d) => `require-group-unknown: ${JSON.stringify(d)} is not a group gate of this tree`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The committed mutant set of THIS checkout, validated against its own index. */
|
|
149
|
+
export function loadSelected(repoDir: string): MutantSpec[] {
|
|
150
|
+
const dir = path.join(repoDir, "scripts", "mutants");
|
|
151
|
+
const manifests = fs
|
|
152
|
+
.readdirSync(dir)
|
|
153
|
+
.filter((f) => f.endsWith(".json"))
|
|
154
|
+
.sort()
|
|
155
|
+
.map((f) => validateManifest(JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")), f));
|
|
156
|
+
return validateManifestSet(manifests, makeOriginChecks(repoDir));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface OracleResult {
|
|
160
|
+
ledger: ReleaseLedger;
|
|
161
|
+
/** sha256 of the canonical ledger — the digest a human acceptance must name. */
|
|
162
|
+
ledgerSha256: string;
|
|
163
|
+
oracleErrors: string[];
|
|
164
|
+
provenance: string[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function decisionDigest(ledger: Ledger): string {
|
|
168
|
+
return sha256(canonicalJson(ledger));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function runOracle(input: {
|
|
172
|
+
api: GhApi;
|
|
173
|
+
repo: string;
|
|
174
|
+
sha: string;
|
|
175
|
+
finalRunId: string;
|
|
176
|
+
ancestors: { sha: string; runId: string }[];
|
|
177
|
+
independent: ContentCandidate;
|
|
178
|
+
firstParent: string[];
|
|
179
|
+
operatorDeclared: string[][];
|
|
180
|
+
}): OracleResult {
|
|
181
|
+
const oracleErrors: string[] = [];
|
|
182
|
+
const runs: CiRun[] = [];
|
|
183
|
+
const take = (ev: RunEvidence, sha: string, label: string) => {
|
|
184
|
+
for (const e of ev.errors) oracleErrors.push(`${label}: ${e}`);
|
|
185
|
+
if (ev.errors.length === 0 && ev.files && ev.runId && ev.runAttempt) {
|
|
186
|
+
runs.push({ runId: ev.runId, runAttempt: ev.runAttempt, headSha: sha, files: ev.files });
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const final = finalRunEvidence(input.api, input.repo, input.sha, input.independent.coreFiles ?? {});
|
|
190
|
+
if (final.runId !== null && final.runId !== input.finalRunId) {
|
|
191
|
+
oracleErrors.push(
|
|
192
|
+
`final-run-pinned-mismatch: the newest run at the SHA is ${final.runId}, the caller pinned ${input.finalRunId}`,
|
|
193
|
+
);
|
|
194
|
+
} else take(final, input.sha, `final run ${input.finalRunId}`);
|
|
195
|
+
for (const a of input.ancestors)
|
|
196
|
+
take(ancestorRunEvidence(input.api, input.repo, a.sha, a.runId), a.sha, `ancestor run ${a.runId}`);
|
|
197
|
+
|
|
198
|
+
const composed = composeRelease({
|
|
199
|
+
finalSha: input.sha,
|
|
200
|
+
firstParent: input.firstParent,
|
|
201
|
+
runs,
|
|
202
|
+
independent: input.independent,
|
|
203
|
+
operatorDeclared: input.operatorDeclared,
|
|
204
|
+
});
|
|
205
|
+
const ledger: ReleaseLedger =
|
|
206
|
+
oracleErrors.length === 0
|
|
207
|
+
? composed
|
|
208
|
+
: {
|
|
209
|
+
...composed,
|
|
210
|
+
verdict: "RED",
|
|
211
|
+
summary: [
|
|
212
|
+
`[qualification-oracle] RED on ${input.sha.slice(0, 12)}: evidence refused before composition`,
|
|
213
|
+
...oracleErrors.map((e) => ` ORACLE ${e}`),
|
|
214
|
+
...composed.summary,
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
ledger,
|
|
219
|
+
ledgerSha256: decisionDigest(ledger),
|
|
220
|
+
oracleErrors,
|
|
221
|
+
provenance: [
|
|
222
|
+
"GitHub run/job/artifact records are read through the injected GhApi; only ghCli touches the network",
|
|
223
|
+
"the pure composer does not prove where its inputs came from",
|
|
224
|
+
"node/node_modules C0 is the final run's own witness; this tree's CORE_FILES are recomputed here",
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const EXIT = { "COMPOSITE-GREEN": 0, RED: 1, "RISK-PENDING": 3, "NOT-QUALIFIED": 4 } as const;
|
|
230
|
+
|
|
231
|
+
/** In-clone CLI. Exit: 0 COMPOSITE-GREEN, 1 RED, 3 RISK-PENDING, 4 NOT-QUALIFIED, 2 ABORT. */
|
|
232
|
+
export function main(argv: string[], repoDir: string, api: GhApi = ghCli()): number {
|
|
233
|
+
let args: OracleArgs;
|
|
234
|
+
try {
|
|
235
|
+
args = parseOracleArgs(argv);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error(`[qualification-oracle] ABORT: ${err instanceof Error ? err.message : String(err)}`);
|
|
238
|
+
return 2;
|
|
239
|
+
}
|
|
240
|
+
const git = gitIn(repoDir);
|
|
241
|
+
const self = selfCheck(git, args.sha);
|
|
242
|
+
if (self.length > 0) {
|
|
243
|
+
for (const e of self) console.error(`[qualification-oracle] ABORT ${e}`);
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
if (args.ledgerOut) checkReceiptPath(args.ledgerOut, repoDir);
|
|
247
|
+
const selected = loadSelected(repoDir);
|
|
248
|
+
const independent = contentCandidate(repoDir, selected);
|
|
249
|
+
const unknown = checkDeclared(args.declared, independent.groups);
|
|
250
|
+
if (unknown.length > 0) {
|
|
251
|
+
for (const e of unknown) console.error(`[qualification-oracle] ABORT ${e}`);
|
|
252
|
+
return 2;
|
|
253
|
+
}
|
|
254
|
+
const firstParent = git(["rev-list", "--first-parent", args.sha]).stdout.trim().split("\n");
|
|
255
|
+
const result = runOracle({
|
|
256
|
+
api,
|
|
257
|
+
repo: args.repo,
|
|
258
|
+
sha: args.sha,
|
|
259
|
+
finalRunId: args.finalRun,
|
|
260
|
+
ancestors: args.ancestors,
|
|
261
|
+
independent,
|
|
262
|
+
firstParent,
|
|
263
|
+
operatorDeclared: args.declared,
|
|
264
|
+
});
|
|
265
|
+
const after = selfCheck(git, args.sha);
|
|
266
|
+
if (after.length > 0) {
|
|
267
|
+
for (const e of after) console.error(`[qualification-oracle] ABORT (after) ${e}`);
|
|
268
|
+
return 2;
|
|
269
|
+
}
|
|
270
|
+
for (const line of result.ledger.summary) console.log(line);
|
|
271
|
+
for (const p of result.provenance) console.log(` PROVENANCE ${p}`);
|
|
272
|
+
console.log(`[qualification-oracle] ledger sha256 ${result.ledgerSha256}`);
|
|
273
|
+
if (args.ledgerOut) writeReceiptExclusive(args.ledgerOut, { ...result, schema: "entwurf.qualification-oracle/v0" });
|
|
274
|
+
return EXIT[result.ledger.verdict];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
278
|
+
process.exit(main(process.argv.slice(2), process.cwd()));
|
|
279
|
+
}
|