@kungfu-tech/buildchain 3.0.5-alpha.6 → 3.0.5-alpha.7

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 (37) hide show
  1. package/README.md +31 -0
  2. package/bin/buildchain.mjs +1 -1
  3. package/contracts/auditable-demo-media-profiles-v1.json +61 -0
  4. package/contracts/auditable-demo-scenario-v1.schema.json +153 -0
  5. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
  6. package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
  7. package/dist/site/buildchain-contract.json +57 -6
  8. package/dist/site/buildchain-site.json +38 -22
  9. package/dist/site/capability-registry.json +1 -1
  10. package/dist/site/cli-registry.json +6 -2
  11. package/dist/site/kfd-claims.json +49 -9
  12. package/dist/site/kfd-upstream-aggregate.json +1 -1
  13. package/dist/site/manual-registry.json +3 -3
  14. package/dist/site/node-api-registry.json +203 -21
  15. package/dist/site/page-registry.json +28 -8
  16. package/dist/site/public-surface-audit.json +94 -6
  17. package/dist/site/publication-authority-registry.json +42 -1
  18. package/dist/site/publication-registry.json +4 -4
  19. package/dist/site/site-manifest.json +7 -7
  20. package/dist/site/workflow-registry.json +63 -0
  21. package/docs/auditable-demo.md +74 -7
  22. package/docs/cli-reference.md +24 -2
  23. package/docs/node-api-reference.md +63 -45
  24. package/package.json +1 -1
  25. package/packages/core/buildchain-contract.js +36 -1
  26. package/packages/core/buildchain-publication-authority.js +2 -0
  27. package/packages/core/release-propagation-pickup.js +386 -0
  28. package/packages/core/release-propagation-release.js +61 -18
  29. package/packages/core/release-propagation.js +12 -2
  30. package/scripts/auditable-demo-platform.mjs +506 -0
  31. package/scripts/auditable-demo-renditions.mjs +6 -1
  32. package/scripts/auditable-demo.mjs +14 -4
  33. package/scripts/build-standalone-binary.mjs +4 -0
  34. package/scripts/buildchain-cli-help.mjs +2 -1
  35. package/scripts/check-inventory.mjs +5 -0
  36. package/scripts/generate-site-bundle.mjs +2 -0
  37. package/scripts/release-propagation.mjs +77 -2
