@kungfu-tech/buildchain 3.0.0 → 3.0.1-alpha.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.
Files changed (35) hide show
  1. package/README.md +3 -0
  2. package/dist/site/buildchain-contract.json +101 -26
  3. package/dist/site/buildchain-site.json +93 -17
  4. package/dist/site/capability-registry.json +6 -5
  5. package/dist/site/controller-registry.json +23 -3
  6. package/dist/site/kfd-claims.json +92 -11
  7. package/dist/site/kfd-upstream-aggregate.json +1 -1
  8. package/dist/site/manual-registry.json +17 -3
  9. package/dist/site/node-api-registry.json +19 -6
  10. package/dist/site/page-registry.json +77 -9
  11. package/dist/site/public-surface-audit.json +61 -9
  12. package/dist/site/publication-authority-registry.json +19 -1
  13. package/dist/site/publication-registry.json +4 -4
  14. package/dist/site/release-provenance.json +1 -0
  15. package/dist/site/site-manifest.json +15 -7
  16. package/dist/site/workflow-registry.json +52 -5
  17. package/docs/MAP.md +1 -0
  18. package/docs/auditable-demo.md +155 -0
  19. package/docs/github-governance-authority.md +2 -2
  20. package/docs/release-activation-transaction.md +38 -0
  21. package/docs/versioning.md +1 -0
  22. package/package.json +2 -1
  23. package/packages/core/buildchain-config.js +14 -0
  24. package/packages/core/buildchain-contract.js +45 -0
  25. package/packages/core/buildchain-kfd-claims.js +1 -0
  26. package/packages/core/buildchain-publication-authority.js +1 -0
  27. package/packages/core/github-governance-authority.js +3 -3
  28. package/packages/core/index.js +14 -0
  29. package/packages/core/release-activation-transaction.js +421 -0
  30. package/scripts/auditable-demo.mjs +576 -0
  31. package/scripts/check-inventory.mjs +4 -0
  32. package/scripts/generate-site-bundle.mjs +5 -0
  33. package/scripts/installer-publication.mjs +15 -5
  34. package/scripts/publication-commit-evidence.mjs +298 -12
  35. package/scripts/resolve-artifact-coordinates.mjs +174 -0