@@ -0,0 +1,506 @@
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 path from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+
9
+ const DIGEST = /^sha256:[0-9a-f]{64}$/u;
10
+ const SAFE_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
11
+ const SAFE_MARKER = /^[a-z0-9][a-z0-9._:-]{0,79}$/u;
12
+ const NON_AUTHORITIES = [
13
+ "first-party-identity",
14
+ "system-identity",
15
+ "kfd-compliance",
16
+ "product-system-metadata",
17
+ "package-metadata",
18
+ "registry-history",
19
+ "scan-output",
20
+ "standalone-generation",
21
+ ];
22
+ const RENDITIONS = [
23
+ { id: "1080p", role: "primary", columns: 150, rows: 36, width: 1920, height: 1080 },
24
+ { id: "720p", role: "responsive", columns: 100, rows: 28, width: 1280, height: 720 },
25
+ ];
26
+ const STANDARD_MAX_SECONDS = 60;
27
+ const LONG_FORM_MAX_SECONDS = 180;
28
+
29
+ function durationPolicy(value = "standard") {
30
+ requireValue(value === "standard" || value === "long-form", "scenario duration class is invalid");
31
+ return {
32
+ durationClass: value,
33
+ maximumSeconds: value === "long-form" ? LONG_FORM_MAX_SECONDS : STANDARD_MAX_SECONDS,
34
+ };
35
+ }
36
+
37
+ function fail(message) {
38
+ throw new Error(`auditable demo platform: ${message}`);
39
+ }
40
+
41
+ function requireValue(condition, message) {
42
+ if (!condition) fail(message);
43
+ }
44
+
45
+ function stableValue(value) {
46
+ if (Array.isArray(value)) return value.map(stableValue);
47
+ if (value && typeof value === "object") {
48
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
49
+ }
50
+ return value;
51
+ }
52
+
53
+ function stableJson(value) {
54
+ return `${JSON.stringify(stableValue(value), null, 2)}\n`;
55
+ }
56
+
57
+ function rootBytes(value) {
58
+ return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
59
+ }
60
+
61
+ function rootJson(value) {
62
+ return rootBytes(Buffer.from(stableJson(value)));
63
+ }
64
+
65
+ function regular(file, label, maximum = 8 * 1024 * 1024) {
66
+ const metadata = fs.lstatSync(file);
67
+ requireValue(metadata.isFile() && !metadata.isSymbolicLink() && metadata.size <= maximum, `${label} must be a bounded regular file`);
68
+ return fs.readFileSync(file);
69
+ }
70
+
71
+ function readJson(file, label) {
72
+ try {
73
+ const value = JSON.parse(regular(file, label).toString("utf8"));
74
+ requireValue(value && typeof value === "object" && !Array.isArray(value), `${label} must contain an object`);
75
+ return value;
76
+ } catch (error) {
77
+ if (error instanceof SyntaxError) fail(`${label} is invalid JSON`);
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ function listBundleFiles(root, prefix = "") {
83
+ const entries = fs.readdirSync(path.join(root, prefix), { withFileTypes: true })
84
+ .sort((left, right) => left.name.localeCompare(right.name, "en"));
85
+ const files = [];
86
+ for (const entry of entries) {
87
+ requireValue(!entry.isSymbolicLink(), `bundle member must not be a symbolic link: ${path.join(prefix, entry.name)}`);
88
+ const relative = path.join(prefix, entry.name);
89
+ if (entry.isDirectory()) files.push(...listBundleFiles(root, relative));
90
+ else {
91
+ requireValue(entry.isFile(), `bundle member must be a regular file: ${relative}`);
92
+ files.push(relative.split(path.sep).join("/"));
93
+ }
94
+ }
95
+ return files;
96
+ }
97
+
98
+ function verifyChecksums(root, label) {
99
+ const resolved = path.resolve(root);
100
+ const bytes = regular(path.join(resolved, "checksums.sha256"), `${label} checksums`);
101
+ const rows = bytes.toString("utf8").split("\n").filter(Boolean);
102
+ const declared = new Set();
103
+ for (const row of rows) {
104
+ const match = /^([0-9a-f]{64}) ([^\0\r\n]+)$/u.exec(row);
105
+ requireValue(match, `${label} checksum row is invalid`);
106
+ const target = inside(resolved, match[2], `${label} checksum member`);
107
+ requireValue(!declared.has(match[2]), `${label} checksum member is repeated`);
108
+ declared.add(match[2]);
109
+ requireValue(rootBytes(regular(target, `${label} member`)) === `sha256:${match[1]}`, `${label} checksum mismatch: ${match[2]}`);
110
+ }
111
+ const actual = listBundleFiles(resolved).filter((name) => name !== "checksums.sha256");
112
+ requireValue(JSON.stringify([...declared].sort()) === JSON.stringify(actual), `${label} checksum member set is not exact`);
113
+ return rootBytes(bytes);
114
+ }
115
+
116
+ function inside(root, relative, label) {
117
+ requireValue(typeof relative === "string" && relative && !path.isAbsolute(relative), `${label} must be relative`);
118
+ const resolvedRoot = path.resolve(root);
119
+ const resolved = path.resolve(resolvedRoot, relative);
120
+ requireValue(resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`), `${label} escapes its root`);
121
+ return resolved;
122
+ }
123
+
124
+ function exactKeys(value, required, optional, label) {
125
+ requireValue(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
126
+ const allowed = new Set([...required, ...optional]);
127
+ for (const key of required) requireValue(Object.hasOwn(value, key), `${label}.${key} is required`);
128
+ for (const key of Object.keys(value)) requireValue(allowed.has(key), `${label}.${key} is not allowed`);
129
+ }
130
+
131
+ function validateProduct(product) {
132
+ exactKeys(product, ["id", "displayName", "binaryName"], [], "scenario.product");
133
+ requireValue(SAFE_ID.test(product.id), "scenario.product.id is invalid");
134
+ requireValue(typeof product.displayName === "string" && product.displayName.length > 0 && product.displayName.length <= 80, "scenario.product.displayName is invalid");
135
+ requireValue(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(product.binaryName), "scenario.product.binaryName is invalid");
136
+ }
137
+
138
+ function validateArtifact(artifact) {
139
+ exactKeys(artifact, ["platformId", "binaryPath", "metadataPath", "metadataContract", "runtimeDependencies"], [], "scenario.artifact");
140
+ requireValue(artifact.platformId === "linux-x64", "scenario artifact must be linux-x64");
141
+ for (const key of ["binaryPath", "metadataPath"]) inside("/scenario-root", artifact[key], `scenario.artifact.${key}`);
142
+ requireValue(typeof artifact.metadataContract === "string" && artifact.metadataContract.length > 0, "scenario artifact metadataContract is invalid");
143
+ requireValue(Array.isArray(artifact.runtimeDependencies) && artifact.runtimeDependencies.length === 0, "scenario artifact must be standalone");
144
+ }
145
+
146
+ function validateExecution(execution) {
147
+ exactKeys(execution, ["deterministic", "network", "secrets", "totalTimeoutSeconds", "environment"], ["durationClass"], "scenario.execution");
148
+ requireValue(execution.deterministic === true && execution.network === "none" && execution.secrets === "none", "scenario execution must be deterministic, network-disabled, and secret-free");
149
+ const policy = durationPolicy(execution.durationClass);
150
+ requireValue(Number.isInteger(execution.totalTimeoutSeconds) && execution.totalTimeoutSeconds >= 1 && execution.totalTimeoutSeconds <= policy.maximumSeconds, "scenario total timeout is invalid");
151
+ requireValue(execution.environment && typeof execution.environment === "object" && !Array.isArray(execution.environment), "scenario environment must be an object");
152
+ for (const [key, item] of Object.entries(execution.environment)) {
153
+ requireValue(/^[A-Z][A-Z0-9_]{0,63}$/u.test(key) && typeof item === "string" && item.length <= 256, `scenario environment entry is invalid: ${key}`);
154
+ }
155
+ return policy;
156
+ }
157
+
158
+ function validateStep(step, stepLabel, stepIds, maximumSeconds) {
159
+ exactKeys(step, ["id", "argv", "timeoutSeconds", "expectedExitCodes", "stdoutIncludes", "fileAssertions"], [], stepLabel);
160
+ requireValue(SAFE_ID.test(step.id) && !stepIds.has(step.id), `${stepLabel}.id is invalid or repeated`);
161
+ stepIds.add(step.id);
162
+ requireValue(Array.isArray(step.argv) && step.argv.length >= 1 && step.argv.length <= 64 && step.argv.every((item) => typeof item === "string" && !item.includes("\0") && item.length <= 512), `${stepLabel}.argv is invalid`);
163
+ requireValue(!Object.hasOwn(step, "command"), `${stepLabel} must not use a shell command string`);
164
+ requireValue(Number.isInteger(step.timeoutSeconds) && step.timeoutSeconds >= 1 && step.timeoutSeconds <= maximumSeconds, `${stepLabel}.timeoutSeconds is invalid`);
165
+ requireValue(Array.isArray(step.expectedExitCodes) && step.expectedExitCodes.length >= 1 && step.expectedExitCodes.length <= 4 && step.expectedExitCodes.every((item) => Number.isInteger(item) && item >= 0 && item <= 255), `${stepLabel}.expectedExitCodes is invalid`);
166
+ requireValue(Array.isArray(step.stdoutIncludes) && step.stdoutIncludes.every((item) => typeof item === "string" && item.length >= 1 && item.length <= 256), `${stepLabel}.stdoutIncludes is invalid`);
167
+ requireValue(Array.isArray(step.fileAssertions) && step.fileAssertions.length <= 32, `${stepLabel}.fileAssertions is invalid`);
168
+ for (const assertion of step.fileAssertions) {
169
+ exactKeys(assertion, ["path", "jsonEquals"], [], `${stepLabel}.fileAssertions[]`);
170
+ inside("/workspace", assertion.path, `${stepLabel} assertion path`);
171
+ requireValue(assertion.jsonEquals && typeof assertion.jsonEquals === "object" && !Array.isArray(assertion.jsonEquals), `${stepLabel} jsonEquals is invalid`);
172
+ }
173
+ }
174
+
175
+ function validateDemo(demo, index, demoIds, maximumSeconds) {
176
+ const label = `scenario.demos[${index}]`;
177
+ exactKeys(demo, ["id", "title", "claimBoundary", "steps"], [], label);
178
+ requireValue(SAFE_ID.test(demo.id) && !demoIds.has(demo.id), `${label}.id is invalid or repeated`);
179
+ demoIds.add(demo.id);
180
+ requireValue(typeof demo.title === "string" && demo.title.length > 0 && demo.title.length <= 120, `${label}.title is invalid`);
181
+ requireValue(typeof demo.claimBoundary === "string" && demo.claimBoundary.length > 0 && demo.claimBoundary.length <= 500, `${label}.claimBoundary is invalid`);
182
+ requireValue(Array.isArray(demo.steps) && demo.steps.length >= 1 && demo.steps.length <= 12, `${label}.steps is invalid`);
183
+ const stepIds = new Set();
184
+ demo.steps.forEach((step, stepIndex) => validateStep(step, `${label}.steps[${stepIndex}]`, stepIds, maximumSeconds));
185
+ }
186
+
187
+ export function validateScenario(value) {
188
+ exactKeys(value, ["schema", "product", "artifact", "execution", "renditions", "demos", "publication", "authority"], [], "scenario");
189
+ requireValue(value.schema === "buildchain.declarative-binary-demo/v1", "unsupported scenario schema");
190
+ validateProduct(value.product);
191
+ validateArtifact(value.artifact);
192
+ const executionPolicy = validateExecution(value.execution);
193
+ requireValue(JSON.stringify(value.renditions) === JSON.stringify(RENDITIONS), "scenario must declare both native rendition profiles exactly");
194
+ requireValue(Array.isArray(value.demos) && value.demos.length >= 1 && value.demos.length <= 8, "scenario requires 1 through 8 demos");
195
+ const demoIds = new Set();
196
+ value.demos.forEach((demo, index) => validateDemo(demo, index, demoIds, executionPolicy.maximumSeconds));
197
+ exactKeys(value.publication, ["evidencePath", "readmePath", "marker"], [], "scenario.publication");
198
+ inside("/repository", value.publication.evidencePath, "scenario.publication.evidencePath");
199
+ inside("/repository", value.publication.readmePath, "scenario.publication.readmePath");
200
+ requireValue(SAFE_MARKER.test(value.publication.marker), "scenario publication marker is invalid");
201
+ exactKeys(value.authority, ["grants", "nonAuthorities"], [], "scenario.authority");
202
+ requireValue(JSON.stringify(value.authority) === JSON.stringify({ grants: [], nonAuthorities: NON_AUTHORITIES }), "scenario authority boundary is invalid");
203
+ return value;
204
+ }
205
+
206
+ function validateCapture(capture, rendition, summaryRoot, durationClass) {
207
+ const policy = durationPolicy(durationClass);
208
+ requireValue(capture.schema === "buildchain.declarative-terminal-capture/v1", "capture schema mismatch");
209
+ requireValue(JSON.stringify(capture.dimensions) === JSON.stringify({ columns: rendition.columns, rows: rendition.rows }), "capture dimensions mismatch");
210
+ requireValue(capture.completion?.status === "qualified" && capture.completion?.reportRoot === summaryRoot, "capture completion mismatch");
211
+ requireValue(capture.exitCode === 0 && capture.authority?.classification === "volatile-terminal-observation", "capture authority or exit mismatch");
212
+ requireValue(JSON.stringify(capture.authority) === JSON.stringify({ classification: "volatile-terminal-observation", grants: [], nonAuthorities: NON_AUTHORITIES }), "capture grants authority");
213
+ requireValue(Array.isArray(capture.events) && capture.events.length === capture.completion.eventCount && capture.events.length > 0, "capture events mismatch");
214
+ requireValue(Number.isInteger(capture.durationMs) && capture.durationMs >= 500 && capture.durationMs <= policy.maximumSeconds * 1000, "capture duration exceeds its declared class");
215
+ let previousAtMs = -1;
216
+ for (const [index, event] of capture.events.entries()) {
217
+ requireValue(Number.isInteger(event.atMs) && event.atMs >= 0 && event.atMs < capture.durationMs && event.atMs >= previousAtMs, "capture event timeline is invalid");
218
+ requireValue(index > 0 || event.atMs === 0, "capture event timeline must start at zero");
219
+ previousAtMs = event.atMs;
220
+ }
221
+ return capture;
222
+ }
223
+
224
+ function projection(capture, transcript, demo, rendition, durationClass, sharedCaptureDurationMs) {
225
+ const lines = transcript.endsWith("\n") ? transcript.slice(0, -1).split("\n") : transcript.split("\n");
226
+ const policy = durationPolicy(durationClass);
227
+ const durationMs = Math.min(policy.maximumSeconds * 1000, sharedCaptureDurationMs + 1000);
228
+ const projected = {
229
+ schema: "kungfu.terminal-capture/v1",
230
+ command: capture.command,
231
+ dimensions: capture.dimensions,
232
+ durationMs: sharedCaptureDurationMs,
233
+ encoding: capture.encoding,
234
+ events: capture.events,
235
+ completion: capture.completion,
236
+ exitCode: capture.exitCode,
237
+ authority: capture.authority,
238
+ };
239
+ const scene = {
240
+ schema: "build-images.demo-scene/v1",
241
+ id: `${demo.id}-${rendition.id}`.slice(0, 64),
242
+ width: rendition.width,
243
+ height: rendition.height,
244
+ fps: policy.durationClass === "long-form" ? 10 : 15,
245
+ ...(policy.durationClass === "long-form" ? { durationClass: "long-form" } : {}),
246
+ durationMs,
247
+ title: demo.title,
248
+ commandLabel: capture.command,
249
+ background: "#0B1020",
250
+ accent: "#67E8A5",
251
+ };
252
+ const publicProjection = {
253
+ schema: "build-images.demo-projection/v1",
254
+ evidenceClass: "exact-standalone-binary-declarative-demo/v1",
255
+ claimBoundary: demo.claimBoundary,
256
+ cues: [{ startMs: 0, endMs: durationMs, transcriptLines: lines.slice(0, 80).map((_, index) => index + 1), annotation: "declared exact-binary scenario" }],
257
+ };
258
+ return { projected, scene, publicProjection };
259
+ }
260
+
261
+ function prepareOutput(output) {
262
+ if (!fs.existsSync(output)) return fs.mkdirSync(output, { recursive: true });
263
+ const metadata = fs.lstatSync(output);
264
+ requireValue(metadata.isDirectory() && !metadata.isSymbolicLink() && fs.readdirSync(output).length === 0, "output must be an empty directory");
265
+ }
266
+
267
+ export function adaptCapture({ artifactRoot, output }) {
268
+ const root = path.resolve(artifactRoot);
269
+ const manifest = readJson(path.join(root, "manifest.json"), "capture manifest");
270
+ requireValue(manifest.schema === "buildchain.declarative-demo-capture/v1" && manifest.status === "qualified", "capture manifest is not qualified");
271
+ const declaredRoot = manifest.root;
272
+ const { root: _root, ...manifestBody } = manifest;
273
+ requireValue(DIGEST.test(declaredRoot) && rootJson(manifestBody) === declaredRoot, "capture manifest root mismatch");
274
+ requireValue(manifest.authority?.grants?.length === 0 && JSON.stringify(manifest.authority?.nonAuthorities) === JSON.stringify(NON_AUTHORITIES), "capture manifest grants authority");
275
+ requireValue(Array.isArray(manifest.renditions) && manifest.renditions.length === 2, "capture rendition set is invalid");
276
+ const executionPolicy = durationPolicy(manifest.execution?.durationClass);
277
+ prepareOutput(output);
278
+ const set = [];
279
+ const loaded = RENDITIONS.map((expected, index) => {
280
+ const descriptor = manifest.renditions[index];
281
+ requireValue(descriptor.id === expected.id && descriptor.role === expected.role && descriptor.width === expected.width && descriptor.height === expected.height, `capture rendition ${index} mismatch`);
282
+ const transcriptBytes = regular(inside(root, descriptor.transcript, "capture transcript"), "capture transcript", 4 * 1024 * 1024);
283
+ const transcript = transcriptBytes.toString("utf8").replace(/\r\n/gu, "\n");
284
+ requireValue(transcript.trim().length > 0, "capture transcript is empty");
285
+ const summary = readJson(inside(root, descriptor.runSummary, "run summary"), "run summary");
286
+ requireValue(rootJson(summary) === descriptor.runSummaryRoot, "run summary root mismatch");
287
+ const captureBytes = regular(inside(root, descriptor.terminalCapture, "terminal capture"), "terminal capture", 4 * 1024 * 1024);
288
+ const capture = validateCapture(JSON.parse(captureBytes.toString("utf8")), expected, descriptor.runSummaryRoot, executionPolicy.durationClass);
289
+ requireValue(rootJson(capture) === descriptor.terminalCaptureRoot, "terminal capture root mismatch");
290
+ return { index, expected, descriptor, transcript, capture };
291
+ });
292
+ const sharedCaptureDurationMs = Math.max(...loaded.map((entry) => entry.capture.durationMs));
293
+ requireValue(sharedCaptureDurationMs <= executionPolicy.maximumSeconds * 1000, "native capture duration exceeds its declared class");
294
+ for (const { index, expected, transcript, capture } of loaded) {
295
+ const { projected, scene, publicProjection } = projection(
296
+ capture,
297
+ transcript,
298
+ manifest.demo,
299
+ expected,
300
+ executionPolicy.durationClass,
301
+ sharedCaptureDurationMs,
302
+ );
303
+ const suffix = index === 0 ? "" : "-720p";
304
+ fs.writeFileSync(path.join(output, `complete-transcript${suffix}.txt`), transcript);
305
+ fs.writeFileSync(path.join(output, `terminal-capture${suffix}.json`), stableJson(projected));
306
+ fs.writeFileSync(path.join(output, `scene${suffix}.json`), stableJson(scene));
307
+ fs.writeFileSync(path.join(output, `public-projection${suffix}.json`), stableJson(publicProjection));
308
+ set.push({ id: expected.id, role: expected.role, transcript: `complete-transcript${suffix}.txt`, projection: `public-projection${suffix}.json`, scene: `scene${suffix}.json`, terminalCapture: `terminal-capture${suffix}.json`, captureRoot: rootJson(projected) });
309
+ }
310
+ requireValue(set[0].captureRoot !== set[1].captureRoot, "native rendition capture roots must differ");
311
+ fs.writeFileSync(path.join(output, "rendition-set.json"), stableJson({
312
+ schema: "kungfu.auditable-demo.rendition-set/v1",
313
+ renditions: set,
314
+ authority: { classification: "capture-routing-metadata", grants: [], nonAuthorities: ["publication-authority", "runtime-authority", ...NON_AUTHORITIES] },
315
+ }));
316
+ return { demoId: manifest.demo.id, captureRoot: declaredRoot, renditionRoots: set.map((entry) => entry.captureRoot) };
317
+ }
318
+
319
+ function copyRegular(source, destination, label) {
320
+ const bytes = regular(source, label, 64 * 1024 * 1024);
321
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
322
+ fs.writeFileSync(destination, bytes);
323
+ return { path: path.basename(destination), bytes: bytes.length, root: rootBytes(bytes) };
324
+ }
325
+
326
+ function replaceReadmeBlock(readme, marker, block) {
327
+ const start = `<!-- ${marker}:start -->`;
328
+ const end = `<!-- ${marker}:end -->`;
329
+ const first = readme.indexOf(start);
330
+ const last = readme.indexOf(end);
331
+ requireValue((first === -1) === (last === -1), "README materialization markers are incomplete");
332
+ if (first !== -1) {
333
+ requireValue(readme.indexOf(start, first + start.length) === -1 && readme.indexOf(end, last + end.length) === -1 && last > first, "README materialization markers are ambiguous");
334
+ return `${readme.slice(0, first)}${block}${readme.slice(last + end.length)}`;
335
+ }
336
+ const headingEnd = readme.indexOf("\n");
337
+ requireValue(headingEnd !== -1, "README must contain a title line");
338
+ return `${readme.slice(0, headingEnd + 1)}\n${block}\n${readme.slice(headingEnd + 1)}`;
339
+ }
340
+
341
+ function authorityBoundary() {
342
+ return {
343
+ grants: [],
344
+ nonAuthorities: NON_AUTHORITIES,
345
+ authorizationSources: [
346
+ "exact-release-passport",
347
+ "core-policy",
348
+ "work-or-warrant",
349
+ "explicit-capability-grant",
350
+ "runtime-isolation",
351
+ ],
352
+ productSystemRole: "assembly-and-distribution-metadata-only",
353
+ };
354
+ }
355
+
356
+ export function materializeDemo({ repositoryRoot, scenarioPath, demoId, captureRoot, gateBundle, mediaBundle, buildchainSha, rendererImage }) {
357
+ const repository = path.resolve(repositoryRoot);
358
+ const scenario = validateScenario(readJson(path.resolve(scenarioPath), "scenario"));
359
+ const demo = scenario.demos.find((entry) => entry.id === demoId);
360
+ requireValue(demo, `unknown demo id: ${demoId}`);
361
+ const captureManifest = readJson(path.join(path.resolve(captureRoot), "manifest.json"), "capture manifest");
362
+ requireValue(captureManifest.demo?.id === demoId && captureManifest.scenarioRoot === rootJson(scenario), "capture does not bind the exact scenario demo");
363
+ const gateReceipt = readJson(path.join(path.resolve(gateBundle), "gate-receipt.json"), "gate receipt");
364
+ const mediaReceipt = readJson(path.join(path.resolve(mediaBundle), "media-receipt.json"), "media receipt");
365
+ const gateRoot = verifyChecksums(gateBundle, "Gate bundle");
366
+ const mediaRoot = verifyChecksums(mediaBundle, "media bundle");
367
+ requireValue(gateReceipt.status === "passed" && mediaReceipt.status === "passed", "Gate and media receipts must pass");
368
+ requireValue(mediaReceipt.qualifiedGateRoot === gateRoot, "media receipt is not bound to the exact qualified Gate");
369
+ const sourceCoordinate = readJson(path.join(path.resolve(captureRoot), "source-coordinate.json"), "source coordinate");
370
+ requireValue(rootJson(sourceCoordinate) === captureManifest.sourceCoordinateRoot, "source coordinate root mismatch");
371
+ requireValue(DIGEST.test(buildchainSha) || /^[0-9a-f]{40}$/u.test(buildchainSha), "Buildchain runtime coordinate is invalid");
372
+ requireValue(/@sha256:[0-9a-f]{64}$/u.test(rendererImage), "renderer image must be immutable");
373
+ const evidencePreimage = {
374
+ schema: "buildchain.declarative-demo-evidence-root/v1",
375
+ scenarioRoot: captureManifest.scenarioRoot,
376
+ captureRoot: captureManifest.root,
377
+ gateRoot,
378
+ mediaRoot,
379
+ demoId,
380
+ };
381
+ const evidenceRoot = rootJson(evidencePreimage);
382
+ const evidenceDirectory = inside(repository, `${scenario.publication.evidencePath}/${evidenceRoot.slice(7)}/${demoId}`, "evidence directory");
383
+ fs.mkdirSync(evidenceDirectory, { recursive: true });
384
+ const publicFiles = [];
385
+ for (const name of ["demo.gif", "demo.mp4", "demo.webm", "demo-720p.mp4", "demo-720p.webm", "poster.png", "media-receipt.json", "gate-receipt.json", "manifest.json", "media-inspection.json", "media-probe.json", "renderer-checksums.sha256"]) {
386
+ const source = path.join(path.resolve(mediaBundle), name);
387
+ if (fs.existsSync(source)) publicFiles.push(copyRegular(source, path.join(evidenceDirectory, name), `media ${name}`));
388
+ }
389
+ copyRegular(path.join(path.resolve(gateBundle), "gate-receipt.json"), path.join(evidenceDirectory, "qualified-gate-receipt.json"), "qualified Gate receipt");
390
+ copyRegular(path.join(path.resolve(captureRoot), "manifest.json"), path.join(evidenceDirectory, "capture-manifest.json"), "capture manifest");
391
+ copyRegular(path.join(path.resolve(captureRoot), "source-coordinate.json"), path.join(evidenceDirectory, "source-coordinate.json"), "source coordinate");
392
+ const passportBody = {
393
+ schema: "buildchain.declarative-demo-release-passport/v1",
394
+ status: "qualified",
395
+ product: scenario.product,
396
+ demo: { id: demo.id, title: demo.title, claimBoundary: demo.claimBoundary },
397
+ evidenceRoot,
398
+ scenarioRoot: captureManifest.scenarioRoot,
399
+ capture: { root: captureManifest.root, binary: captureManifest.artifact, networkIsolation: captureManifest.networkIsolation },
400
+ source: sourceCoordinate,
401
+ gate: { root: evidencePreimage.gateRoot },
402
+ media: { root: evidencePreimage.mediaRoot, profile: mediaReceipt.qualification?.profile?.id || "responsive-web-delivery-v1", qualificationRoot: mediaReceipt.qualificationRoot || mediaReceipt.qualification?.qualificationRoot || "" },
403
+ toolchain: { buildchainSha, rendererImage },
404
+ authority: authorityBoundary(),
405
+ };
406
+ const passport = { ...passportBody, passportRoot: rootJson(passportBody) };
407
+ fs.writeFileSync(path.join(evidenceDirectory, "release-passport.json"), stableJson(passport));
408
+ const publicEvidence = { ...evidencePreimage, evidenceRoot, passportRoot: passport.passportRoot, source: sourceCoordinate, files: publicFiles.sort((left, right) => left.path.localeCompare(right.path)) };
409
+ fs.writeFileSync(path.join(evidenceDirectory, "public-evidence.json"), stableJson(publicEvidence));
410
+ const relative = path.relative(repository, evidenceDirectory).split(path.sep).join("/");
411
+ const marker = scenario.demos.length === 1
412
+ ? scenario.publication.marker
413
+ : `${scenario.publication.marker}:${demo.id}`;
414
+ const commandLines = demo.steps.map((step) => `$ ${scenario.product.binaryName} ${step.argv.join(" ")}`.trim()).join("\n");
415
+ const block = [
416
+ `<!-- ${marker}:start -->`,
417
+ `## ${demo.title}`,
418
+ "",
419
+ `[![${demo.title}](${relative}/demo.gif)](${relative}/public-evidence.json)`,
420
+ "",
421
+ "Animation scenario:",
422
+ "",
423
+ "```text",
424
+ commandLines,
425
+ "```",
426
+ "",
427
+ `Native renditions: [1080p MP4](${relative}/demo.mp4) · [1080p WebM](${relative}/demo.webm) · [720p MP4](${relative}/demo-720p.mp4) · [720p WebM](${relative}/demo-720p.webm)`,
428
+ "",
429
+ `[Static poster / reduced-motion fallback](${relative}/poster.png)`,
430
+ "",
431
+ "<details>",
432
+ "<summary>Evidence and claim boundary</summary>",
433
+ "",
434
+ `${demo.claimBoundary}`,
435
+ "",
436
+ `[Release Passport](${relative}/release-passport.json) · [auditable evidence](${relative}/public-evidence.json)`,
437
+ "",
438
+ "</details>",
439
+ `<!-- ${marker}:end -->`,
440
+ ].join("\n");
441
+ const readmePath = inside(repository, scenario.publication.readmePath, "README path");
442
+ const readme = regular(readmePath, "README", 4 * 1024 * 1024).toString("utf8");
443
+ fs.writeFileSync(readmePath, replaceReadmeBlock(readme, marker, block));
444
+ return { ok: true, demoId, evidenceRoot, evidenceDirectory: relative, passportRoot: passport.passportRoot };
445
+ }
446
+
447
+ function parseArgs(argv) {
448
+ const values = {};
449
+ for (let index = 0; index < argv.length; index += 2) {
450
+ const key = argv[index];
451
+ const value = argv[index + 1];
452
+ requireValue(key?.startsWith("--") && value !== undefined && !(key in values), `invalid argument near ${key || "<empty>"}`);
453
+ values[key.slice(2)] = value;
454
+ }
455
+ return values;
456
+ }
457
+
458
+ function main(argv = process.argv.slice(2)) {
459
+ const directAdapter = argv[0]?.startsWith("--");
460
+ const [command, ...rest] = directAdapter ? ["adapt", ...argv] : argv;
461
+ const args = parseArgs(rest);
462
+ if (command === "validate") {
463
+ const scenario = validateScenario(readJson(path.resolve(args.scenario), "scenario"));
464
+ process.stdout.write(stableJson({ ok: true, scenarioRoot: rootJson(scenario), demoIds: scenario.demos.map((entry) => entry.id) }));
465
+ return;
466
+ }
467
+ if (command === "list") {
468
+ const scenario = validateScenario(readJson(path.resolve(args.scenario), "scenario"));
469
+ process.stdout.write(`${scenario.demos.map((entry) => entry.id).join("\n")}\n`);
470
+ return;
471
+ }
472
+ if (command === "publication") {
473
+ const scenario = validateScenario(readJson(path.resolve(args.scenario), "scenario"));
474
+ process.stdout.write(stableJson(scenario.publication));
475
+ return;
476
+ }
477
+ if (command === "adapt") {
478
+ const result = adaptCapture({ artifactRoot: path.resolve(args["artifact-root"]), output: path.resolve(args.output) });
479
+ process.stdout.write(stableJson({ ok: true, ...result }));
480
+ return;
481
+ }
482
+ if (command === "materialize") {
483
+ const result = materializeDemo({
484
+ repositoryRoot: path.resolve(args.repository),
485
+ scenarioPath: path.resolve(args.scenario),
486
+ demoId: args["demo-id"],
487
+ captureRoot: path.resolve(args.capture),
488
+ gateBundle: path.resolve(args.gate),
489
+ mediaBundle: path.resolve(args.media),
490
+ buildchainSha: args["buildchain-sha"],
491
+ rendererImage: args["renderer-image"],
492
+ });
493
+ process.stdout.write(stableJson(result));
494
+ return;
495
+ }
496
+ fail(`unknown command: ${command || "<empty>"}`);
497
+ }
498
+
499
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
500
+ try {
501
+ main();
502
+ } catch (error) {
503
+ console.error(error.message);
504
+ process.exitCode = 1;
505
+ }
506
+ }
@@ -29,7 +29,8 @@ export function validateTerminalCapture(value, scene, helpers) {
29
29
  exactKeys(value.dimensions, ["columns", "rows"], [], "terminalCapture.dimensions");
30
30
  integer(value.dimensions.columns, 80, 200, "terminalCapture.dimensions.columns");
31
31
  integer(value.dimensions.rows, 24, 80, "terminalCapture.dimensions.rows");
32
- const durationMs = integer(value.durationMs, 500, 60000, "terminalCapture.durationMs");
32
+ const maximumDurationMs = scene.durationClass === "long-form" ? 180000 : 60000;
33
+ const durationMs = integer(value.durationMs, 500, maximumDurationMs, "terminalCapture.durationMs");
33
34
  invariant(
34
35
  durationMs <= scene.durationMs && scene.durationMs - durationMs <= 2000,
35
36
  "terminal capture duration must end within two seconds of the scene",
@@ -117,6 +118,10 @@ export function validateRenditionSet(output, helpers) {
117
118
  return { ...entry, files: Object.fromEntries(["transcript", "projection", "scene", "terminalCapture"].map((key) => [key, entry[key]])), transcript, lines, scene, projection, capture };
118
119
  });
119
120
  invariant(normalized[0].captureRoot !== normalized[1].captureRoot, "native rendition capture roots must be distinct");
121
+ invariant(
122
+ (normalized[0].scene.durationClass ?? "standard") === (normalized[1].scene.durationClass ?? "standard"),
123
+ "native rendition duration classes must match",
124
+ );
120
125
  invariant(
121
126
  JSON.stringify(normalized[0].capture.dimensions) !== JSON.stringify(normalized[1].capture.dimensions),
122
127
  "native rendition PTY dimensions must be distinct",
@@ -36,6 +36,10 @@ const OPTIONAL_ADAPTER_FILES = [
36
36
  ];
37
37
  const MAX_TERMINAL_CAPTURE_BYTES = 4 * 1024 * 1024;
38
38
  const MAX_TERMINAL_CAPTURE_EVENTS = 10_000;
39
+ const STANDARD_MAX_DURATION_MS = 60_000;
40
+ const LONG_FORM_MAX_DURATION_MS = 180_000;
41
+ const LONG_FORM_MAX_FPS = 10;
42
+ const MAX_RENDER_FRAMES = 1_800;
39
43
 
40
44
  function invariant(condition, message) {
41
45
  if (!condition) throw new Error(message);
@@ -186,15 +190,20 @@ function validateScene(value) {
186
190
  exactKeys(
187
191
  value,
188
192
  ["schema", "id", "width", "height", "fps", "durationMs", "title"],
189
- ["commandLabel", "background", "accent"],
193
+ ["durationClass", "commandLabel", "background", "accent"],
190
194
  "scene",
191
195
  );
192
196
  invariant(value.schema === "build-images.demo-scene/v1", "unsupported scene schema");
193
197
  invariant(/^[a-z0-9][a-z0-9._-]{0,63}$/.test(value.id), "scene.id is invalid");
194
198
  integer(value.width, 640, 1920, "scene.width");
195
199
  integer(value.height, 360, 1080, "scene.height");
196
- integer(value.fps, 1, 30, "scene.fps");
197
- integer(value.durationMs, 500, 60000, "scene.durationMs");
200
+ const durationClass = value.durationClass ?? "standard";
201
+ invariant(durationClass === "standard" || durationClass === "long-form", "scene.durationClass is invalid");
202
+ const maximumDurationMs = durationClass === "long-form" ? LONG_FORM_MAX_DURATION_MS : STANDARD_MAX_DURATION_MS;
203
+ const maximumFps = durationClass === "long-form" ? LONG_FORM_MAX_FPS : 30;
204
+ integer(value.fps, 1, maximumFps, "scene.fps");
205
+ integer(value.durationMs, 500, maximumDurationMs, "scene.durationMs");
206
+ invariant(Math.ceil((value.durationMs / 1000) * value.fps) <= MAX_RENDER_FRAMES, "scene exceeds the deterministic source-frame bound");
198
207
  text(value.title, 1, 120, "scene.title");
199
208
  if (value.commandLabel !== undefined) text(value.commandLabel, 0, 160, "scene.commandLabel");
200
209
  for (const key of ["background", "accent"]) {
@@ -206,6 +215,7 @@ function validateScene(value) {
206
215
  width: value.width,
207
216
  height: value.height,
208
217
  fps: value.fps,
218
+ ...(value.durationClass === undefined ? {} : { durationClass }),
209
219
  durationMs: value.durationMs,
210
220
  title: value.title,
211
221
  commandLabel: value.commandLabel ?? "",
@@ -524,7 +534,7 @@ function validateBudgetBasis(entry, label) {
524
534
  MAX_BUNDLE_MEMBER_BYTES,
525
535
  `${label}.budgetBasis.observedBytes`,
526
536
  );
527
- const multiplier = integer(entry.budgetBasis.multiplier, 1, 64, `${label}.budgetBasis.multiplier`);
537
+ const multiplier = integer(entry.budgetBasis.multiplier, 1, 128, `${label}.budgetBasis.multiplier`);
528
538
  invariant(entry.budgetBasis.rounding === "next-power-of-two", `${label}.budgetBasis.rounding is unsupported`);
529
539
  const expected = 2 ** Math.ceil(Math.log2(observedBytes * multiplier));
530
540
  invariant(entry.maximumBytes === expected, `${label}.maximumBytes does not match its measured budget basis`);
@@ -352,7 +352,11 @@ export function buildStandaloneBinary({
352
352
  name,
353
353
  version,
354
354
  platform: triple,
355
+ platformId: process.platform === "linux" && process.arch === "x64" ? "linux-x64" : triple,
355
356
  binary: relativePath(cwd, binaryPath),
357
+ sha256: crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex"),
358
+ sourceSha: sourceSha(cwd),
359
+ runtimeDependencies: [],
356
360
  archive: relativePath(cwd, archivePath),
357
361
  node: process.version,
358
362
  observability: {
@@ -208,7 +208,7 @@ export const BUILDCHAIN_USAGE = `Usage:
208
208
  [--execute] [--json]
209
209
  buildchain paper status [--cwd <dir>] [--json]
210
210
  buildchain paper resume [--cwd <dir>] [--buildchain-ref <ref>] [--execute] [--json]
211
- buildchain release-propagation <plan|write-lock> ...
211
+ buildchain release-propagation <plan|write-lock|work|pickup> ...
212
212
  buildchain badges readme [--cwd <dir>] [--readme <path>] [--check] [--write] [--json]
213
213
  buildchain badges bundle [--cwd <dir>] [--readme <path>] [--claims <csv>] [--check] [--write] [--json]
214
214
  buildchain homebrew update-formula --package <name> --release-passport <file-or-url> [--write] [--json]
@@ -246,6 +246,7 @@ Examples:
246
246
  buildchain paper preflight --json
247
247
  buildchain paper status --json
248
248
  buildchain release-propagation plan --graph graph.json --upstream-release release.json --json
249
+ buildchain release-propagation pickup plan --config manual-upstreams.json --source-id buildchain --channel release --current-version 3.0.3 --json
249
250
  buildchain kfd status --json
250
251
  buildchain kfd schema list --json
251
252
  buildchain kfd 1 witness --json
@@ -73,6 +73,7 @@ const requiredPaths = [
73
73
  "docs/github-artifact-attestation.md",
74
74
  "docs/shifu-gate-profiles.md",
75
75
  "docs/auditable-demo.md",
76
+ "contracts/auditable-demo-scenario-v1.schema.json",
76
77
  "contracts/auditable-demo-media-profiles-v1.json",
77
78
  "contracts/evidence/auditable-demo-web-delivery-v1.json",
78
79
  "contracts/evidence/auditable-demo-responsive-web-delivery-v1.json",
@@ -103,6 +104,8 @@ const requiredPaths = [
103
104
  "scripts/seal-macos-credential-input.mjs",
104
105
  "scripts/shifu-gate-profile.mjs",
105
106
  "scripts/auditable-demo.mjs",
107
+ "scripts/auditable-demo-platform.mjs",
108
+ "scripts/auditable-demo-capture.py",
106
109
  "scripts/resolve-artifact-coordinates.mjs",
107
110
  "scripts/artifact-relay-s3.mjs",
108
111
  "scripts/anchored-version-material.mjs",
@@ -154,6 +157,8 @@ const requiredPaths = [
154
157
  ".github/workflows/.build.yml",
155
158
  ".github/workflows/.gate-profile.yml",
156
159
  ".github/workflows/.auditable-demo.yml",
160
+ ".github/workflows/.declarative-auditable-demo.yml",
161
+ ".github/workflows/auditable-demo.yml",
157
162
  ".github/workflows/build.yml",
158
163
  ".github/workflows/build-surface-fixture.yml",
159
164
  ".github/workflows/candidate-lab.yml",
@@ -767,6 +767,7 @@ function buildSiteBundle() {
767
767
  ["build", "channel-build-router"],
768
768
  [".build", "reusable-build"],
769
769
  [".auditable-demo", "auditable-demo"],
770
+ [".declarative-auditable-demo", "declarative-auditable-demo"],
770
771
  ["web-surface", "site-app-deployment"],
771
772
  ["buildchain-ref-promotion", "release-governance"],
772
773
  ["release-line-bootstrap", "release-governance"],
@@ -793,6 +794,7 @@ function buildSiteBundle() {
793
794
  ]);
794
795
  const statusById = new Map([
795
796
  [".auditable-demo", "preview"],
797
+ [".declarative-auditable-demo", "preview"],
796
798
  ["release-propagation", "preview"],
797
799
  ["candidate-lab", "repository-internal"],
798
800
  ["build-surface-fixture", "repository-internal"],