@@ -0,0 +1,576 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import crypto from "node:crypto";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { spawnSync } from "node:child_process";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const UTF8 = new TextDecoder("utf-8", { fatal: true });
12
+ const IMAGE_PATTERN = /^[a-z0-9][a-z0-9./_-]*@sha256:[0-9a-f]{64}$/;
13
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
14
+ const REQUIRED_ADAPTER_FILES = [
15
+ "complete-transcript.txt",
16
+ "public-projection.json",
17
+ "scene.json",
18
+ ];
19
+
20
+ function invariant(condition, message) {
21
+ if (!condition) throw new Error(message);
22
+ }
23
+
24
+ function stableJson(value) {
25
+ const canonical = (item) => {
26
+ if (Array.isArray(item)) return item.map(canonical);
27
+ if (!item || typeof item !== "object") return item;
28
+ return Object.fromEntries(Object.keys(item).sort().map((key) => [key, canonical(item[key])]));
29
+ };
30
+ return `${JSON.stringify(canonical(value), null, 2)}\n`;
31
+ }
32
+
33
+ function sha256(bytes) {
34
+ return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
35
+ }
36
+
37
+ function readRegular(filePath, label, maximumBytes = 8 * 1024 * 1024) {
38
+ const metadata = fs.lstatSync(filePath);
39
+ invariant(metadata.isFile() && !metadata.isSymbolicLink(), `${label} must be a regular non-symlink file`);
40
+ invariant(metadata.size <= maximumBytes, `${label} exceeds ${maximumBytes} bytes`);
41
+ return fs.readFileSync(filePath);
42
+ }
43
+
44
+ function decodeUtf8(bytes, label) {
45
+ try {
46
+ return UTF8.decode(bytes);
47
+ } catch {
48
+ throw new Error(`${label} must be valid UTF-8`);
49
+ }
50
+ }
51
+
52
+ function readJson(filePath, label) {
53
+ try {
54
+ return JSON.parse(decodeUtf8(readRegular(filePath, label), label));
55
+ } catch (error) {
56
+ if (error instanceof SyntaxError) throw new Error(`${label} must be valid JSON`);
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ function writeJson(filePath, value) {
62
+ fs.writeFileSync(filePath, stableJson(value));
63
+ }
64
+
65
+ function resolveInside(root, relativePath, label) {
66
+ invariant(typeof relativePath === "string" && relativePath.length > 0, `${label} is required`);
67
+ invariant(!path.isAbsolute(relativePath), `${label} must be repository-relative`);
68
+ const resolvedRoot = path.resolve(root);
69
+ const resolved = path.resolve(resolvedRoot, relativePath);
70
+ const relation = path.relative(resolvedRoot, resolved);
71
+ invariant(relation && !relation.startsWith("..") && !path.isAbsolute(relation), `${label} escapes its root`);
72
+ return resolved;
73
+ }
74
+
75
+ function ensureEmptyDirectory(directory, label) {
76
+ fs.mkdirSync(directory, { recursive: true });
77
+ const metadata = fs.lstatSync(directory);
78
+ invariant(metadata.isDirectory() && !metadata.isSymbolicLink(), `${label} must be a non-symlink directory`);
79
+ invariant(fs.readdirSync(directory).length === 0, `${label} must be initially empty`);
80
+ }
81
+
82
+ function listFiles(root, prefix = "") {
83
+ const directory = path.join(root, prefix);
84
+ const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) =>
85
+ left.name.localeCompare(right.name, "en"),
86
+ );
87
+ const files = [];
88
+ for (const entry of entries) {
89
+ invariant(!entry.isSymbolicLink(), `bundle member must not be a symlink: ${path.join(prefix, entry.name)}`);
90
+ const relative = path.join(prefix, entry.name);
91
+ if (entry.isDirectory()) {
92
+ files.push(...listFiles(root, relative));
93
+ } else {
94
+ invariant(entry.isFile(), `bundle member must be a regular file: ${relative}`);
95
+ files.push(relative.split(path.sep).join("/"));
96
+ }
97
+ }
98
+ return files;
99
+ }
100
+
101
+ function writeChecksums(root, checksumName = "checksums.sha256") {
102
+ const names = listFiles(root).filter((name) => name !== checksumName);
103
+ const rows = names.map((name) => `${sha256(fs.readFileSync(path.join(root, name))).slice(7)} ${name}`);
104
+ const bytes = `${rows.join("\n")}\n`;
105
+ fs.writeFileSync(path.join(root, checksumName), bytes);
106
+ return sha256(Buffer.from(bytes));
107
+ }
108
+
109
+ function verifyChecksums(root, checksumName = "checksums.sha256") {
110
+ const bytes = readRegular(path.join(root, checksumName), checksumName);
111
+ const text = decodeUtf8(bytes, checksumName);
112
+ invariant(text.endsWith("\n"), `${checksumName} must end with a newline`);
113
+ const rows = text.slice(0, -1).split("\n").filter(Boolean);
114
+ const declared = new Set();
115
+ for (const row of rows) {
116
+ const match = /^([0-9a-f]{64}) ([^\0\r\n]+)$/.exec(row);
117
+ invariant(match, `invalid checksum row: ${row}`);
118
+ const member = match[2];
119
+ const target = resolveInside(root, member, "checksum member");
120
+ invariant(!declared.has(member), `duplicate checksum member: ${member}`);
121
+ declared.add(member);
122
+ invariant(sha256(readRegular(target, member)).slice(7) === match[1], `checksum mismatch: ${member}`);
123
+ }
124
+ const actual = listFiles(root).filter((name) => name !== checksumName);
125
+ invariant(
126
+ actual.length === declared.size && actual.every((name) => declared.has(name)),
127
+ `${checksumName} must cover every bundle member exactly once`,
128
+ );
129
+ return sha256(bytes);
130
+ }
131
+
132
+ function exactKeys(value, required, optional, label) {
133
+ invariant(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
134
+ const allowed = new Set([...required, ...optional]);
135
+ for (const key of Object.keys(value)) invariant(allowed.has(key), `${label}.${key} is not declared`);
136
+ for (const key of required) invariant(key in value, `${label}.${key} is required`);
137
+ }
138
+
139
+ function integer(value, minimum, maximum, label) {
140
+ invariant(Number.isInteger(value) && value >= minimum && value <= maximum, `${label} is out of range`);
141
+ return value;
142
+ }
143
+
144
+ function text(value, minimum, maximum, label) {
145
+ invariant(typeof value === "string" && value.length >= minimum && value.length <= maximum, `${label} is invalid`);
146
+ return value;
147
+ }
148
+
149
+ function validateScene(value) {
150
+ exactKeys(
151
+ value,
152
+ ["schema", "id", "width", "height", "fps", "durationMs", "title"],
153
+ ["commandLabel", "background", "accent"],
154
+ "scene",
155
+ );
156
+ invariant(value.schema === "build-images.demo-scene/v1", "unsupported scene schema");
157
+ invariant(/^[a-z0-9][a-z0-9._-]{0,63}$/.test(value.id), "scene.id is invalid");
158
+ integer(value.width, 640, 1920, "scene.width");
159
+ integer(value.height, 360, 1080, "scene.height");
160
+ integer(value.fps, 1, 30, "scene.fps");
161
+ integer(value.durationMs, 500, 60000, "scene.durationMs");
162
+ text(value.title, 1, 120, "scene.title");
163
+ if (value.commandLabel !== undefined) text(value.commandLabel, 0, 160, "scene.commandLabel");
164
+ for (const key of ["background", "accent"]) {
165
+ if (value[key] !== undefined) invariant(/^#[0-9a-fA-F]{6}$/.test(value[key]), `scene.${key} is invalid`);
166
+ }
167
+ return value;
168
+ }
169
+
170
+ function validateProjection(value, scene, transcriptLineCount) {
171
+ exactKeys(value, ["schema", "evidenceClass", "claimBoundary", "cues"], [], "projection");
172
+ invariant(value.schema === "build-images.demo-projection/v1", "unsupported projection schema");
173
+ text(value.evidenceClass, 1, 120, "projection.evidenceClass");
174
+ text(value.claimBoundary, 1, 500, "projection.claimBoundary");
175
+ invariant(Array.isArray(value.cues) && value.cues.length > 0 && value.cues.length <= 240, "projection.cues is invalid");
176
+ for (const [index, cue] of value.cues.entries()) {
177
+ exactKeys(cue, ["startMs", "endMs", "transcriptLines"], ["annotation"], `projection.cues[${index}]`);
178
+ integer(cue.startMs, 0, scene.durationMs - 1, `projection.cues[${index}].startMs`);
179
+ integer(cue.endMs, 1, scene.durationMs, `projection.cues[${index}].endMs`);
180
+ invariant(cue.endMs > cue.startMs, `projection.cues[${index}] has a non-positive interval`);
181
+ invariant(
182
+ Array.isArray(cue.transcriptLines) && cue.transcriptLines.length > 0 && cue.transcriptLines.length <= 80,
183
+ `projection.cues[${index}].transcriptLines is invalid`,
184
+ );
185
+ const lines = new Set();
186
+ for (const line of cue.transcriptLines) {
187
+ integer(line, 1, transcriptLineCount, `projection.cues[${index}] transcript line`);
188
+ invariant(!lines.has(line), `projection.cues[${index}] repeats transcript line ${line}`);
189
+ lines.add(line);
190
+ }
191
+ if (cue.annotation !== undefined) text(cue.annotation, 0, 200, `projection.cues[${index}].annotation`);
192
+ }
193
+ return value;
194
+ }
195
+
196
+ function validateSourceCoordinate(value) {
197
+ exactKeys(
198
+ value,
199
+ [
200
+ "schema",
201
+ "repository",
202
+ "runId",
203
+ "runAttempt",
204
+ "sourceSha",
205
+ "id",
206
+ "nodeId",
207
+ "name",
208
+ "digest",
209
+ "sizeInBytes",
210
+ "createdAt",
211
+ "expiresAt",
212
+ ],
213
+ [],
214
+ "sourceArtifact",
215
+ );
216
+ invariant(value.schema === "buildchain.github-artifact-coordinate/v1", "unsupported source artifact coordinate schema");
217
+ invariant(/^[^/\s]+\/[^/\s]+$/.test(value.repository), "sourceArtifact.repository is invalid");
218
+ for (const key of ["runId", "runAttempt", "id"]) {
219
+ invariant(/^[1-9][0-9]*$/.test(value[key]), `sourceArtifact.${key} is invalid`);
220
+ }
221
+ invariant(/^[0-9a-f]{40}$/.test(value.sourceSha), "sourceArtifact.sourceSha is invalid");
222
+ text(value.nodeId, 1, 256, "sourceArtifact.nodeId");
223
+ text(value.name, 1, 256, "sourceArtifact.name");
224
+ invariant(!/[\0\r\n]/.test(value.name), "sourceArtifact.name is invalid");
225
+ invariant(DIGEST_PATTERN.test(value.digest), "sourceArtifact.digest is invalid");
226
+ integer(value.sizeInBytes, 0, Number.MAX_SAFE_INTEGER, "sourceArtifact.sizeInBytes");
227
+ const createdAt = Date.parse(value.createdAt);
228
+ const expiresAt = Date.parse(value.expiresAt);
229
+ invariant(Number.isFinite(createdAt), "sourceArtifact.createdAt is invalid");
230
+ invariant(Number.isFinite(expiresAt) && expiresAt > createdAt, "sourceArtifact.expiresAt is invalid");
231
+ return value;
232
+ }
233
+
234
+ function validateAdapterOutput(output, strict = true) {
235
+ for (const name of REQUIRED_ADAPTER_FILES) readRegular(path.join(output, name), `adapter output ${name}`, 4 * 1024 * 1024);
236
+ const transcript = decodeUtf8(
237
+ readRegular(path.join(output, "complete-transcript.txt"), "complete transcript", 4 * 1024 * 1024),
238
+ "complete transcript",
239
+ ).replace(/\r\n/g, "\n");
240
+ invariant(transcript.trim().length > 0, "complete transcript must not be empty");
241
+ const lines = transcript.endsWith("\n") ? transcript.slice(0, -1).split("\n") : transcript.split("\n");
242
+ invariant(lines.length <= 20000, "complete transcript exceeds 20000 lines");
243
+ const scene = validateScene(readJson(path.join(output, "scene.json"), "scene"));
244
+ const projection = validateProjection(
245
+ readJson(path.join(output, "public-projection.json"), "projection"),
246
+ scene,
247
+ lines.length,
248
+ );
249
+ if (strict) {
250
+ const allowed = new Set(REQUIRED_ADAPTER_FILES);
251
+ for (const member of listFiles(output)) invariant(allowed.has(member), `undeclared adapter output: ${member}`);
252
+ }
253
+ return { transcript: transcript.endsWith("\n") ? transcript : `${transcript}\n`, lines, scene, projection };
254
+ }
255
+
256
+ function parseArguments(argv) {
257
+ const command = argv[0];
258
+ const values = {};
259
+ for (let index = 1; index < argv.length; index += 2) {
260
+ const key = argv[index];
261
+ const value = argv[index + 1];
262
+ invariant(key?.startsWith("--") && value !== undefined, `invalid argument near ${key || "<empty>"}`);
263
+ invariant(!(key in values), `duplicate argument: ${key}`);
264
+ values[key] = value;
265
+ }
266
+ return { command, values };
267
+ }
268
+
269
+ function required(values, key) {
270
+ invariant(values[key], `${key} is required`);
271
+ return values[key];
272
+ }
273
+
274
+ function appendOutputs(outputPath, entries) {
275
+ if (!outputPath) return;
276
+ const rows = Object.entries(entries).map(([key, value]) => `${key}=${value}`);
277
+ fs.appendFileSync(outputPath, `${rows.join("\n")}\n`);
278
+ }
279
+
280
+ function copyFile(source, target) {
281
+ fs.mkdirSync(path.dirname(target), { recursive: true });
282
+ fs.copyFileSync(source, target);
283
+ }
284
+
285
+ function runAdapter(values) {
286
+ const sourceRoot = path.resolve(required(values, "--source-root"));
287
+ const artifactRoot = path.resolve(required(values, "--artifact-root"));
288
+ const output = path.resolve(required(values, "--output"));
289
+ const diagnostics = path.resolve(required(values, "--diagnostics"));
290
+ const sourceCoordinate = path.resolve(required(values, "--source-coordinate"));
291
+ const adapterRelative = required(values, "--adapter");
292
+ const adapter = resolveInside(sourceRoot, adapterRelative, "adapter path");
293
+ const metadata = fs.lstatSync(adapter);
294
+ invariant(metadata.isFile() && !metadata.isSymbolicLink(), "adapter must be a regular non-symlink file");
295
+ invariant((metadata.mode & 0o111) !== 0, "adapter must be executable");
296
+ invariant(fs.realpathSync(adapter).startsWith(`${fs.realpathSync(sourceRoot)}${path.sep}`), "adapter resolves outside source");
297
+ validateSourceCoordinate(readJson(sourceCoordinate, "source artifact coordinate"));
298
+ ensureEmptyDirectory(output, "adapter output");
299
+ fs.mkdirSync(diagnostics, { recursive: true });
300
+ const disposableHome = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-auditable-demo-home-"));
301
+ const environment = {
302
+ PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
303
+ HOME: disposableHome,
304
+ XDG_CACHE_HOME: path.join(disposableHome, ".cache"),
305
+ XDG_CONFIG_HOME: path.join(disposableHome, ".config"),
306
+ XDG_DATA_HOME: path.join(disposableHome, ".local", "share"),
307
+ XDG_STATE_HOME: path.join(disposableHome, ".local", "state"),
308
+ npm_config_prefix: path.join(disposableHome, ".npm-prefix"),
309
+ LANG: "C.UTF-8",
310
+ LC_ALL: "C.UTF-8",
311
+ TZ: "UTC",
312
+ CI: "true",
313
+ SOURCE_DATE_EPOCH: "0",
314
+ };
315
+ try {
316
+ const result = spawnSync(
317
+ adapter,
318
+ ["--artifact-root", artifactRoot, "--output", output, "--source-coordinate", sourceCoordinate],
319
+ { cwd: sourceRoot, env, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 },
320
+ );
321
+ fs.writeFileSync(path.join(diagnostics, "adapter.stdout.log"), result.stdout || "");
322
+ fs.writeFileSync(path.join(diagnostics, "adapter.stderr.log"), result.stderr || result.error?.message || "");
323
+ invariant(!result.error && result.status === 0, `adapter failed with exit code ${result.status ?? "spawn-error"}`);
324
+ const normalized = validateAdapterOutput(output);
325
+ fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
326
+ writeJson(path.join(output, "scene.json"), normalized.scene);
327
+ writeJson(path.join(output, "public-projection.json"), normalized.projection);
328
+ writeJson(path.join(diagnostics, "adapter.json"), {
329
+ schema: "buildchain.auditable-demo-adapter-execution/v1",
330
+ path: adapterRelative,
331
+ sha256: sha256(readRegular(adapter, "adapter", 4 * 1024 * 1024)),
332
+ exitCode: 0,
333
+ });
334
+ } finally {
335
+ fs.rmSync(disposableHome, { recursive: true, force: true });
336
+ }
337
+ }
338
+
339
+ function prepareSmoke(values) {
340
+ const adapterOutput = path.resolve(required(values, "--adapter-output"));
341
+ const output = path.resolve(required(values, "--output"));
342
+ const normalized = validateAdapterOutput(adapterOutput);
343
+ ensureEmptyDirectory(output, "smoke input");
344
+ const firstCue = normalized.projection.cues[0];
345
+ const identifier = `${normalized.scene.id.slice(0, 52)}.gate-smoke`.slice(0, 64);
346
+ const scene = {
347
+ schema: "build-images.demo-scene/v1",
348
+ id: identifier,
349
+ width: 640,
350
+ height: 360,
351
+ fps: 5,
352
+ durationMs: 1000,
353
+ title: `${normalized.scene.title.slice(0, 96)} gate smoke`,
354
+ commandLabel: "buildchain auditable demo gate",
355
+ background: normalized.scene.background || "#10151f",
356
+ accent: normalized.scene.accent || "#67e8a5",
357
+ };
358
+ const projection = {
359
+ schema: "build-images.demo-projection/v1",
360
+ evidenceClass: normalized.projection.evidenceClass,
361
+ claimBoundary: normalized.projection.claimBoundary,
362
+ cues: [{
363
+ startMs: 0,
364
+ endMs: 1000,
365
+ transcriptLines: firstCue.transcriptLines.slice(0, 8),
366
+ annotation: "bounded renderer compatibility smoke",
367
+ }],
368
+ };
369
+ fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
370
+ writeJson(path.join(output, "scene.json"), scene);
371
+ writeJson(path.join(output, "public-projection.json"), projection);
372
+ }
373
+
374
+ function verifyRendererOutput(renderOutput, expectedImage, expectedInputs) {
375
+ invariant(IMAGE_PATTERN.test(expectedImage), "renderer image must use an immutable sha256 coordinate");
376
+ const expectedMembers = [
377
+ "checksums.sha256",
378
+ "complete-transcript.txt",
379
+ "demo.gif",
380
+ "demo.mp4",
381
+ "demo.webm",
382
+ "manifest.json",
383
+ "media-probe.json",
384
+ "poster.png",
385
+ "public-projection.json",
386
+ "scene.json",
387
+ ];
388
+ invariant(
389
+ JSON.stringify(listFiles(renderOutput)) === JSON.stringify(expectedMembers),
390
+ "renderer output member set is not exact",
391
+ );
392
+ verifyChecksums(renderOutput);
393
+ const manifest = readJson(path.join(renderOutput, "manifest.json"), "renderer manifest");
394
+ invariant(manifest.schema === "build-images.auditable-demo-render/v1", "unexpected renderer manifest schema");
395
+ invariant(manifest.renderer?.image === expectedImage, "renderer manifest image coordinate mismatch");
396
+ invariant(
397
+ JSON.stringify(Object.keys(manifest.outputs || {}).sort()) ===
398
+ JSON.stringify(["demo.gif", "demo.mp4", "demo.webm", "media-probe.json", "poster.png"]),
399
+ "renderer manifest output set is not exact",
400
+ );
401
+ for (const [key, filePath] of Object.entries(expectedInputs)) {
402
+ const observed = manifest.inputs?.[key]?.root;
403
+ invariant(observed === sha256(readRegular(filePath, `${key} input`)), `renderer ${key} input root mismatch`);
404
+ }
405
+ const probe = readJson(path.join(renderOutput, "media-probe.json"), "media probe");
406
+ invariant(probe.schema === "build-images.demo-media-probe/v1" && probe.passed === true, "renderer media probe failed");
407
+ return { manifest, probe };
408
+ }
409
+
410
+ function finalizeGate(values) {
411
+ const adapterOutput = path.resolve(required(values, "--adapter-output"));
412
+ const smokeInput = path.resolve(required(values, "--smoke-input"));
413
+ const smokeOutput = path.resolve(required(values, "--smoke-output"));
414
+ const sourceCoordinatePath = path.resolve(required(values, "--source-coordinate"));
415
+ const diagnostics = path.resolve(required(values, "--diagnostics"));
416
+ const output = path.resolve(required(values, "--output"));
417
+ const rendererImage = required(values, "--renderer-image");
418
+ const adapterRelative = required(values, "--adapter");
419
+ const sourceSha = required(values, "--source-sha");
420
+ invariant(/^[0-9a-f]{40}$/.test(sourceSha), "source SHA must be exact");
421
+ const normalized = validateAdapterOutput(adapterOutput);
422
+ verifyRendererOutput(smokeOutput, rendererImage, {
423
+ scene: path.join(smokeInput, "scene.json"),
424
+ transcript: path.join(smokeInput, "complete-transcript.txt"),
425
+ projection: path.join(smokeInput, "public-projection.json"),
426
+ });
427
+ const sourceCoordinate = validateSourceCoordinate(readJson(sourceCoordinatePath, "source artifact coordinate"));
428
+ invariant(sourceCoordinate.sourceSha === sourceSha, "source artifact coordinate SHA mismatch");
429
+ ensureEmptyDirectory(output, "gate bundle");
430
+ for (const name of REQUIRED_ADAPTER_FILES) copyFile(path.join(adapterOutput, name), path.join(output, name));
431
+ copyFile(sourceCoordinatePath, path.join(output, "source-artifact.json"));
432
+ copyFile(path.join(diagnostics, "adapter.json"), path.join(output, "adapter.json"));
433
+ for (const name of listFiles(smokeOutput)) {
434
+ copyFile(path.join(smokeOutput, name), path.join(output, "smoke", name));
435
+ }
436
+ writeJson(path.join(output, "gate-receipt.json"), {
437
+ schema: "buildchain.auditable-demo-gate/v1",
438
+ status: "passed",
439
+ sourceRepository: sourceCoordinate.repository,
440
+ sourceSha,
441
+ sourceArtifact: {
442
+ id: sourceCoordinate.id,
443
+ name: sourceCoordinate.name,
444
+ digest: sourceCoordinate.digest,
445
+ runId: sourceCoordinate.runId,
446
+ expiresAt: sourceCoordinate.expiresAt,
447
+ },
448
+ adapter: {
449
+ path: adapterRelative,
450
+ sha256: readJson(path.join(diagnostics, "adapter.json"), "adapter execution").sha256,
451
+ },
452
+ renderer: {
453
+ image: rendererImage,
454
+ smokeManifestRoot: sha256(readRegular(path.join(smokeOutput, "manifest.json"), "smoke manifest")),
455
+ },
456
+ qualifiedInputs: {
457
+ transcript: sha256(readRegular(path.join(adapterOutput, "complete-transcript.txt"), "transcript")),
458
+ projection: sha256(readRegular(path.join(adapterOutput, "public-projection.json"), "projection")),
459
+ scene: sha256(readRegular(path.join(adapterOutput, "scene.json"), "scene")),
460
+ evidenceClass: normalized.projection.evidenceClass,
461
+ claimBoundary: normalized.projection.claimBoundary,
462
+ },
463
+ });
464
+ const root = writeChecksums(output);
465
+ const artifactName = `auditable-demo-gate-${sourceSha.slice(0, 12)}-${root.slice(7, 23)}`;
466
+ appendOutputs(values["--github-output"], {
467
+ "gate-root": root,
468
+ "gate-artifact-name": artifactName,
469
+ });
470
+ process.stdout.write(stableJson({ status: "passed", root, artifactName }));
471
+ }
472
+
473
+ function verifyGate(values) {
474
+ const bundle = path.resolve(required(values, "--bundle"));
475
+ const expectedRoot = required(values, "--expected-root");
476
+ const expectedImage = required(values, "--renderer-image");
477
+ const expectedSourceSha = required(values, "--source-sha");
478
+ invariant(DIGEST_PATTERN.test(expectedRoot), "expected gate root must be sha256");
479
+ invariant(verifyChecksums(bundle) === expectedRoot, "gate bundle root mismatch");
480
+ const receipt = readJson(path.join(bundle, "gate-receipt.json"), "gate receipt");
481
+ invariant(receipt.schema === "buildchain.auditable-demo-gate/v1" && receipt.status === "passed", "gate did not pass");
482
+ invariant(receipt.sourceSha === expectedSourceSha, "gate source SHA mismatch");
483
+ invariant(receipt.renderer?.image === expectedImage, "gate renderer image mismatch");
484
+ const normalized = validateAdapterOutput(bundle, false);
485
+ invariant(
486
+ receipt.qualifiedInputs?.transcript === sha256(readRegular(path.join(bundle, "complete-transcript.txt"), "transcript"))
487
+ && receipt.qualifiedInputs?.projection === sha256(readRegular(path.join(bundle, "public-projection.json"), "projection"))
488
+ && receipt.qualifiedInputs?.scene === sha256(readRegular(path.join(bundle, "scene.json"), "scene")),
489
+ "gate qualified input roots mismatch",
490
+ );
491
+ invariant(receipt.qualifiedInputs.evidenceClass === normalized.projection.evidenceClass, "gate evidence class drifted");
492
+ }
493
+
494
+ function finalizeMedia(values) {
495
+ const gateBundle = path.resolve(required(values, "--gate-bundle"));
496
+ const renderOutput = path.resolve(required(values, "--render-output"));
497
+ const output = path.resolve(required(values, "--output"));
498
+ const rendererImage = required(values, "--renderer-image");
499
+ const gateRoot = required(values, "--gate-root");
500
+ const sourceSha = required(values, "--source-sha");
501
+ verifyGate({
502
+ "--bundle": gateBundle,
503
+ "--expected-root": gateRoot,
504
+ "--renderer-image": rendererImage,
505
+ "--source-sha": sourceSha,
506
+ });
507
+ verifyRendererOutput(renderOutput, rendererImage, {
508
+ scene: path.join(gateBundle, "scene.json"),
509
+ transcript: path.join(gateBundle, "complete-transcript.txt"),
510
+ projection: path.join(gateBundle, "public-projection.json"),
511
+ });
512
+ ensureEmptyDirectory(output, "media bundle");
513
+ for (const name of listFiles(renderOutput)) {
514
+ const destination = name === "checksums.sha256" ? "renderer-checksums.sha256" : name;
515
+ copyFile(path.join(renderOutput, name), path.join(output, destination));
516
+ }
517
+ copyFile(path.join(gateBundle, "gate-receipt.json"), path.join(output, "gate-receipt.json"));
518
+ writeJson(path.join(output, "media-receipt.json"), {
519
+ schema: "buildchain.auditable-demo-media/v1",
520
+ status: "passed",
521
+ sourceSha,
522
+ qualifiedGateRoot: gateRoot,
523
+ rendererImage,
524
+ rendererManifestRoot: sha256(readRegular(path.join(renderOutput, "manifest.json"), "renderer manifest")),
525
+ });
526
+ const root = writeChecksums(output);
527
+ const artifactName = `auditable-demo-media-${sourceSha.slice(0, 12)}-${root.slice(7, 23)}`;
528
+ appendOutputs(values["--github-output"], {
529
+ "media-root": root,
530
+ "media-artifact-name": artifactName,
531
+ });
532
+ process.stdout.write(stableJson({ status: "passed", root, artifactName }));
533
+ }
534
+
535
+ function main(argv) {
536
+ const { command, values } = parseArguments(argv);
537
+ switch (command) {
538
+ case "run-adapter":
539
+ return runAdapter(values);
540
+ case "prepare-smoke":
541
+ return prepareSmoke(values);
542
+ case "finalize-gate":
543
+ return finalizeGate(values);
544
+ case "verify-gate":
545
+ return verifyGate(values);
546
+ case "finalize-media":
547
+ return finalizeMedia(values);
548
+ default:
549
+ throw new Error(`unknown command: ${command || "<empty>"}`);
550
+ }
551
+ }
552
+
553
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
554
+ if (invokedDirectly) {
555
+ try {
556
+ main(process.argv.slice(2));
557
+ } catch (error) {
558
+ process.stderr.write(`auditable-demo: ${error instanceof Error ? error.message : String(error)}\n`);
559
+ process.exitCode = 1;
560
+ }
561
+ }
562
+
563
+ export {
564
+ finalizeGate,
565
+ finalizeMedia,
566
+ prepareSmoke,
567
+ runAdapter,
568
+ sha256,
569
+ stableJson,
570
+ validateAdapterOutput,
571
+ validateSourceCoordinate,
572
+ verifyChecksums,
573
+ verifyGate,
574
+ verifyRendererOutput,
575
+ writeChecksums,
576
+ };
@@ -47,6 +47,7 @@ const requiredPaths = [
47
47
  "docs/readme-badges.md",
48
48
  "docs/release-passport.md",
49
49
  "docs/shifu-gate-profiles.md",
50
+ "docs/auditable-demo.md",
50
51
  "docs/release-propagation.md",
51
52
  "docs/site-bundle-contract.md",
52
53
  "docs/toolkit-observability.md",
@@ -66,6 +67,8 @@ const requiredPaths = [
66
67
  "scripts/generate-release-candidate-passport.mjs",
67
68
  "scripts/seal-macos-credential-input.mjs",
68
69
  "scripts/shifu-gate-profile.mjs",
70
+ "scripts/auditable-demo.mjs",
71
+ "scripts/resolve-artifact-coordinates.mjs",
69
72
  "scripts/artifact-relay-s3.mjs",
70
73
  "scripts/anchored-version-material.mjs",
71
74
  "scripts/npm-publish-dry-run.mjs",
@@ -112,6 +115,7 @@ const requiredPaths = [
112
115
  ".github/workflows/verify.yml",
113
116
  ".github/workflows/.build.yml",
114
117
  ".github/workflows/.gate-profile.yml",
118
+ ".github/workflows/.auditable-demo.yml",
115
119
  ".github/workflows/build.yml",
116
120
  ".github/workflows/build-surface-fixture.yml",
117
121
  ".github/workflows/candidate-lab.yml",
@@ -327,8 +327,10 @@ const manualMetaById = new Map(Object.entries({
327
327
  "publication-authority": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 105 },
328
328
  "github-governance-authority": { capabilityGroup: "governance-versioning", audience: ["maintainer", "release-operator", "agent"], maturity: "preview", order: 106 },
329
329
  "controller-evidence": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator", "agent"], maturity: "draft", order: 205 },
330
+ "auditable-demo": { capabilityGroup: "reusable-build", audience: ["consumer", "agent"], maturity: "preview", order: 207 },
330
331
  "binary-distribution": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 110 },
331
332
  "publish-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator"], maturity: "stable", order: 120 },
333
+ "release-activation-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 125 },
332
334
  "release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
333
335
  "stable-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer"], maturity: "preview", order: 135 },
334
336
  "observed-evidence-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 140 },
@@ -519,6 +521,7 @@ function nodeApiMeta(exportName) {
519
521
  "./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
520
522
  "./stable-candidate-ledger": { group: "governance-versioning", summary: "Immutable alpha candidate ledger, qualification, revocation, selection, and exact stable source-lock APIs." },
521
523
  "./release-propagation": { group: "site-and-propagation", summary: "Release propagation graph, plan, and exact upstream lock APIs." },
524
+ "./release-activation-transaction": { group: "release-passport-trust", summary: "Ordered cross-repository activation, exact receipt-set binding, retry, abort, rollback, and shadow-rehearsal APIs." },
522
525
  "./release-line-bootstrap": { group: "governance-versioning", summary: "Semver release-line bootstrap planning and version-state APIs." },
523
526
  "./buildchain-contract": { group: "governance-versioning", summary: "Runtime contract world and compatibility digest APIs for floating-ref drift checks." },
524
527
  "./controller-evidence": { group: "reusable-build", summary: "Project-independent controller descriptors, source/runtime-bound plans, receipts, aggregates, and validation APIs." },
@@ -866,6 +869,7 @@ function buildSiteBundle() {
866
869
  const surfaceById = new Map([
867
870
  ["build", "channel-build-router"],
868
871
  [".build", "reusable-build"],
872
+ [".auditable-demo", "auditable-demo"],
869
873
  ["web-surface", "site-app-deployment"],
870
874
  ["buildchain-ref-promotion", "release-governance"],
871
875
  ["release-line-bootstrap", "release-governance"],
@@ -889,6 +893,7 @@ function buildSiteBundle() {
889
893
  ["patrol-observed-evidence", "repository-patrol"],
890
894
  ]);
891
895
  const statusById = new Map([
896
+ [".auditable-demo", "preview"],
892
897
  ["release-propagation", "preview"],
893
898
  ["candidate-lab", "repository-internal"],
894
899
  ["build-surface-fixture", "repository-internal"],
@@ -77,11 +77,14 @@ function readAsset(artifactRoot, relativePath, expected) {
77
77
  const safePath = safeRelative(relativePath, "installer asset path");
78
78
  const absolute = path.resolve(artifactRoot, safePath);
79
79
  const rootPath = path.resolve(artifactRoot);
80
- if (
81
- !absolute.startsWith(`${rootPath}${path.sep}`) ||
82
- !fs.statSync(absolute).isFile()
83
- ) {
84
- throw new Error(`installer asset is missing: ${safePath}`);
80
+ if (!absolute.startsWith(`${rootPath}${path.sep}`)) {
81
+ throw new Error(`installer asset escapes artifact root: ${safePath}`);
82
+ }
83
+ const stat = fs.lstatSync(absolute);
84
+ if (!stat.isFile() || stat.isSymbolicLink()) {
85
+ throw new Error(
86
+ `installer asset must be a regular non-symlink file: ${safePath}`,
87
+ );
85
88
  }
86
89
  const bytes = fs.readFileSync(absolute);
87
90
  const observed = {
@@ -164,6 +167,13 @@ export function validateInstallerPublication({ publication, artifactRoot }) {
164
167
  if (!Number.isSafeInteger(asset.size) || asset.size < 1) {
165
168
  throw new Error(`${asset.name}.size is invalid`);
166
169
  }
170
+ const expectedContentType =
171
+ asset.name === "install.sh"
172
+ ? "text/x-shellscript; charset=utf-8"
173
+ : "text/plain; charset=utf-8";
174
+ if (asset.contentType !== expectedContentType) {
175
+ throw new Error(`${asset.name}.contentType is invalid`);
176
+ }
167
177
  const immutable = readAsset(
168
178
  artifactRoot,
169
179
  `${immutablePath}/${asset.name}`,